diff --git a/src/document-check.js b/src/document-check.js index 17e04c181..eba44ed36 100644 --- a/src/document-check.js +++ b/src/document-check.js @@ -1,9 +1,12 @@ /* eslint-disable no-console */ +import fs from 'fs-extra' import { globby } from 'globby' +import kleur from 'kleur' import Listr from 'listr' +import merge from 'merge-options' import { compileSnippets } from 'typescript-docs-verifier' -import { hasTsconfig } from './utils.js' +import { formatCode, formatError, fromRoot, hasTsconfig, readJson } from './utils.js' /** * @typedef {import("./types").GlobalOptions} GlobalOptions * @typedef {import("./types").DocsVerifierOptions} DocsVerifierOptions @@ -23,33 +26,61 @@ const tasks = new Listr( * @param {Task} task */ task: async (ctx, task) => { - let tsconfigPath = 'tsconfig.json' - let markdownFiles = ['README.md'] + const isWindows = process.platform === 'win32' - if (ctx.tsConfigPath) { - tsconfigPath = `${ctx.tsConfigPath}/tsconfig.json` - } + if (!isWindows) { + const configPath = './tsconfig-doc-check.aegir.json' - if (ctx.inputFiles) { - markdownFiles = await globby(ctx.inputFiles) - } + let userTSConfig = {} + let markdownFiles = ['README.md'] + + if (ctx.tsConfigPath && ctx.tsConfigPath !== '.') { + userTSConfig = readJson(`${ctx.tsConfigPath}/tsconfig.json`) + } else { + userTSConfig = readJson(fromRoot('tsconfig.json')) + } + + if (ctx.inputFiles) { + markdownFiles = await globby(ctx.inputFiles) + } + + try { + fs.writeJsonSync( + configPath, + merge.apply({ concatArrays: true }, [ + userTSConfig, + { + compilerOptions: { + target: 'esnext', + module: 'esnext', + noImplicitAny: true, + noEmit: true + } + } + ]) + ) + + const results = await compileSnippets({ markdownFiles, project: configPath }) - compileSnippets({ markdownFiles, project: tsconfigPath }) - .then((results) => { results.forEach((result) => { if (result.error) { - console.log(`Error compiling example code block ${result.index} in file ${result.file}`) - console.log(result.error.message) - console.log('Original code:') - console.log(result.snippet) + process.exitCode = 1 + console.log(kleur.red().bold(`Error compiling example code block ${result.index} in file ${result.file}:`)) + console.log(formatError(result.error)) + console.log(kleur.blue().bold('Original code:')) + console.log(formatCode(result.snippet, result.linesWithErrors)) } }) - }) - .catch((error) => { - console.error('Error compiling TypeScript snippets', error) - }) + } catch (err) { + console.log('Error in trying to compile Typescript code', err) + } finally { + fs.removeSync(configPath) + fs.removeSync(fromRoot('dist', 'tsconfig-doc-check.aegir.tsbuildinfo')) + } + } else { + console.log(kleur.red('The underlying plugin used for TS-doc checks currently does not support Windows OS (See Github issue https://github.com/bbc/typescript-docs-verifier/issues/26). Skipping document check.')) + } } - } ], { diff --git a/src/utils.js b/src/utils.js index 710435236..b3508ec07 100644 --- a/src/utils.js +++ b/src/utils.js @@ -15,6 +15,7 @@ import envPaths from 'env-paths' import { execa } from 'execa' import extract from 'extract-zip' import fs from 'fs-extra' +import kleur from 'kleur' import Listr from 'listr' import { minimatch } from 'minimatch' import lockfile from 'proper-lockfile' @@ -513,3 +514,28 @@ async function * _glob (base, dir, pattern, options) { } } } + +/** + * + * @param {Error} error + * @returns + */ +export const formatError = (error) => ' ' + error.message.split('\n').join('\n ') + +/** + * + * @param {string} code + * @param {number[]} errorLines + * @returns + */ +export const formatCode = (code, errorLines) => { + const lines = code.split('\n').map((line, index) => { + const lineNumber = index + 1 + if (errorLines.includes(lineNumber)) { + return kleur.red().bold(`${String(lineNumber).padStart(2)}| ${line}`) + } else { + return `${String(lineNumber).padStart(2)}| ${line}` + } + }) + return ' ' + lines.join('\n ') +} diff --git a/test/document-check.js b/test/document-check.js new file mode 100644 index 000000000..b5fc243b5 --- /dev/null +++ b/test/document-check.js @@ -0,0 +1,41 @@ +/* eslint-env mocha */ + +import { createRequire } from 'module' +import path from 'path' +import { fileURLToPath } from 'url' +import { execa } from 'execa' +import { expect } from '../utils/chai.js' + +const require = createRequire(import.meta.url) +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const bin = require.resolve('../src/index.js') +const isWindows = process.platform === 'win32' + +describe('document check', () => { + // Skip tests on windows until https://github.com/bbc/typescript-docs-verifier/issues/26 is resolved + if (!isWindows) { + it('should fail for errant typescript in docs', async () => { + const cwd = path.join(__dirname, 'fixtures/document-check/ts-fail') + + await expect(execa(bin, ['doc-check', '--inputFiles', `${cwd}/*.md`, '--tsConfigPath', `${cwd}`])) + .to.eventually.be.rejected() + .with.property('stdout') + .that.include('Error compiling example code block 1') + }) + + it('should pass for correct typescript in docs', async () => { + const cwd = path.join(__dirname, 'fixtures/document-check/pass') + + await expect(execa(bin, ['doc-check', '--inputFiles', `${cwd}/*.md`])).to.eventually.be.fulfilled() + }) + + it('should fail for missing tsconfig.json', async () => { + const cwd = path.join(__dirname, 'fixtures/document-check/tsconfig-fail') + + await expect(execa(bin, ['doc-check', '--inputFiles', `${cwd}/*.md`, '--tsConfigPath', `${cwd}`])) + .to.eventually.be.rejected() + .with.property('stderr') + .that.include('no such file or directory') + }) + } +}) diff --git a/test/fixtures/document-check/pass/GOODREADME.md b/test/fixtures/document-check/pass/GOODREADME.md new file mode 100644 index 000000000..3097c2fae --- /dev/null +++ b/test/fixtures/document-check/pass/GOODREADME.md @@ -0,0 +1,3 @@ +```ts +export const a = 1; +``` \ No newline at end of file diff --git a/test/fixtures/document-check/pass/tsconfig.json b/test/fixtures/document-check/pass/tsconfig.json new file mode 100644 index 000000000..7b352c7fb --- /dev/null +++ b/test/fixtures/document-check/pass/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./../../../../src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": true, + "isolatedModules": true + }, + "include": [ + "package.json", + "src", + "test", + "utils" + ] +} diff --git a/test/fixtures/document-check/ts-fail/ANOTHERBADREADME.md b/test/fixtures/document-check/ts-fail/ANOTHERBADREADME.md new file mode 100644 index 000000000..9b4f7fd46 --- /dev/null +++ b/test/fixtures/document-check/ts-fail/ANOTHERBADREADME.md @@ -0,0 +1,3 @@ +```typescript +still.bad.code +``` \ No newline at end of file diff --git a/test/fixtures/document-check/ts-fail/BADREADME.md b/test/fixtures/document-check/ts-fail/BADREADME.md new file mode 100644 index 000000000..50378f397 --- /dev/null +++ b/test/fixtures/document-check/ts-fail/BADREADME.md @@ -0,0 +1,3 @@ +```typescript + wrong.code() +``` \ No newline at end of file diff --git a/test/fixtures/document-check/ts-fail/tsconfig.json b/test/fixtures/document-check/ts-fail/tsconfig.json new file mode 100644 index 000000000..0d2cb5e2b --- /dev/null +++ b/test/fixtures/document-check/ts-fail/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./../../../../src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": true + }, + "include": [ + "package.json", + "src", + "test", + "utils" + ] +} diff --git a/test/fixtures/document-check/tsconfig-fail/GOODREADME.md b/test/fixtures/document-check/tsconfig-fail/GOODREADME.md new file mode 100644 index 000000000..3097c2fae --- /dev/null +++ b/test/fixtures/document-check/tsconfig-fail/GOODREADME.md @@ -0,0 +1,3 @@ +```ts +export const a = 1; +``` \ No newline at end of file diff --git a/test/node.js b/test/node.js index ec45a8de0..986c3d25b 100644 --- a/test/node.js +++ b/test/node.js @@ -3,6 +3,7 @@ import './docs.js' import './lint.js' import './fixtures.js' import './dependants.js' +import './document-check.js' import './dependency-check.js' import './utils/echo-server.js' import './utils/get-port.js'