diff --git a/README.md b/README.md index cae7e5c..f0db217 100644 --- a/README.md +++ b/README.md @@ -104,28 +104,43 @@ Rspack's modules resolution**. It means that you have to setup `tsconfig.json` c Options for the TypeScript checker (`typescript` option object). -| Name | Type | Default value | Description | -| ------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `memoryLimit` | `number` | `8192` | Memory limit for the checker process in MB. If the process exits with the allocation failed error, try to increase this number. | -| `configFile` | `string` | `'tsconfig.json'` | Path to the `tsconfig.json` file (path relative to the `compiler.options.context` or absolute path) | -| `configOverwrite` | `object` | `{ compilerOptions: { skipLibCheck: true, sourceMap: false, inlineSourceMap: false, declarationMap: false } }` | This configuration will overwrite configuration from the `tsconfig.json` file. Supported fields are: `extends`, `compilerOptions`, `include`, `exclude`, `files`, and `references`. | -| `context` | `string` | `dirname(configuration.configFile)` | The base path for finding files specified in the `tsconfig.json`. Same as the `context` option from the [ts-loader](https://github.com/TypeStrong/ts-loader#context). Useful if you want to keep your `tsconfig.json` in an external package. Keep in mind that **not** having a `tsconfig.json` in your project root can cause different behaviour between `ts-checker-rspack-plugin` and `tsc`. When using editors like `VS Code` it is advised to add a `tsconfig.json` file to the root of the project and extend the config file referenced in option `configFile`. | -| `build` | `boolean` | `false` | The equivalent of the `--build` flag for the `tsc` command. | -| `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` | `tsgo === true ? require.resolve('@typescript/native-preview/package.json') : 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/native-preview/package.json`. | -| `tsgo` | `boolean` | `false` | Enables experimental TypeScript Go (`@typescript/native-preview`) support. The plugin runs the tsgo binary in a child process. | +| Name | Type | Default value | Description | +| ------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `memoryLimit` | `number` | `8192` | Memory limit for the checker process in MB. If the process exits with the allocation failed error, try to increase this number. | +| `configFile` | `string` | `'tsconfig.json'` | Path to the `tsconfig.json` file (path relative to the `compiler.options.context` or absolute path) | +| `configOverwrite` | `object` | `{ compilerOptions: { skipLibCheck: true, sourceMap: false, inlineSourceMap: false, declarationMap: false } }` | This configuration will overwrite configuration from the `tsconfig.json` file. Supported fields are: `extends`, `compilerOptions`, `include`, `exclude`, `files`, and `references`. | +| `context` | `string` | `dirname(configuration.configFile)` | The base path for finding files specified in the `tsconfig.json`. Same as the `context` option from the [ts-loader](https://github.com/TypeStrong/ts-loader#context). Useful if you want to keep your `tsconfig.json` in an external package. Keep in mind that **not** having a `tsconfig.json` in your project root can cause different behaviour between `ts-checker-rspack-plugin` and `tsc`. When using editors like `VS Code` it is advised to add a `tsconfig.json` file to the root of the project and extend the config file referenced in option `configFile`. | +| `build` | `boolean` | `false` | The equivalent of the `--build` flag for the `tsc` command. | +| `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`. | +| `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/native-preview`](https://www.npmjs.com/package/@typescript/native-preview). +`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). -In this mode, the plugin runs the `tsgo` 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 `tsgo` output is printed and the build fails when `tsgo` exits with errors. +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`. + +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`. -Install `@typescript/native-preview` and enable `tsgo`: +Install TypeScript 7.0 RC to enable `tsgo` automatically: + +```sh +# with npm +npm install -D typescript@rc + +# with yarn +yarn add -D typescript@rc + +# with pnpm +pnpm add -D typescript@rc +``` + +Or install `@typescript/native-preview` and enable `tsgo` explicitly: ```sh # with npm @@ -146,13 +161,15 @@ new TsCheckerRspackPlugin({ }); ``` +> The `@typescript/native-preview` usage is deprecated and kept only for compatibility. We recommend installing `typescript@rc` to use `tsgo`. + Limitations: - `issue.include`, `issue.exclude`, and `issue.defaultSeverity` only apply to diagnostics whose `tsgo` output can be matched by `file`, `line`, `column`, `code`, and `message`. - `typescript.configOverwrite`, `typescript.diagnosticOptions`, and `typescript.profile` are not supported. - TypeScript API-based formatting or filesystem output rewrites are not supported. - Plugin-controlled declaration or reference emit modes such as `write-dts` and `write-references` are not supported; `tsgo` always runs with `--noEmit`. -- This integration may change when TypeScript provides a JavaScript API for `tsgo`. +- This integration may change when TypeScript provides a stable JavaScript API for `tsgo`. ### Issues options diff --git a/src/typescript/type-script-go-constants.ts b/src/typescript/type-script-go-constants.ts index 6f18e52..c169fdb 100644 --- a/src/typescript/type-script-go-constants.ts +++ b/src/typescript/type-script-go-constants.ts @@ -1,5 +1,13 @@ -const TYPESCRIPT_GO_PACKAGE = '@typescript/native-preview'; -const TYPESCRIPT_GO_PACKAGE_JSON = `${TYPESCRIPT_GO_PACKAGE}/package.json`; +const TYPESCRIPT_PACKAGE = 'typescript'; +const TYPESCRIPT_PACKAGE_JSON = `${TYPESCRIPT_PACKAGE}/package.json`; +const TYPESCRIPT_PREVIEW_PACKAGE = '@typescript/native-preview'; +const TYPESCRIPT_PREVIEW_PACKAGE_JSON = `${TYPESCRIPT_PREVIEW_PACKAGE}/package.json`; const TYPESCRIPT_GO_ISSUE_CODE = 'TSGO'; -export { TYPESCRIPT_GO_PACKAGE, TYPESCRIPT_GO_PACKAGE_JSON, TYPESCRIPT_GO_ISSUE_CODE }; +export { + TYPESCRIPT_GO_ISSUE_CODE, + TYPESCRIPT_PACKAGE, + TYPESCRIPT_PACKAGE_JSON, + TYPESCRIPT_PREVIEW_PACKAGE, + TYPESCRIPT_PREVIEW_PACKAGE_JSON, +}; diff --git a/src/typescript/type-script-go-package.ts b/src/typescript/type-script-go-package.ts new file mode 100644 index 0000000..3557f57 --- /dev/null +++ b/src/typescript/type-script-go-package.ts @@ -0,0 +1,70 @@ +import path from 'node:path'; + +import { + TYPESCRIPT_PACKAGE, + TYPESCRIPT_PREVIEW_PACKAGE, +} from './type-script-go-constants'; + +type TypeScriptGoPackage = 'typescript' | 'preview'; + +type TypeScriptGoPackageJson = { + name?: string; + version?: string; + bin?: string | Record; +}; + +type ResolvedTypeScriptGoPackage = { + packageJsonPath: string; + tsgoPackage: TypeScriptGoPackage; +}; + +function readTsgoPackageJson(packageJsonPath: string): TypeScriptGoPackageJson { + return require(packageJsonPath) as TypeScriptGoPackageJson; +} + +function getTsgoPackage(packageJson: TypeScriptGoPackageJson): TypeScriptGoPackage | undefined { + if (packageJson.name === TYPESCRIPT_PACKAGE) { + const versionMatch = packageJson.version?.match(/^(\d+)\.(\d+)(?:\.|$|-)/); + + if (versionMatch && Number(versionMatch[1]) >= 7) { + return 'typescript'; + } + } + + if (packageJson.name === TYPESCRIPT_PREVIEW_PACKAGE) { + return 'preview'; + } + + return undefined; +} + +function resolveTypeScriptGoPackage( + packageJsonPath: string, +): ResolvedTypeScriptGoPackage | undefined { + if ( + !path.isAbsolute(packageJsonPath) || + path.basename(packageJsonPath) !== 'package.json' + ) { + return undefined; + } + + try { + const tsgoPackage = getTsgoPackage(readTsgoPackageJson(packageJsonPath)); + + return tsgoPackage ? { packageJsonPath, tsgoPackage } : undefined; + } catch { + return undefined; + } +} + +export { + getTsgoPackage, + readTsgoPackageJson, + resolveTypeScriptGoPackage, +}; + +export type { + ResolvedTypeScriptGoPackage, + TypeScriptGoPackage, + TypeScriptGoPackageJson, +}; diff --git a/src/typescript/type-script-go-runner.ts b/src/typescript/type-script-go-runner.ts index b82227d..f4fd31e 100644 --- a/src/typescript/type-script-go-runner.ts +++ b/src/typescript/type-script-go-runner.ts @@ -1,4 +1,5 @@ import { spawn } from 'node:child_process'; +import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -9,35 +10,78 @@ import { AbortError } from '../utils/async/abort-error'; import type { TypeScriptWorkerConfig } from './type-script-worker-config'; import { TYPESCRIPT_GO_ISSUE_CODE, - TYPESCRIPT_GO_PACKAGE, - TYPESCRIPT_GO_PACKAGE_JSON, + TYPESCRIPT_PACKAGE, + TYPESCRIPT_PREVIEW_PACKAGE, + TYPESCRIPT_PREVIEW_PACKAGE_JSON, } from './type-script-go-constants'; +type TypeScriptGoExecutable = { + command: string; + args: string[]; +}; + function resolveTypeScriptGoPackageJsonPath(config: TypeScriptWorkerConfig): string { if ( !path.isAbsolute(config.typescriptPath) || path.basename(config.typescriptPath) !== 'package.json' ) { throw new Error( - `The typescriptPath option must be an absolute path to "${TYPESCRIPT_GO_PACKAGE_JSON}" when tsgo is enabled.`, + `The typescriptPath option must be an absolute path to a package.json file when tsgo is enabled.`, + ); + } + + if (!config.tsgoPackage) { + throw new Error( + `The typescriptPath option must point to "${TYPESCRIPT_PREVIEW_PACKAGE_JSON}" or "${TYPESCRIPT_PACKAGE}@rc".`, ); } return config.typescriptPath; } -async function resolveTypeScriptGoBinPath(config: TypeScriptWorkerConfig): Promise { - const tsgoPkgPath = resolveTypeScriptGoPackageJsonPath(config); - const getExePathPath = path.resolve(path.dirname(tsgoPkgPath), './lib/getExePath.js'); +async function resolveTypeScriptGoNativeExecutablePath( + packageJsonPath: string, + packageName: string, +): Promise { + const getExePathPath = path.resolve(path.dirname(packageJsonPath), './lib/getExePath.js'); const getExePathUrl = pathToFileURL(getExePathPath).href; const getExePathModule = await import(getExePathUrl); const getExePath = getExePathModule.default || getExePathModule.getExePath; if (typeof getExePath !== 'function') { - throw new Error(`Cannot resolve the typescript-go executable from "${TYPESCRIPT_GO_PACKAGE}".`); + throw new Error( + `Cannot resolve the typescript-go executable from "${packageName}".`, + ); + } + + const executablePath = getExePath(); + + if (typeof executablePath !== 'string' || !fs.existsSync(executablePath)) { + throw new Error('Executable not found'); + } + + return executablePath; +} + +async function resolveTypeScriptGoBinPath(config: TypeScriptWorkerConfig): Promise { + const packageJsonPath = resolveTypeScriptGoPackageJsonPath(config); + + if (config.tsgoPackage === 'typescript') { + return resolveTypeScriptGoNativeExecutablePath(packageJsonPath, TYPESCRIPT_PACKAGE); } - return getExePath(); + return resolveTypeScriptGoNativeExecutablePath(packageJsonPath, TYPESCRIPT_PREVIEW_PACKAGE); +} + +async function resolveTypeScriptGoExecutable( + config: TypeScriptWorkerConfig, +): Promise { + const binPath = await resolveTypeScriptGoBinPath(config); + + return { + command: binPath, + args: [], + }; } function createTypeScriptGoArgs(config: TypeScriptWorkerConfig) { @@ -218,13 +262,13 @@ async function runTypeScriptGo( ): Promise { AbortError.throwIfAborted(signal); - const binPath = await resolveTypeScriptGoBinPath(config); + const executable = await resolveTypeScriptGoExecutable(config); const args = createTypeScriptGoArgs(config); return new Promise((resolve, reject) => { let settled = false; let output = ''; - const childProcess = spawn(binPath, args, { + const childProcess = spawn(executable.command, [...executable.args, ...args], { cwd: config.context, stdio: ['inherit', 'pipe', 'pipe'], }); @@ -312,6 +356,7 @@ export { isTypeScriptGoIssue, isTypeScriptGoStatsError, parseTypeScriptGoIssues, + resolveTypeScriptGoExecutable, resolveTypeScriptGoBinPath, resolveTypeScriptGoPackageJsonPath, runTypeScriptGo, diff --git a/src/typescript/type-script-support.ts b/src/typescript/type-script-support.ts index 99a17d3..29ea0c2 100644 --- a/src/typescript/type-script-support.ts +++ b/src/typescript/type-script-support.ts @@ -3,19 +3,43 @@ import fs from 'node:fs'; import path from 'node:path'; import type { TypeScriptWorkerConfig } from './type-script-worker-config'; -import { TYPESCRIPT_GO_PACKAGE, TYPESCRIPT_GO_PACKAGE_JSON } from './type-script-go-constants'; +import { + getTsgoPackage, + readTsgoPackageJson, +} from './type-script-go-package'; +import { + TYPESCRIPT_PACKAGE, + TYPESCRIPT_PACKAGE_JSON, + TYPESCRIPT_PREVIEW_PACKAGE, + TYPESCRIPT_PREVIEW_PACKAGE_JSON, +} from './type-script-go-constants'; import { resolveTypeScriptGoBinPath, resolveTypeScriptGoPackageJsonPath, } from './type-script-go-runner'; function isDefaultTypeScriptGoPath(typescriptPath: string): boolean { - if (typescriptPath === TYPESCRIPT_GO_PACKAGE_JSON) { + if (typescriptPath === TYPESCRIPT_PREVIEW_PACKAGE_JSON) { return true; } try { - return typescriptPath === require.resolve(TYPESCRIPT_GO_PACKAGE_JSON); + if (typescriptPath === require.resolve(TYPESCRIPT_PREVIEW_PACKAGE_JSON)) { + return true; + } + } catch { + // silent catch + } + + if ( + !path.isAbsolute(typescriptPath) || + path.basename(typescriptPath) !== 'package.json' + ) { + return false; + } + + try { + return Boolean(getTsgoPackage(readTsgoPackageJson(typescriptPath))); } catch { return false; } @@ -23,12 +47,12 @@ function isDefaultTypeScriptGoPath(typescriptPath: string): boolean { function createTypeScriptGoSupportError(config: TypeScriptWorkerConfig, error?: unknown) { const message = [ - `When you enable TsCheckerRspackPlugin with \`typescript.tsgo\`, you must install \`${TYPESCRIPT_GO_PACKAGE}\` package.`, + `When you enable TsCheckerRspackPlugin with \`typescript.tsgo\`, you must install \`${TYPESCRIPT_PACKAGE}@rc\` or \`${TYPESCRIPT_PREVIEW_PACKAGE}\` package.`, ]; if (!isDefaultTypeScriptGoPath(config.typescriptPath)) { message.push( - `If you set \`typescript.typescriptPath\`, it must be an absolute path to \`${TYPESCRIPT_GO_PACKAGE_JSON}\`.`, + `If you set \`typescript.typescriptPath\`, it must be an absolute path to \`${TYPESCRIPT_PACKAGE_JSON}\` from \`${TYPESCRIPT_PACKAGE}@rc\` or \`${TYPESCRIPT_PREVIEW_PACKAGE_JSON}\`.`, ); } @@ -36,7 +60,9 @@ function createTypeScriptGoSupportError(config: TypeScriptWorkerConfig, error?: message.push(`Failed to resolve the tsgo executable: ${error.message}`); } - message.push(`You can install it with \`npm add ${TYPESCRIPT_GO_PACKAGE} -D\`.`); + message.push( + `You can install it with \`npm add ${TYPESCRIPT_PACKAGE}@rc -D\` or \`npm add ${TYPESCRIPT_PREVIEW_PACKAGE} -D\`.`, + ); return new Error(message.join(os.EOL)); } @@ -44,7 +70,10 @@ function createTypeScriptGoSupportError(config: TypeScriptWorkerConfig, error?: function assertTypeScriptGoSupport(config: TypeScriptWorkerConfig) { try { const tsgoPackageJsonPath = resolveTypeScriptGoPackageJsonPath(config); - const getExePathPath = path.resolve(path.dirname(tsgoPackageJsonPath), './lib/getExePath.js'); + const getExePathPath = path.resolve( + path.dirname(tsgoPackageJsonPath), + './lib/getExePath.js', + ); if (!fs.existsSync(getExePathPath)) { throw new Error(); @@ -66,6 +95,12 @@ function assertTypeScriptSupport(config: TypeScriptWorkerConfig) { if (config.tsgo) { assertTypeScriptGoSupport(config); } else { + if (path.basename(config.typescriptPath) === 'package.json') { + throw new Error( + "When you use TsCheckerRspackPlugin without `typescript.tsgo`, `typescript.typescriptPath` should point to a path like `require.resolve('typescript')`.", + ); + } + let typescriptVersion: string | undefined; try { diff --git a/src/typescript/type-script-worker-config.ts b/src/typescript/type-script-worker-config.ts index 34d336b..910e379 100644 --- a/src/typescript/type-script-worker-config.ts +++ b/src/typescript/type-script-worker-config.ts @@ -4,7 +4,15 @@ import type * as rspack from '@rspack/core'; import type { TypeScriptConfigOverwrite } from './type-script-config-overwrite'; import type { TypeScriptDiagnosticsOptions } from './type-script-diagnostics-options'; -import { TYPESCRIPT_GO_PACKAGE_JSON } from './type-script-go-constants'; +import { + type ResolvedTypeScriptGoPackage, + resolveTypeScriptGoPackage, + type TypeScriptGoPackage, +} from './type-script-go-package'; +import { + TYPESCRIPT_PACKAGE_JSON, + TYPESCRIPT_PREVIEW_PACKAGE_JSON, +} from './type-script-go-constants'; import type { TypeScriptWorkerOptions } from './type-script-worker-options'; interface TypeScriptWorkerConfig { @@ -19,18 +27,82 @@ interface TypeScriptWorkerConfig { profile: boolean; typescriptPath: string; tsgo?: boolean; + tsgoPackage?: TypeScriptGoPackage; } -function resolveDefaultTypeScriptPath(tsgo?: boolean): string { - if (tsgo === true) { - try { - return require.resolve(TYPESCRIPT_GO_PACKAGE_JSON); - } catch { - return TYPESCRIPT_GO_PACKAGE_JSON; +type TypeScriptRuntimeConfig = Pick< + TypeScriptWorkerConfig, + 'typescriptPath' | 'tsgo' | 'tsgoPackage' +>; + +function resolveInstalledTypeScriptPackageForTsgo(): ResolvedTypeScriptGoPackage | undefined { + try { + const packageJsonPath = require.resolve(TYPESCRIPT_PACKAGE_JSON); + const tsgoPackage = resolveTypeScriptGoPackage(packageJsonPath); + + if (tsgoPackage?.tsgoPackage === 'typescript') { + return tsgoPackage; } + } catch { + // silent catch + } + + return undefined; +} + +function resolveDefaultPreviewPackageJsonPath(): string { + try { + return require.resolve(TYPESCRIPT_PREVIEW_PACKAGE_JSON); + } catch { + return TYPESCRIPT_PREVIEW_PACKAGE_JSON; + } +} + +function resolveTypeScriptRuntimeConfig( + options: Exclude, +): TypeScriptRuntimeConfig { + if (options.typescriptPath) { + const tsgoPackage = resolveTypeScriptGoPackage(options.typescriptPath); + const tsgo = + options.tsgo === undefined && tsgoPackage?.tsgoPackage === 'typescript' + ? true + : options.tsgo; + + return { + typescriptPath: options.typescriptPath, + ...(tsgo === undefined ? {} : { tsgo }), + ...(tsgo === true && tsgoPackage ? { tsgoPackage: tsgoPackage.tsgoPackage } : {}), + }; } - return require.resolve('typescript'); + if (options.tsgo === false) { + return { + typescriptPath: require.resolve('typescript'), + tsgo: false, + }; + } + + const installedTypeScriptTsgoPackage = resolveInstalledTypeScriptPackageForTsgo(); + + if (installedTypeScriptTsgoPackage) { + return { + typescriptPath: installedTypeScriptTsgoPackage.packageJsonPath, + tsgo: true, + tsgoPackage: 'typescript', + }; + } + + if (options.tsgo === true) { + return { + typescriptPath: resolveDefaultPreviewPackageJsonPath(), + tsgo: true, + tsgoPackage: 'preview', + }; + } + + return { + typescriptPath: require.resolve('typescript'), + }; } function createTypeScriptWorkerConfig( @@ -49,9 +121,7 @@ function createTypeScriptWorkerConfig( const optionsAsObject: Exclude = typeof options === 'object' ? options : {}; - - const typescriptPath = - optionsAsObject.typescriptPath || resolveDefaultTypeScriptPath(optionsAsObject.tsgo); + const typescriptRuntimeConfig = resolveTypeScriptRuntimeConfig(optionsAsObject); return { enabled: Boolean(options) || options === undefined, @@ -60,6 +130,7 @@ function createTypeScriptWorkerConfig( mode: optionsAsObject.build ? 'write-tsbuildinfo' : 'readonly', profile: false, ...optionsAsObject, + ...typescriptRuntimeConfig, configFile: configFile, configOverwrite: optionsAsObject.configOverwrite || {}, context: optionsAsObject.context || path.dirname(configFile), @@ -70,10 +141,11 @@ function createTypeScriptWorkerConfig( global: false, ...(optionsAsObject.diagnosticOptions || {}), }, - typescriptPath, }; } -export { createTypeScriptWorkerConfig }; +export { + createTypeScriptWorkerConfig, +}; export type { TypeScriptWorkerConfig }; diff --git a/test/unit/typescript/type-script-go-runner.spec.ts b/test/unit/typescript/type-script-go-runner.spec.ts index 8758f4f..a385282 100644 --- a/test/unit/typescript/type-script-go-runner.spec.ts +++ b/test/unit/typescript/type-script-go-runner.spec.ts @@ -1,10 +1,12 @@ +import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import type { TypeScriptWorkerConfig } from 'src/typescript/type-script-worker-config'; describe('typescript/type-script-go-runner', () => { const tsgoPackageJsonPath = require.resolve('@typescript/native-preview/package.json'); - const projectContext = path.resolve('/project'); + const projectContext = path.resolve('project'); const config: TypeScriptWorkerConfig = { enabled: true, memoryLimit: 8192, @@ -22,7 +24,46 @@ describe('typescript/type-script-go-runner', () => { profile: false, typescriptPath: tsgoPackageJsonPath, tsgo: true, + tsgoPackage: 'preview', }; + const tempDirs: string[] = []; + + function createTypeScriptPackage(version: string) { + const packagePath = fs.mkdtempSync(path.join(os.tmpdir(), 'ts-checker-typescript-go-')); + const libDir = path.join(packagePath, 'lib'); + const nativeTscPath = path.join(libDir, 'tsc'); + + tempDirs.push(packagePath); + fs.mkdirSync(libDir, { recursive: true }); + fs.writeFileSync( + path.join(packagePath, 'package.json'), + JSON.stringify({ + name: 'typescript', + version, + bin: { + tsc: 'bin/tsc', + }, + }), + ); + fs.writeFileSync( + path.join(libDir, 'getExePath.js'), + "module.exports = function getExePath() { return require('./nativeTscPath.json'); };\n", + ); + fs.writeFileSync(path.join(libDir, 'nativeTscPath.json'), JSON.stringify(nativeTscPath)); + fs.writeFileSync(nativeTscPath, '#!/usr/bin/env node\n'); + fs.chmodSync(nativeTscPath, 0o755); + + return { + packageJsonPath: path.join(packagePath, 'package.json'), + nativeTscPath, + }; + } + + afterEach(() => { + for (const tempDir of tempDirs.splice(0)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); it('creates tsgo project args for regular checks', async () => { const { createTypeScriptGoArgs } = await import('src/typescript/type-script-go-runner'); @@ -82,6 +123,44 @@ describe('typescript/type-script-go-runner', () => { ).toThrowError('typescriptPath option must be an absolute path'); }); + it('resolves native executable from supported TypeScript package', async () => { + const { packageJsonPath, nativeTscPath } = createTypeScriptPackage('7.1.0'); + const { resolveTypeScriptGoBinPath, resolveTypeScriptGoExecutable } = + await import('src/typescript/type-script-go-runner'); + + await expect( + resolveTypeScriptGoBinPath({ + ...config, + typescriptPath: packageJsonPath, + tsgoPackage: 'typescript', + }), + ).resolves.toBe(nativeTscPath); + await expect( + resolveTypeScriptGoExecutable({ + ...config, + typescriptPath: packageJsonPath, + tsgoPackage: 'typescript', + }), + ).resolves.toEqual({ + command: nativeTscPath, + args: [], + }); + }); + + it('rejects tsgo package paths that were not classified by config', async () => { + const { packageJsonPath } = createTypeScriptPackage('6.0.3'); + const { resolveTypeScriptGoPackageJsonPath } = + await import('src/typescript/type-script-go-runner'); + + expect(() => + resolveTypeScriptGoPackageJsonPath({ + ...config, + typescriptPath: packageJsonPath, + tsgoPackage: undefined, + }), + ).toThrowError('typescript@rc'); + }); + it('extracts the error count from tsgo summary output', async () => { const { getTypeScriptGoErrorCount } = await import('src/typescript/type-script-go-runner'); diff --git a/test/unit/typescript/type-script-support.spec.ts b/test/unit/typescript/type-script-support.spec.ts index 916d0e2..cc39b63 100644 --- a/test/unit/typescript/type-script-support.spec.ts +++ b/test/unit/typescript/type-script-support.spec.ts @@ -5,6 +5,35 @@ import type { TypeScriptWorkerConfig } from 'src/typescript/type-script-worker-c describe('typescript/type-script-support', () => { let configuration: TypeScriptWorkerConfig; + const tempDirs: string[] = []; + + function createTypeScriptPackage(version: string) { + const packagePath = fs.mkdtempSync(path.join(os.tmpdir(), 'ts-checker-typescript-support-')); + const libDir = path.join(packagePath, 'lib'); + const nativeTscPath = path.join(libDir, 'tsc'); + + tempDirs.push(packagePath); + fs.mkdirSync(libDir, { recursive: true }); + fs.writeFileSync( + path.join(packagePath, 'package.json'), + JSON.stringify({ + name: 'typescript', + version, + bin: { + tsc: 'bin/tsc', + }, + }), + ); + fs.writeFileSync( + path.join(libDir, 'getExePath.js'), + "module.exports = function getExePath() { return require('./nativeTscPath.json'); };\n", + ); + fs.writeFileSync(path.join(libDir, 'nativeTscPath.json'), JSON.stringify(nativeTscPath)); + fs.writeFileSync(nativeTscPath, '#!/usr/bin/env node\n'); + fs.chmodSync(nativeTscPath, 0o755); + + return path.join(packagePath, 'package.json'); + } beforeEach(() => { rs.resetModules(); @@ -28,6 +57,12 @@ describe('typescript/type-script-support', () => { }; }); + afterEach(() => { + for (const tempDir of tempDirs.splice(0)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + it('throws error if typescript is not installed', async () => { const { assertTypeScriptSupport } = await import('src/typescript/type-script-support'); @@ -67,25 +102,60 @@ describe('typescript/type-script-support', () => { let error: Error | undefined; try { + const missingPackageJsonPath = path.join( + os.tmpdir(), + 'ts-checker-native-preview-missing', + 'package.json', + ); + assertTypeScriptSupport({ ...configuration, - context: path.join(os.tmpdir(), 'ts-checker-native-preview-missing'), - typescriptPath: path.join( - os.tmpdir(), - 'ts-checker-native-preview-missing/package.json' - ), + context: path.dirname(missingPackageJsonPath), + typescriptPath: missingPackageJsonPath, tsgo: true, + tsgoPackage: 'preview', }); } catch (caughtError) { error = caughtError as Error; } expect(error?.message).toContain( - 'When you enable TsCheckerRspackPlugin with `typescript.tsgo`, you must install `@typescript/native-preview` package.' + 'When you enable TsCheckerRspackPlugin with `typescript.tsgo`, you must install `typescript@rc` or `@typescript/native-preview` package.' ); expect(error?.message).toContain('If you set `typescript.typescriptPath`'); }); + it('supports TypeScript package with native executable for tsgo', async () => { + const packageJsonPath = createTypeScriptPackage('7.1.0'); + const { assertTypeScriptGoExecutable, assertTypeScriptSupport } = await import( + 'src/typescript/type-script-support' + ); + const config = { + ...configuration, + typescriptPath: packageJsonPath, + tsgo: true, + tsgoPackage: 'typescript', + }; + + expect(() => assertTypeScriptSupport(config)).not.toThrow(); + await expect(assertTypeScriptGoExecutable(config)).resolves.toBeUndefined(); + }); + + it('throws error if a package.json path is used without tsgo', async () => { + const packageJsonPath = createTypeScriptPackage('7.1.0'); + const { assertTypeScriptSupport } = await import('src/typescript/type-script-support'); + + expect(() => + assertTypeScriptSupport({ + ...configuration, + typescriptPath: packageJsonPath, + tsgo: false, + }) + ).toThrowError( + "When you use TsCheckerRspackPlugin without `typescript.tsgo`, `typescript.typescriptPath` should point to a path like `require.resolve('typescript')`." + ); + }); + it('does not print the typescriptPath hint for the default typescript-go path', async () => { const existsSyncSpy = rs .spyOn(fs, 'existsSync') @@ -100,6 +170,7 @@ describe('typescript/type-script-support', () => { ...configuration, typescriptPath: require.resolve('@typescript/native-preview/package.json'), tsgo: true, + tsgoPackage: 'preview', }); } catch (caughtError) { error = caughtError as Error; @@ -108,7 +179,7 @@ describe('typescript/type-script-support', () => { } expect(error?.message).toContain( - 'When you enable TsCheckerRspackPlugin with `typescript.tsgo`, you must install `@typescript/native-preview` package.' + 'When you enable TsCheckerRspackPlugin with `typescript.tsgo`, you must install `typescript@rc` or `@typescript/native-preview` package.' ); expect(error?.message).not.toContain('If you set `typescript.typescriptPath`'); }); @@ -124,13 +195,14 @@ describe('typescript/type-script-support', () => { // optional peer dependency cannot be resolved, not a supported custom value. typescriptPath: '@typescript/native-preview/package.json', tsgo: true, + tsgoPackage: 'preview', }); } catch (caughtError) { error = caughtError as Error; } expect(error?.message).toContain( - 'When you enable TsCheckerRspackPlugin with `typescript.tsgo`, you must install `@typescript/native-preview` package.' + 'When you enable TsCheckerRspackPlugin with `typescript.tsgo`, you must install `typescript@rc` or `@typescript/native-preview` package.' ); expect(error?.message).not.toContain('If you set `typescript.typescriptPath`'); }); @@ -154,6 +226,7 @@ describe('typescript/type-script-support', () => { ...configuration, typescriptPath: require.resolve('@typescript/native-preview/package.json'), tsgo: true, + tsgoPackage: 'preview', }) ).rejects.toThrowError('Failed to resolve the tsgo executable: Executable not found'); } finally { diff --git a/test/unit/typescript/type-script-worker-config.spec.ts b/test/unit/typescript/type-script-worker-config.spec.ts index a884ece..db0ef7f 100644 --- a/test/unit/typescript/type-script-worker-config.spec.ts +++ b/test/unit/typescript/type-script-worker-config.spec.ts @@ -1,3 +1,5 @@ +import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import type { TypeScriptWorkerConfig } from 'src/typescript/type-script-worker-config'; @@ -6,7 +8,12 @@ import type * as rspack from '@rspack/core'; describe('typescript/type-scripts-worker-config', () => { let compiler: rspack.Compiler; - const context = '/webpack/context'; + const context = path.resolve('webpack-context'); + const customPreviewPackageJsonPath = path.resolve( + 'custom', + 'native-preview', + 'package.json', + ); const configuration: TypeScriptWorkerConfig = { enabled: true, @@ -28,8 +35,31 @@ describe('typescript/type-scripts-worker-config', () => { const tsgoConfiguration: TypeScriptWorkerConfig = { ...configuration, tsgo: true, + tsgoPackage: 'preview', typescriptPath: require.resolve('@typescript/native-preview/package.json'), }; + const tempDirs: string[] = []; + + function createTypeScriptPackage(version: string) { + const packagePath = fs.mkdtempSync(path.join(os.tmpdir(), 'ts-checker-typescript-config-')); + const binDir = path.join(packagePath, 'bin'); + + tempDirs.push(packagePath); + fs.mkdirSync(binDir, { recursive: true }); + fs.writeFileSync( + path.join(packagePath, 'package.json'), + JSON.stringify({ + name: 'typescript', + version, + bin: { + tsc: 'bin/tsc', + }, + }), + ); + fs.writeFileSync(path.join(binDir, 'tsc'), '#!/usr/bin/env node\n'); + + return path.join(packagePath, 'package.json'); + } beforeEach(() => { compiler = { @@ -40,6 +70,9 @@ describe('typescript/type-scripts-worker-config', () => { }); afterEach(() => { rs.resetModules(); + for (const tempDir of tempDirs.splice(0)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } }); it.each([ @@ -84,10 +117,11 @@ describe('typescript/type-scripts-worker-config', () => { [{ profile: true }, { ...configuration, profile: true }], [{ tsgo: true }, tsgoConfiguration], [ - { tsgo: true, typescriptPath: '/custom/native-preview/package.json' }, + { tsgo: true, typescriptPath: customPreviewPackageJsonPath }, { - ...tsgoConfiguration, - typescriptPath: '/custom/native-preview/package.json', + ...configuration, + tsgo: true, + typescriptPath: customPreviewPackageJsonPath, }, ], ])('creates configuration from options %p', async (options, expectedConfig) => { @@ -98,4 +132,89 @@ describe('typescript/type-scripts-worker-config', () => { expect(config).toEqual(expectedConfig); }); + + it('infers tsgo from the default TypeScript package only when tsgo is not configured', async () => { + const packageJsonPath = require.resolve('typescript/package.json'); + const packageModule = await import('src/typescript/type-script-go-package'); + const resolveTypeScriptGoPackageSpy = rs + .spyOn(packageModule, 'resolveTypeScriptGoPackage') + .mockImplementation((resolvedPackageJsonPath: string) => ({ + packageJsonPath: resolvedPackageJsonPath, + tsgoPackage: 'typescript', + })); + + try { + const { createTypeScriptWorkerConfig } = await import( + 'src/typescript/type-script-worker-config' + ); + + expect(createTypeScriptWorkerConfig(compiler, {})).toEqual({ + ...configuration, + tsgo: true, + tsgoPackage: 'typescript', + typescriptPath: packageJsonPath, + }); + expect(createTypeScriptWorkerConfig(compiler, { tsgo: false })).toEqual({ + ...configuration, + tsgo: false, + }); + } finally { + resolveTypeScriptGoPackageSpy.mockRestore(); + } + }); + + it('infers tsgo when typescriptPath points to supported TypeScript package', async () => { + const packageJsonPath = createTypeScriptPackage('7.1.0'); + const { createTypeScriptWorkerConfig } = await import( + 'src/typescript/type-script-worker-config' + ); + + expect( + createTypeScriptWorkerConfig(compiler, { + typescriptPath: packageJsonPath, + }) + ).toEqual({ + ...configuration, + tsgo: true, + tsgoPackage: 'typescript', + typescriptPath: packageJsonPath, + }); + }); + + it('uses configured TypeScript package path when tsgo is explicitly enabled', async () => { + const packageJsonPath = createTypeScriptPackage('7.1.0'); + const { createTypeScriptWorkerConfig } = await import( + 'src/typescript/type-script-worker-config' + ); + + expect( + createTypeScriptWorkerConfig(compiler, { + tsgo: true, + typescriptPath: packageJsonPath, + }) + ).toEqual({ + ...configuration, + tsgo: true, + tsgoPackage: 'typescript', + typescriptPath: packageJsonPath, + }); + }); + + it('does not infer tsgo from configured TypeScript package path when tsgo is disabled', async () => { + const packageJsonPath = createTypeScriptPackage('7.1.0'); + const { createTypeScriptWorkerConfig } = await import( + 'src/typescript/type-script-worker-config' + ); + + expect( + createTypeScriptWorkerConfig(compiler, { + tsgo: false, + typescriptPath: packageJsonPath, + }) + ).toEqual({ + ...configuration, + tsgo: false, + typescriptPath: packageJsonPath, + }); + }); });