Skip to content

Replace ts-node with the TypeScript language service #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,86 +1,134 @@
# 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 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 `<!-- ts-docs-verifier:ignore -->` 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.
9 changes: 2 additions & 7 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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"];

Expand Down Expand Up @@ -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
);
Expand Down
16 changes: 7 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -35,7 +35,7 @@
},
"homepage": "https://github.com/bbc/typescript-docs-verifier#readme",
"engines": {
"node": ">=12"
"node": ">=20"
},
"scripts": {
"format": "prettier --write '**/*.ts' '**/*.json'",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/CodeBlockExtractor.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -26,7 +26,7 @@ export class CodeBlockExtractor {
}

private static async readFile(path: string): Promise<string> {
return await fsExtra.readFile(path, "utf-8");
return await readFile(path, "utf-8");
}

private static extractCodeBlocksFromMarkdown(
Expand Down
98 changes: 98 additions & 0 deletions src/CodeCompiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import ts from "typescript";

const createServiceHost = (
options: ts.CompilerOptions,
workingDirectory: string,
fileMap: Map<string, string>
): 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<ts.Diagnostic>;
}> => {
const id = process.hrtime.bigint().toString();
const filename = `block-${id}.${type}`;

const fileMap = new Map<string, string>([[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();
}
};
8 changes: 2 additions & 6 deletions src/LocalImportSubstituter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/PackageInfo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from "path";
import * as fsExtra from "fs-extra";
import { readFile } from "fs/promises";

export type SubpathPattern = "." | string;

Expand Down Expand Up @@ -36,7 +36,7 @@ const searchParentsForPackage = async (
currentPath: string
): Promise<string> => {
try {
await fsExtra.readFile(path.join(currentPath, "package.json"));
await readFile(path.join(currentPath, "package.json"));
return currentPath;
} catch {
const parentPath = path.dirname(currentPath);
Expand All @@ -60,7 +60,7 @@ export class PackageInfo {
static async read(): Promise<PackageDefinition> {
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 {
Expand Down
Loading