diff --git a/CHANGELOG b/CHANGELOG index 8ede609..7c09188 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), @@ -6,81 +7,128 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [3.0.0-beta.1] + +### Changed + +- Remove dependency on `ts-node` to avoid memory issues when compiling many snippets. + - [BREAKING] Compilation errors are now `CompilationError` instances, not `TSNode.TSError` instances +- Compile snippets in series to avoid memory issues. + +### Removed + +- [BREAKING] Support for TypeScript versions <4.7.2 +- [BREAKING] Support for Node.js <20 +- Production dependency on `tsconfig` and `strip-ansi` and `fs-extra` + ## [2.5.3] + ### Changed + - Use a separate `ts-node` compiler per-snippet to ensure that compilation of snippets is independent ## [2.5.2] + ### Removed + - Obsolete Travis CI build badge from README ## [2.5.1] + ### Added + - Override any project-specific `ts-node` `transpileOnly` to force type checking when compiling code snippets ## [2.5.0] + ### Added + - Support for `tsx` snippets ## [2.4.1] + ### Changed + - Various fixes for Windows environments ## [2.4.0] + ### Added + - A new `--project` option that overrides the `tsconfig.json` file to be used when compiling snippets ## [2.3.1] + ### Changed + - Update dependencies ## [2.3.0] + ### Added + - Support for `exports` in `package.json` including wildcard subpaths ### Changed + - Compile documentation snippets in the project folder so that dependent packages can be resolved reliably even in nested projects ## [2.2.2] + ### Changed + - Unpinned `ts-node` dependency to fix issues with the most recent TypeScript versions (required the extraction of the line numbers of compilation errors to be changed) ## [2.2.1] + ### Added + - Allow code blocks to be ignored by preceding them with a `` comment ## [2.2.0] + ### Changed + - Link project `node_modules` to snippet compilation directory so you can import from the current project's dependencies in snippets ### Added + - Support for importing sub-paths within packages - Support for scoped package names ## [2.1.0] + ### Changed + - No longer wrap TypeScript code blocks in functions before compilation - Write temporary files to the OS temporary directory ### Added + - An 1-indexed `index` property to the `CodeBlock` type to indicate where in the file the code block was found - Add a `linesWithErrors` property to the compilation result to indicate which lines contained errors ## [2.0.1] - 2021-10-08 + ### Changed + - Updated dependencies (including dropping `tslint` in favour of `eslint`) - Pin to version 5.x.x of `ora` to avoid issues with ESM ## [2.0.0-rc.1] - 2021-09-27 + ### Added + - Support for TypeScript code blocked marked with \`\`\`ts as well as ```typescript - This changelog 🎉 ### Changed + - [BREAKING] TypeScript is now a peerDependency and must be installed by the client. - Tagging of the source is now done using a `release` branch. - Migrated code to `async` / `await`. ### Removed + - [BREAKING] Support for NodeJS versions prior to version 12. - Bluebird dependency. diff --git a/index.ts b/index.ts index 763dc74..32f2972 100644 --- a/index.ts +++ b/index.ts @@ -1,11 +1,10 @@ -import * as path from "path"; import { PackageInfo } from "./src/PackageInfo"; import { SnippetCompiler, SnippetCompilationResult, } from "./src/SnippetCompiler"; -export { SnippetCompilationResult } from "./src/SnippetCompiler"; +export type { SnippetCompilationResult, CompilationError } from "./src/SnippetCompiler"; const DEFAULT_FILES = ["README.md"]; @@ -40,12 +39,8 @@ export async function compileSnippets( const { project, markdownFiles } = parseArguments(args); const packageDefinition = await PackageInfo.read(); - const compiledDocsFolder = path.join( - packageDefinition.packageRoot, - ".tmp-compiled-docs" - ); const compiler = new SnippetCompiler( - compiledDocsFolder, + packageDefinition.packageRoot, packageDefinition, project ); diff --git a/package.json b/package.json index 301c201..302e644 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "typescript", "verify" ], - "version": "2.5.3", + "version": "3.0.0-beta.1", "main": "dist/index.js", "@types": "dist/index.d.ts", "bin": { @@ -35,7 +35,7 @@ }, "homepage": "https://github.com/bbc/typescript-docs-verifier#readme", "engines": { - "node": ">=12" + "node": ">=20" }, "scripts": { "format": "prettier --write '**/*.ts' '**/*.json'", @@ -54,7 +54,7 @@ "@types/chai-as-promised": "^8.0.1", "@types/fs-extra": "^11.0.4", "@types/mocha": "^10.0.10", - "@types/node": "^16.11.59", + "@types/node": "^20.19.1", "@types/react": "^18.2.12", "@types/yargs": "^17.0.12", "@typescript-eslint/eslint-plugin": "^8.24.0", @@ -66,25 +66,23 @@ "eslint-plugin-functional": "^6.6.3", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^7.2.1", + "fs-extra": "^11.3.0", "mocha": "^11.1.0", "npm-run-all2": "^7.0.2", "nyc": "^17.1.0", "prettier": "^3.5.1", "react": "^18.2.0", - "typescript": "^4.7.3", + "ts-node": "^10.9.2", + "typescript": "^4.7.2", "verify-it": "^2.3.3" }, "dependencies": { "chalk": "^4.1.2", - "fs-extra": "^10.0.0", "ora": "^5.4.1", - "strip-ansi": "^7.0.1", - "ts-node": "^10.8.1", - "tsconfig": "^7.0.0", "yargs": "^17.5.1" }, "peerDependencies": { - "typescript": ">3.8.3" + "typescript": ">=4.7.2" }, "files": [ "dist/index.js", diff --git a/src/CodeBlockExtractor.ts b/src/CodeBlockExtractor.ts index 1830a00..5ef99b9 100644 --- a/src/CodeBlockExtractor.ts +++ b/src/CodeBlockExtractor.ts @@ -1,4 +1,4 @@ -import * as fsExtra from "fs-extra"; +import { readFile } from "fs/promises"; // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class CodeBlockExtractor { @@ -26,7 +26,7 @@ export class CodeBlockExtractor { } private static async readFile(path: string): Promise { - return await fsExtra.readFile(path, "utf-8"); + return await readFile(path, "utf-8"); } private static extractCodeBlocksFromMarkdown( diff --git a/src/CodeCompiler.ts b/src/CodeCompiler.ts new file mode 100644 index 0000000..707bb62 --- /dev/null +++ b/src/CodeCompiler.ts @@ -0,0 +1,98 @@ +import ts from "typescript"; + +const createServiceHost = ( + options: ts.CompilerOptions, + workingDirectory: string, + fileMap: Map +): ts.LanguageServiceHost => ({ + getScriptFileNames: () => { + return [...fileMap.keys()]; + }, + getScriptVersion: () => "1", + getProjectVersion: () => "1", + getScriptSnapshot: (fileName) => { + const contents = + fileMap.get(fileName) ?? ts.sys.readFile(fileName, "utf-8"); + + return typeof contents === "undefined" + ? contents + : ts.ScriptSnapshot.fromString(contents); + }, + readFile: (fileName) => { + return fileMap.get(fileName) ?? ts.sys.readFile(fileName); + }, + fileExists: (fileName) => { + return fileMap.has(fileName) || ts.sys.fileExists(fileName); + }, + getCurrentDirectory: () => workingDirectory, + getDirectories: ts.sys.getDirectories, + directoryExists: ts.sys.directoryExists, + getCompilationSettings: () => options, + getDefaultLibFileName: () => ts.getDefaultLibFilePath(options), +}); + +export const compile = async ({ + compilerOptions, + workingDirectory, + code, + type, +}: { + compilerOptions: ts.CompilerOptions; + workingDirectory: string; + code: string; + type: "ts" | "tsx"; +}): Promise<{ + hasError: boolean; + diagnostics: ReadonlyArray; +}> => { + const id = process.hrtime.bigint().toString(); + const filename = `block-${id}.${type}`; + + const fileMap = new Map([[filename, code]]); + + const registry = ts.createDocumentRegistry( + ts.sys.useCaseSensitiveFileNames, + workingDirectory + ); + + const serviceHost = createServiceHost( + { + ...compilerOptions, + noEmit: false, + declaration: false, + sourceMap: false, + noEmitOnError: true, + incremental: false, + composite: false, + declarationMap: false, + noUnusedLocals: false, + }, + workingDirectory, + fileMap + ); + + const service = ts.createLanguageService(serviceHost, registry); + + try { + const output = service.getEmitOutput(filename, false, false); + + if (output.emitSkipped) { + const diagnostics = [ + ...service.getSemanticDiagnostics(filename), + ...service.getSyntacticDiagnostics(filename), + ]; + + return { + diagnostics, + hasError: true, + }; + } + + return { + diagnostics: [], + hasError: false, + }; + } finally { + service.dispose(); + } +}; diff --git a/src/LocalImportSubstituter.ts b/src/LocalImportSubstituter.ts index 4ac7241..8885deb 100644 --- a/src/LocalImportSubstituter.ts +++ b/src/LocalImportSubstituter.ts @@ -101,21 +101,17 @@ class ExportResolver { ); } - stripSuffix(filePath: string): string { - return filePath.replace(/\.(tsx?|js)$/, ""); - } - resolveExportPath(path?: string): string { if (!this.packageExports) { if (!this.packageMain) { throw new Error("Failed to find main or exports entry in package.json"); } - return path ?? this.stripSuffix(this.packageMain); + return path ?? this.packageMain; } const matchingExport = this.findMatchingExport(path); - return this.stripSuffix(matchingExport); + return matchingExport; } } diff --git a/src/PackageInfo.ts b/src/PackageInfo.ts index df8599a..a093396 100644 --- a/src/PackageInfo.ts +++ b/src/PackageInfo.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import * as fsExtra from "fs-extra"; +import { readFile } from "fs/promises"; export type SubpathPattern = "." | string; @@ -36,7 +36,7 @@ const searchParentsForPackage = async ( currentPath: string ): Promise => { try { - await fsExtra.readFile(path.join(currentPath, "package.json")); + await readFile(path.join(currentPath, "package.json")); return currentPath; } catch { const parentPath = path.dirname(currentPath); @@ -60,7 +60,7 @@ export class PackageInfo { static async read(): Promise { const packageRoot = await searchParentsForPackage(process.cwd()); const packageJsonPath = path.join(packageRoot, "package.json"); - const contents = await fsExtra.readFile(packageJsonPath, "utf-8"); + const contents = await readFile(packageJsonPath, "utf-8"); const packageInfo = JSON.parse(contents); return { diff --git a/src/SnippetCompiler.ts b/src/SnippetCompiler.ts index e41998f..2e7e7ce 100644 --- a/src/SnippetCompiler.ts +++ b/src/SnippetCompiler.ts @@ -1,11 +1,9 @@ -import * as path from "path"; -import chalk from "chalk"; -import * as tsconfig from "tsconfig"; -import * as fsExtra from "fs-extra"; -import * as TSNode from "ts-node"; +import fs from "fs"; +import ts from "typescript"; import { PackageDefinition } from "./PackageInfo"; import { CodeBlockExtractor } from "./CodeBlockExtractor"; import { LocalImportSubstituter } from "./LocalImportSubstituter"; +import { compile } from "./CodeCompiler"; type CodeBlock = { readonly file: string; @@ -20,11 +18,26 @@ export type SnippetCompilationResult = { readonly index: number; readonly snippet: string; readonly linesWithErrors: number[]; - readonly error?: TSNode.TSError | Error; + readonly error?: CompilationError | Error; }; +export class CompilationError extends Error { + diagnosticCodes: number[]; + name: string; + diagnosticText: string; + diagnostics: ts.Diagnostic[]; + + constructor(diagnosticText: string, diagnostics?: ts.Diagnostic[]) { + super(diagnosticText); + this.name = this.constructor.name; + this.diagnosticText = diagnosticText; + this.diagnosticCodes = diagnostics?.map(({ code }) => code) ?? []; + this.diagnostics = diagnostics ?? []; + } +} + export class SnippetCompiler { - private readonly compilerConfig: TSNode.CreateOptions; + private readonly compilerOptions: ts.CompilerOptions; constructor( private readonly workingDirectory: string, @@ -35,49 +48,64 @@ export class SnippetCompiler { packageDefinition.packageRoot, project ); - this.compilerConfig = { - ...(configOptions.config as TSNode.CreateOptions), - transpileOnly: false, - }; + this.compilerOptions = configOptions.options; } private static loadTypeScriptConfig( packageRoot: string, project?: string - ): { - config: unknown; - } { - const fullProjectPath = path.join(packageRoot, project ?? ""); - const { base, dir } = path.parse(fullProjectPath); - - const typeScriptConfig = tsconfig.loadSync(dir, base); - if (typeScriptConfig?.config?.compilerOptions) { - typeScriptConfig.config.compilerOptions.noUnusedLocals = false; + ): ts.ParsedCommandLine { + const configFile = ts.findConfigFile( + packageRoot, + ts.sys.fileExists, + project + ); + + if (!configFile) { + throw new Error( + `Unable to find TypeScript configuration file in ${packageRoot}` + ); } - return typeScriptConfig; - } - private static escapeRegExp(rawString: string): string { - return rawString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - } + const fileContents = fs.readFileSync(configFile, "utf-8"); + const { config: configJSON, error } = ts.parseConfigFileTextToJson( + configFile, + fileContents + ); - async compileSnippets( - documentationFiles: string[] - ): Promise { - try { - await this.cleanWorkingDirectory(); - await fsExtra.ensureDir(this.workingDirectory); - const examples = await this.extractAllCodeBlocks(documentationFiles); - return await Promise.all( - examples.map(async (example) => await this.testCodeCompilation(example)) + if (error) { + throw new Error( + `Error reading tsconfig from ${configFile}: ${ts.flattenDiagnosticMessageText(error.messageText, ts.sys.newLine)}` ); - } finally { - await this.cleanWorkingDirectory(); } + + const parsedConfig = ts.parseJsonConfigFileContent( + configJSON, + { + fileExists: ts.sys.fileExists, + readDirectory: ts.sys.readDirectory, + readFile: ts.sys.readFile, + useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, + }, + packageRoot + ); + + return parsedConfig; } + async compileSnippets( + documentationFiles: string[] + ): Promise { + const results: SnippetCompilationResult[] = []; + const examples = await this.extractAllCodeBlocks(documentationFiles); - private async cleanWorkingDirectory() { - return await fsExtra.remove(this.workingDirectory); + for (const example of examples) { + const result = await this.testCodeCompilation(example); + results.push(result); + + // Yield to event loop + await new Promise((resolve) => setImmediate(resolve)); + } + return results; } private async extractAllCodeBlocks(documentationFiles: string[]) { @@ -119,78 +147,78 @@ export class SnippetCompiler { return localisedBlock; } - private async compile(code: string, type: "ts" | "tsx"): Promise { - const id = process.hrtime.bigint().toString(); - const codeFile = path.join(this.workingDirectory, `block-${id}.${type}`); - await fsExtra.writeFile(codeFile, code); - const compiler = TSNode.create(this.compilerConfig); - compiler.compile(code, codeFile); - } - - private removeTemporaryFilePaths( - message: string, - example: CodeBlock - ): string { - const escapedCompiledDocsFolder = SnippetCompiler.escapeRegExp( - path.basename(this.workingDirectory) - ); - const compiledDocsFilePrefixPattern = new RegExp( - `${escapedCompiledDocsFolder}/block-\\d+\\.ts`, - "g" - ); - return message.replace( - compiledDocsFilePrefixPattern, - chalk`{blue ${example.file}} → {cyan Code Block ${example.index}}` - ); - } - private async testCodeCompilation( example: CodeBlock ): Promise { try { - await this.compile(example.sanitisedCode, example.type); + const { hasError, diagnostics } = await compile({ + compilerOptions: this.compilerOptions, + workingDirectory: this.workingDirectory, + code: example.sanitisedCode, + type: example.type, + }); + + if (!hasError) { + return { + snippet: example.snippet, + file: example.file, + index: example.index, + linesWithErrors: [], + }; + } + + const linesWithErrors = new Set(); + + const enrichedDiagnostics = diagnostics.map((diagnostic) => { + if (typeof diagnostic.start !== "undefined") { + const startLine = + [...example.sanitisedCode.substring(0, diagnostic.start)].filter( + (char) => char === ts.sys.newLine + ).length + 1; + linesWithErrors.add(startLine); + } + + return { + ...diagnostic, + file: diagnostic.file + ? { + ...diagnostic.file, + fileName: `${example.file} → Code Block ${example.index}`, + } + : undefined, + }; + }); + + const formatter = process.stdout.isTTY + ? ts.formatDiagnosticsWithColorAndContext + : ts.formatDiagnostics; + + const diagnosticText = formatter(enrichedDiagnostics, { + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => this.workingDirectory, + getNewLine: () => ts.sys.newLine, + }); + + const error = new CompilationError( + `⨯ Unable to compile TypeScript:\n${diagnosticText}`, + enrichedDiagnostics + ); + return { snippet: example.snippet, file: example.file, + error: error, index: example.index, - linesWithErrors: [], + linesWithErrors: [...linesWithErrors], }; } catch (rawError) { const error = rawError instanceof Error ? rawError : new Error(String(rawError)); - error.message = this.removeTemporaryFilePaths(error.message, example); - - Object.entries(error).forEach(([key, value]) => { - if (typeof value === "string") { - error[key as keyof typeof error] = this.removeTemporaryFilePaths( - value, - example - ); - } - }); - - const linesWithErrors = new Set(); - - if (error instanceof TSNode.TSError) { - error.diagnostics.forEach((diagnostic) => { - const { start } = diagnostic; - - if (typeof start === "undefined") { - return; - } - - const lineNumber = - [...example.sanitisedCode.substring(0, start)].filter( - (char) => char === "\n" - ).length + 1; - linesWithErrors.add(lineNumber); - }); - } return { snippet: example.snippet, error: error, - linesWithErrors: [...linesWithErrors], + linesWithErrors: [], file: example.file, index: example.index, }; diff --git a/test/LocalImportSubstituterSpec.ts b/test/LocalImportSubstituterSpec.ts index a9fb76d..11b1fd2 100644 --- a/test/LocalImportSubstituterSpec.ts +++ b/test/LocalImportSubstituterSpec.ts @@ -10,27 +10,27 @@ const defaultPackageInfo = { const scenarios = [ { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/index'`, + expected: `import something from '/path/to/package/index.ts'`, name: "single quotes", }, { importLine: `import something from 'awesome';`, - expected: `import something from '/path/to/package/index'`, + expected: `import something from '/path/to/package/index.ts'`, name: "a trailing semicolon", }, { importLine: `import something from "awesome"`, - expected: `import something from "/path/to/package/index"`, + expected: `import something from "/path/to/package/index.ts"`, name: "double quotes", }, { importLine: `import something from "awesome" `, - expected: `import something from "/path/to/package/index"`, + expected: `import something from "/path/to/package/index.ts"`, name: "trailing whitespace", }, { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/main'`, + expected: `import something from '/path/to/package/main.ts'`, packageInfo: { exports: "main.ts", }, @@ -38,7 +38,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/main'`, + expected: `import something from '/path/to/package/main.ts'`, packageInfo: { exports: { ".": "main.ts", @@ -48,7 +48,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/main'`, + expected: `import something from '/path/to/package/main.ts'`, packageInfo: { exports: { "node-addons": "main.ts", @@ -58,7 +58,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/main'`, + expected: `import something from '/path/to/package/main.ts'`, packageInfo: { exports: { node: "main.ts", @@ -68,7 +68,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/main'`, + expected: `import something from '/path/to/package/main.ts'`, packageInfo: { exports: { require: "main.ts", @@ -78,7 +78,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/main'`, + expected: `import something from '/path/to/package/main.ts'`, packageInfo: { exports: { default: "main.ts", @@ -88,7 +88,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/main'`, + expected: `import something from '/path/to/package/main.ts'`, packageInfo: { exports: { ".": { @@ -100,7 +100,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/main'`, + expected: `import something from '/path/to/package/main.ts'`, packageInfo: { exports: { ".": { @@ -112,7 +112,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome'`, - expected: `import something from '/path/to/package/main'`, + expected: `import something from '/path/to/package/main.ts'`, packageInfo: { exports: { ".": { @@ -124,7 +124,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome/some/path'`, - expected: `import something from '/path/to/package/internal/path'`, + expected: `import something from '/path/to/package/internal/path.ts'`, packageInfo: { exports: { ".": "main.ts", @@ -135,7 +135,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome/some/path'`, - expected: `import something from '/path/to/package/internal/path'`, + expected: `import something from '/path/to/package/internal/path.ts'`, packageInfo: { exports: { ".": "main.ts", @@ -148,7 +148,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome/lib/some/thing'`, - expected: `import something from '/path/to/package/internal/some/thing'`, + expected: `import something from '/path/to/package/internal/some/thing.ts'`, packageInfo: { exports: { ".": "main.ts", @@ -159,7 +159,7 @@ const scenarios = [ }, { importLine: `import something from 'awesome/lib/some/thing'`, - expected: `import something from '/path/to/package/internal/some/thing'`, + expected: `import something from '/path/to/package/internal/some/thing.ts'`, packageInfo: { exports: { ".": "main.ts", @@ -172,7 +172,7 @@ const scenarios = [ }, { importLine: `import something from '@my-scope/awesome'`, - expected: `import something from '/path/to/package/index'`, + expected: `import something from '/path/to/package/index.ts'`, name: "a scoped package name", packageName: "@my-scope/awesome", }, diff --git a/test/TypeScriptDocsVerifierSpec.ts b/test/TypeScriptDocsVerifierSpec.ts index 777f22d..4f06bc4 100644 --- a/test/TypeScriptDocsVerifierSpec.ts +++ b/test/TypeScriptDocsVerifierSpec.ts @@ -14,7 +14,7 @@ const fixturePath = path.join(__dirname, "fixtures"); const defaultPackageJson = { name: Gen.string(), - main: `${Gen.string()}.ts`, + main: `${Gen.string()}.js`, }; const defaultMainFile = { name: defaultPackageJson.main, @@ -595,7 +595,7 @@ console.log('This line is also OK'); }); verify.it( - "localises imports of the current package if the package main is a ts file", + "localises imports of the current package if the package main is a js file", async () => { const snippet = ` import { MyClass } from '${defaultPackageJson.name}' @@ -629,7 +629,7 @@ console.log('This line is also OK'); ); verify.it( - "localises imports of the current package if the package main is a tsx file", + "localises imports of the current package if the package main is a jsx file", async () => { const snippet = ` import React from 'react'; @@ -651,7 +651,7 @@ console.log('This line is also OK'); mainFile, packageJson: { ...defaultPackageJson, - main: "main.tsx", + main: "main.jsx", }, tsConfig: JSON.stringify({ ...defaultTsConfig, @@ -835,6 +835,8 @@ console.log('This line is also OK'); } }`, }; + const otherTranspiledFile = path.join(sourceFolder, "other.js"); + const otherFile = { name: path.join(sourceFolder, "other.ts"), contents: ` @@ -849,7 +851,7 @@ console.log('This line is also OK'); ...defaultPackageJson, exports: { "./some/export": { - require: otherFile.name, + require: otherTranspiledFile, }, }, main: "some/other/file.js", @@ -952,7 +954,7 @@ console.log('This line is also OK'); await createProject({ packageJson: { name: "lib", - main: "lib.ts", + main: "lib.js", }, markdownFiles: [{ name: "README.md", contents: typeScriptMarkdown }], mainFile, @@ -1150,6 +1152,76 @@ console.log('This line is also OK'); } ); + verify.it("handles a non-JSON content in tsconfig.json file", async () => { + const snippet = ` + import { MyClass } from '${defaultPackageJson.name}' + await Promise.resolve(); + const instance = new MyClass() + instance.doStuff()`; + const mainFile = { + name: `${defaultPackageJson.main}`, + contents: ` + export class MyClass { + doStuff (): void { + return + } + }`, + }; + + const typeScriptMarkdown = wrapSnippet(snippet); + await createProject({ + markdownFiles: [{ name: "DOCS.md", contents: typeScriptMarkdown }], + mainFile, + }); + + const tsconfigFilename = `tsconfig.json`; + const tsconfigText = `{ + "compilerOptions": { + "target": "es2019", // comments are permitted! + "module": "esnext", + }, + }`; + + await FsExtra.writeFile( + path.join(workingDirectory, tsconfigFilename), + tsconfigText + ); + + return await TypeScriptDocsVerifier.compileSnippets({ + markdownFiles: ["DOCS.md"], + project: tsconfigFilename, + }).should.eventually.eql([ + { + file: "DOCS.md", + index: 1, + snippet, + linesWithErrors: [], + }, + ]); + }); + + verify.it("returns an error if the tsconfig file is invalid", async () => { + await createProject(); + + const tsconfigFilename = `tsconfig.json`; + const tsconfigText = `{ + "compilerOptions": { + "target": "es2019", + "module": "esnext", + }, + `; + + await FsExtra.writeFile( + path.join(workingDirectory, tsconfigFilename), + tsconfigText + ); + + return await TypeScriptDocsVerifier.compileSnippets({ + markdownFiles: ["DOCS.md"], + project: tsconfigFilename, + }).should.be.rejectedWith("Error reading tsconfig from"); + }); + verify.it( "uses the default settings if an empty object is supplied", genSnippet,