diff --git a/README.md b/README.md index f0db217..e4ed6c0 100644 --- a/README.md +++ b/README.md @@ -114,18 +114,19 @@ Options for the TypeScript checker (`typescript` option object). | `mode` | `'readonly'` or `'write-dts'` or `'write-tsbuildinfo'` or `'write-references'` | `build === true ? 'write-tsbuildinfo' ? 'readonly'` | Use `readonly` if you don't want to write anything on the disk, `write-dts` to write only `.d.ts` files, `write-tsbuildinfo` to write only `.tsbuildinfo` files, `write-references` to write both `.js` and `.d.ts` files of project references (last 2 modes requires `build: true`). | | `diagnosticOptions` | `object` | `{ syntactic: false, semantic: true, declaration: false, global: false }` | Settings to select which diagnostics do we want to perform. | | `profile` | `boolean` | `false` | Measures and prints timings related to the TypeScript performance. | -| `typescriptPath` | `string` | `require.resolve('typescript/package.json')` for TypeScript 7+, `require.resolve('@typescript/native-preview/package.json')` when `tsgo` falls back to preview, otherwise `require.resolve('typescript')` | If supplied this is a custom path where TypeScript can be found. In `tsgo` mode, it must be an absolute path to `typescript/package.json` from TypeScript 7+ or `@typescript/native-preview/package.json`. | +| `resolveRoot` | `string` | `undefined` | Root used to resolve the default TypeScript package. Relative paths are resolved from `compiler.options.context`. Only used when `typescriptPath` is not set. | +| `typescriptPath` | `string` | `require.resolve('typescript/package.json')` for TypeScript 7+, `require.resolve('@typescript/native-preview/package.json')` when `tsgo` falls back to preview, otherwise `require.resolve('typescript')` | Custom TypeScript path. In `tsgo` mode, use an absolute path to `typescript/package.json` or `@typescript/native-preview/package.json`. | | `tsgo` | `boolean` | `true` when TypeScript 7+ is detected, otherwise `false` | Enables experimental TypeScript Go support. The plugin runs the TypeScript Go checker binary in a child process. | ### TypeScript Go support `typescript.tsgo` can reduce type-checking time by about 5-10x, especially on large projects, by using the Go implementation of TypeScript. It enables experimental [typescript-go](https://github.com/microsoft/typescript-go) support through TypeScript 7+ or [`@typescript/native-preview`](https://www.npmjs.com/package/@typescript/native-preview). -When the configured or default installed `typescript` package is major version 7 or higher, the plugin enables `typescript.tsgo` automatically and runs the TypeScript Go executable from that package. When `typescript.tsgo: true` is set without a custom `typescriptPath` and TypeScript 7+ is not installed, the plugin falls back to `@typescript/native-preview`. +When the configured or default installed `typescript` package is major version 7 or higher, the plugin enables `typescript.tsgo` automatically and runs the TypeScript Go executable from that package. The default package detection uses `typescript.resolveRoot` when it is set. When `typescript.tsgo: true` is set without a custom `typescriptPath` and TypeScript 7+ is not installed, the plugin falls back to `@typescript/native-preview`. In this mode, the plugin runs the TypeScript Go binary in a child process, parses its diagnostics, and reports them through the existing issue formatter when possible. If the output cannot be parsed safely, the raw output is printed and the build fails when it exits with errors. -Supported options include `typescript.configFile`, `typescript.context`, `typescript.build`, `typescript.typescriptPath`, `async`, and `logger`. It also supports `tsconfig.json` compiler options used by `tsgo`, including `incremental` and `composite`. +Supported options include `typescript.configFile`, `typescript.context`, `typescript.build`, `typescript.resolveRoot`, `typescript.typescriptPath`, `async`, and `logger`. It also supports `tsconfig.json` compiler options used by `tsgo`, including `incremental` and `composite`. Install TypeScript 7.0 RC to enable `tsgo` automatically: diff --git a/src/typescript/type-script-worker-config.ts b/src/typescript/type-script-worker-config.ts index 910e379..44ecfbb 100644 --- a/src/typescript/type-script-worker-config.ts +++ b/src/typescript/type-script-worker-config.ts @@ -25,6 +25,7 @@ interface TypeScriptWorkerConfig { mode: 'readonly' | 'write-dts' | 'write-tsbuildinfo' | 'write-references'; diagnosticOptions: TypeScriptDiagnosticsOptions; profile: boolean; + resolveRoot?: string; typescriptPath: string; tsgo?: boolean; tsgoPackage?: TypeScriptGoPackage; @@ -35,9 +36,17 @@ type TypeScriptRuntimeConfig = Pick< 'typescriptPath' | 'tsgo' | 'tsgoPackage' >; -function resolveInstalledTypeScriptPackageForTsgo(): ResolvedTypeScriptGoPackage | undefined { +function resolveModule(request: string, resolveRoot?: string): string { + return resolveRoot + ? require.resolve(request, { paths: [resolveRoot] }) + : require.resolve(request); +} + +function resolveInstalledTsgoPackage( + resolveRoot?: string, +): ResolvedTypeScriptGoPackage | undefined { try { - const packageJsonPath = require.resolve(TYPESCRIPT_PACKAGE_JSON); + const packageJsonPath = resolveModule(TYPESCRIPT_PACKAGE_JSON, resolveRoot); const tsgoPackage = resolveTypeScriptGoPackage(packageJsonPath); if (tsgoPackage?.tsgoPackage === 'typescript') { @@ -50,9 +59,9 @@ function resolveInstalledTypeScriptPackageForTsgo(): ResolvedTypeScriptGoPackage return undefined; } -function resolveDefaultPreviewPackageJsonPath(): string { +function resolveDefaultPreviewPackageJsonPath(resolveRoot?: string): string { try { - return require.resolve(TYPESCRIPT_PREVIEW_PACKAGE_JSON); + return resolveModule(TYPESCRIPT_PREVIEW_PACKAGE_JSON, resolveRoot); } catch { return TYPESCRIPT_PREVIEW_PACKAGE_JSON; } @@ -77,16 +86,16 @@ function resolveTypeScriptRuntimeConfig( if (options.tsgo === false) { return { - typescriptPath: require.resolve('typescript'), + typescriptPath: resolveModule('typescript', options.resolveRoot), tsgo: false, }; } - const installedTypeScriptTsgoPackage = resolveInstalledTypeScriptPackageForTsgo(); + const installedTsgoPackage = resolveInstalledTsgoPackage(options.resolveRoot); - if (installedTypeScriptTsgoPackage) { + if (installedTsgoPackage) { return { - typescriptPath: installedTypeScriptTsgoPackage.packageJsonPath, + typescriptPath: installedTsgoPackage.packageJsonPath, tsgo: true, tsgoPackage: 'typescript', }; @@ -94,14 +103,14 @@ function resolveTypeScriptRuntimeConfig( if (options.tsgo === true) { return { - typescriptPath: resolveDefaultPreviewPackageJsonPath(), + typescriptPath: resolveDefaultPreviewPackageJsonPath(options.resolveRoot), tsgo: true, tsgoPackage: 'preview', }; } return { - typescriptPath: require.resolve('typescript'), + typescriptPath: resolveModule('typescript', options.resolveRoot), }; } @@ -121,7 +130,16 @@ function createTypeScriptWorkerConfig( const optionsAsObject: Exclude = typeof options === 'object' ? options : {}; - const typescriptRuntimeConfig = resolveTypeScriptRuntimeConfig(optionsAsObject); + const resolveRoot = optionsAsObject.resolveRoot + ? path.isAbsolute(optionsAsObject.resolveRoot) + ? optionsAsObject.resolveRoot + : path.resolve(compiler.options.context || process.cwd(), optionsAsObject.resolveRoot) + : undefined; + const normalizedOptions = { + ...optionsAsObject, + ...(resolveRoot ? { resolveRoot } : {}), + }; + const typescriptRuntimeConfig = resolveTypeScriptRuntimeConfig(normalizedOptions); return { enabled: Boolean(options) || options === undefined, @@ -129,17 +147,17 @@ function createTypeScriptWorkerConfig( build: false, mode: optionsAsObject.build ? 'write-tsbuildinfo' : 'readonly', profile: false, - ...optionsAsObject, + ...normalizedOptions, ...typescriptRuntimeConfig, configFile: configFile, - configOverwrite: optionsAsObject.configOverwrite || {}, - context: optionsAsObject.context || path.dirname(configFile), + configOverwrite: normalizedOptions.configOverwrite || {}, + context: normalizedOptions.context || path.dirname(configFile), diagnosticOptions: { syntactic: false, // by default they are reported by the loader semantic: true, declaration: false, global: false, - ...(optionsAsObject.diagnosticOptions || {}), + ...(normalizedOptions.diagnosticOptions || {}), }, }; } diff --git a/src/typescript/type-script-worker-options.ts b/src/typescript/type-script-worker-options.ts index 920a668..8ae5072 100644 --- a/src/typescript/type-script-worker-options.ts +++ b/src/typescript/type-script-worker-options.ts @@ -10,6 +10,7 @@ type TypeScriptWorkerOptions = { mode?: 'readonly' | 'write-tsbuildinfo' | 'write-dts' | 'write-references'; diagnosticOptions?: Partial; profile?: boolean; + resolveRoot?: string; typescriptPath?: string; tsgo?: boolean; }; diff --git a/test/unit/typescript/type-script-worker-config.spec.ts b/test/unit/typescript/type-script-worker-config.spec.ts index db0ef7f..44cd441 100644 --- a/test/unit/typescript/type-script-worker-config.spec.ts +++ b/test/unit/typescript/type-script-worker-config.spec.ts @@ -40,23 +40,42 @@ describe('typescript/type-scripts-worker-config', () => { }; const tempDirs: string[] = []; - function createTypeScriptPackage(version: string) { - const packagePath = fs.mkdtempSync(path.join(os.tmpdir(), 'ts-checker-typescript-config-')); + function createResolveRoot() { + const resolveRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ts-checker-resolve-root-')); + + tempDirs.push(resolveRoot); + + return resolveRoot; + } + + function createTypeScriptPackage(version: string, resolveRoot?: string) { + const packagePath = resolveRoot + ? path.join(resolveRoot, 'node_modules', 'typescript') + : fs.mkdtempSync(path.join(os.tmpdir(), 'ts-checker-typescript-config-')); const binDir = path.join(packagePath, 'bin'); + const libDir = path.join(packagePath, 'lib'); - tempDirs.push(packagePath); + if (!resolveRoot) { + tempDirs.push(packagePath); + } fs.mkdirSync(binDir, { recursive: true }); + fs.mkdirSync(libDir, { recursive: true }); fs.writeFileSync( path.join(packagePath, 'package.json'), JSON.stringify({ name: 'typescript', version, + main: 'lib/typescript.js', bin: { tsc: 'bin/tsc', }, }), ); fs.writeFileSync(path.join(binDir, 'tsc'), '#!/usr/bin/env node\n'); + fs.writeFileSync( + path.join(libDir, 'typescript.js'), + `module.exports = { version: ${JSON.stringify(version)} };\n`, + ); return path.join(packagePath, 'package.json'); } @@ -163,6 +182,70 @@ describe('typescript/type-scripts-worker-config', () => { } }); + it('uses resolveRoot to resolve the default TypeScript package', async () => { + const resolveRoot = createResolveRoot(); + createTypeScriptPackage('6.1.0', resolveRoot); + const typescriptPath = require.resolve('typescript', { paths: [resolveRoot] }); + const { createTypeScriptWorkerConfig } = await import( + 'src/typescript/type-script-worker-config' + ); + + expect( + createTypeScriptWorkerConfig(compiler, { + resolveRoot, + }) + ).toEqual({ + ...configuration, + resolveRoot, + typescriptPath, + }); + }); + + it('uses resolveRoot when detecting the default TypeScript Go package', async () => { + const resolveRoot = createResolveRoot(); + createTypeScriptPackage('7.1.0', resolveRoot); + const packageJsonPath = require.resolve('typescript/package.json', { + paths: [resolveRoot], + }); + const { createTypeScriptWorkerConfig } = await import( + 'src/typescript/type-script-worker-config' + ); + + expect( + createTypeScriptWorkerConfig(compiler, { + resolveRoot, + }) + ).toEqual({ + ...configuration, + resolveRoot, + tsgo: true, + tsgoPackage: 'typescript', + typescriptPath: packageJsonPath, + }); + }); + + it('prefers typescriptPath over resolveRoot', async () => { + const resolveRoot = createResolveRoot(); + createTypeScriptPackage('7.1.0', resolveRoot); + const packageJsonPath = createTypeScriptPackage('7.2.0'); + const { createTypeScriptWorkerConfig } = await import( + 'src/typescript/type-script-worker-config' + ); + + expect( + createTypeScriptWorkerConfig(compiler, { + resolveRoot, + typescriptPath: packageJsonPath, + }) + ).toEqual({ + ...configuration, + resolveRoot, + tsgo: true, + tsgoPackage: 'typescript', + typescriptPath: packageJsonPath, + }); + }); + it('infers tsgo when typescriptPath points to supported TypeScript package', async () => { const packageJsonPath = createTypeScriptPackage('7.1.0'); const { createTypeScriptWorkerConfig } = await import(