From b992e2586b9af58af52261c51730a40e99fbe8fd Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Fri, 28 Feb 2025 16:08:45 -0500 Subject: [PATCH] fix(js): do not add typecheck target if tsc is used for build (#30211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds support for skipping `typecheck` targets when using `@nx/js/typescript`. Inside `tsconfig.json` for each project, you can set `nx.addTypecheckTarget` to `false` to not infer `typecheck`. This allows use to skip `typecheck` for JS projects using `tsc` to build. Since `tsc` is already used during build, we don't need to run `typecheck` that is just duplicated work. ## Current Behavior JS libs using `tsc` to build will do typechecking twice. Once during `build` and once during `typecheck`. ## Expected Behavior JS libs using `tsc` do not infer `typecheck` by default. And users can change this behavior by setting `nx.addTypecheckTarget` in `tsconfig.json`. ## Related Issue(s) Fixes # --------- Co-authored-by: Leosvel Pérez Espinosa --- .../packages/js/documents/overview.md | 98 +++++++++++++++++++ docs/shared/packages/js/js-plugin.md | 98 +++++++++++++++++++ e2e/js/src/js-ts-solution.test.ts | 11 +-- e2e/node/src/node-ts-solution.test.ts | 1 - e2e/plugin/src/nx-plugin-ts-solution.test.ts | 3 - e2e/vite/src/vite-ts-solution.test.ts | 2 +- .../js/src/generators/library/library.spec.ts | 18 ++++ packages/js/src/generators/library/library.ts | 12 +++ .../js/src/plugins/typescript/plugin.spec.ts | 71 ++++++++++++++ packages/js/src/plugins/typescript/plugin.ts | 12 ++- .../src/generators/plugin/plugin.spec.ts | 3 + 11 files changed, 316 insertions(+), 13 deletions(-) diff --git a/docs/generated/packages/js/documents/overview.md b/docs/generated/packages/js/documents/overview.md index 5c7560a8c9..4395a2fbe3 100644 --- a/docs/generated/packages/js/documents/overview.md +++ b/docs/generated/packages/js/documents/overview.md @@ -63,6 +63,104 @@ Nx 20 updates the TS monorepo setup when using `--preset=ts`. The workspace is s To create with the older setup for TS monorepo with `compilerOptions.paths`, use `create-nx-workspace --preset=apps`. {% /callout %} +### How @nx/js Infers Tasks + +The `@nx/js/typescript` plugin will add a `typecheck` task to projects that have a `tsconfig.json`. + +This plugin adds a `build` task for projects that: + +1. Have a runtime tsconfig file (defaults to `tsconfig.lib.json`). +2. Have a `package.json` file containing entry points that are not source files. + +For example, this project is buildable and will have a `build` task. + +```json {% fileName="packages/pkg1/package.json" %} +{ + "name": "@acme/pkg1", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } +} +``` + +Whereas this project points to source files and will not have a `build` task. + +```json {% fileName="packages/pkg1/package.json" %} +{ + "name": "@acme/pkg1", + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + } +} +``` + +### View Inferred Tasks + +To view inferred tasks for a project, open the [project details view](/concepts/inferred-tasks) in Nx Console or run `nx show project my-project` in the command line. + +### @nx/js Configuration + +The `@nx/js/typescript` plugin is configured in the `plugins` array in `nx.json`. + +```json {% fileName="nx.json" %} +{ + "plugins": [ + { + "plugin": "@nx/js/typescript", + "options": { + "typecheck": { + "targetName": "typecheck" + }, + "build": { + "targetName": "build", + "configName": "tsconfig.lib.json" + } + } + } + ] +} +``` + +You can also set `typecheck` and `build` options to `false` to not infer the corresponding tasks. + +```json {% fileName="nx.json" %} +{ + "plugins": [ + { + "plugin": "@nx/js/typescript", + "options": { + "build": false + } + } + ] +} +``` + +### Disable Typechecking + +To disable `typecheck` task for a specific project, set the `nx.addTypecheckTarget` property to `false` in `tsconfig.json`. + +```json {% fileName="packages/pkg1/tsconfig.json" highlightLines=["10-12"] %} +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "nx": { + "addTypecheckTarget": false + } +} +``` + ## Create Libraries You can add a new JS/TS library with the following command: diff --git a/docs/shared/packages/js/js-plugin.md b/docs/shared/packages/js/js-plugin.md index 5c7560a8c9..4395a2fbe3 100644 --- a/docs/shared/packages/js/js-plugin.md +++ b/docs/shared/packages/js/js-plugin.md @@ -63,6 +63,104 @@ Nx 20 updates the TS monorepo setup when using `--preset=ts`. The workspace is s To create with the older setup for TS monorepo with `compilerOptions.paths`, use `create-nx-workspace --preset=apps`. {% /callout %} +### How @nx/js Infers Tasks + +The `@nx/js/typescript` plugin will add a `typecheck` task to projects that have a `tsconfig.json`. + +This plugin adds a `build` task for projects that: + +1. Have a runtime tsconfig file (defaults to `tsconfig.lib.json`). +2. Have a `package.json` file containing entry points that are not source files. + +For example, this project is buildable and will have a `build` task. + +```json {% fileName="packages/pkg1/package.json" %} +{ + "name": "@acme/pkg1", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } +} +``` + +Whereas this project points to source files and will not have a `build` task. + +```json {% fileName="packages/pkg1/package.json" %} +{ + "name": "@acme/pkg1", + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + } +} +``` + +### View Inferred Tasks + +To view inferred tasks for a project, open the [project details view](/concepts/inferred-tasks) in Nx Console or run `nx show project my-project` in the command line. + +### @nx/js Configuration + +The `@nx/js/typescript` plugin is configured in the `plugins` array in `nx.json`. + +```json {% fileName="nx.json" %} +{ + "plugins": [ + { + "plugin": "@nx/js/typescript", + "options": { + "typecheck": { + "targetName": "typecheck" + }, + "build": { + "targetName": "build", + "configName": "tsconfig.lib.json" + } + } + } + ] +} +``` + +You can also set `typecheck` and `build` options to `false` to not infer the corresponding tasks. + +```json {% fileName="nx.json" %} +{ + "plugins": [ + { + "plugin": "@nx/js/typescript", + "options": { + "build": false + } + } + ] +} +``` + +### Disable Typechecking + +To disable `typecheck` task for a specific project, set the `nx.addTypecheckTarget` property to `false` in `tsconfig.json`. + +```json {% fileName="packages/pkg1/tsconfig.json" highlightLines=["10-12"] %} +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "nx": { + "addTypecheckTarget": false + } +} +``` + ## Create Libraries You can add a new JS/TS library with the following command: diff --git a/e2e/js/src/js-ts-solution.test.ts b/e2e/js/src/js-ts-solution.test.ts index 6e6b1716a0..f73dea3a8b 100644 --- a/e2e/js/src/js-ts-solution.test.ts +++ b/e2e/js/src/js-ts-solution.test.ts @@ -131,19 +131,16 @@ ${content}` // check typecheck expect(runCLI(`typecheck ${esbuildParentLib}`)).toContain( - `Successfully ran target typecheck for project @proj/${esbuildParentLib} and 5 tasks it depends on` + `Successfully ran target typecheck for project @proj/${esbuildParentLib} and 4 tasks it depends on` ); expect(runCLI(`typecheck ${rollupParentLib}`)).toContain( - `Successfully ran target typecheck for project @proj/${rollupParentLib} and 5 tasks it depends on` + `Successfully ran target typecheck for project @proj/${rollupParentLib} and 4 tasks it depends on` ); expect(runCLI(`typecheck ${swcParentLib}`)).toContain( - `Successfully ran target typecheck for project @proj/${swcParentLib} and 5 tasks it depends on` - ); - expect(runCLI(`typecheck ${tscParentLib}`)).toContain( - `Successfully ran target typecheck for project @proj/${tscParentLib} and 5 tasks it depends on` + `Successfully ran target typecheck for project @proj/${swcParentLib} and 4 tasks it depends on` ); expect(runCLI(`typecheck ${viteParentLib}`)).toContain( - `Successfully ran target typecheck for project @proj/${viteParentLib} and 5 tasks it depends on` + `Successfully ran target typecheck for project @proj/${viteParentLib} and 4 tasks it depends on` ); // check lint diff --git a/e2e/node/src/node-ts-solution.test.ts b/e2e/node/src/node-ts-solution.test.ts index 4323815b3c..215741739e 100644 --- a/e2e/node/src/node-ts-solution.test.ts +++ b/e2e/node/src/node-ts-solution.test.ts @@ -105,7 +105,6 @@ describe('Node Applications', () => { expect(() => runCLI(`lint ${nodelib}`)).not.toThrow(); expect(() => runCLI(`test ${nodelib}`)).not.toThrow(); expect(() => runCLI(`build ${nodelib}`)).not.toThrow(); - expect(() => runCLI(`typecheck ${nodelib}`)).not.toThrow(); const p = await runCommandUntil( `serve ${nodeapp}`, diff --git a/e2e/plugin/src/nx-plugin-ts-solution.test.ts b/e2e/plugin/src/nx-plugin-ts-solution.test.ts index e7b56c0703..05d68b9ec4 100644 --- a/e2e/plugin/src/nx-plugin-ts-solution.test.ts +++ b/e2e/plugin/src/nx-plugin-ts-solution.test.ts @@ -45,9 +45,6 @@ describe('Nx Plugin (TS solution)', () => { expect(runCLI(`lint @proj/${plugin}`)).toContain( `Successfully ran target lint for project @proj/${plugin}` ); - expect(runCLI(`typecheck @proj/${plugin}`)).toContain( - `Successfully ran target typecheck for project @proj/${plugin}` - ); expect(runCLI(`build @proj/${plugin}`)).toContain( `Successfully ran target build for project @proj/${plugin}` ); diff --git a/e2e/vite/src/vite-ts-solution.test.ts b/e2e/vite/src/vite-ts-solution.test.ts index ae74cbde8d..78e85581b7 100644 --- a/e2e/vite/src/vite-ts-solution.test.ts +++ b/e2e/vite/src/vite-ts-solution.test.ts @@ -106,7 +106,7 @@ ${content}` // check typecheck expect(runCLI(`typecheck ${reactApp}`)).toContain( - `Successfully ran target typecheck for project @proj/${reactApp} and 6 tasks it depends on` + `Successfully ran target typecheck for project @proj/${reactApp} and 5 tasks it depends on` ); }, 300_000); }); diff --git a/packages/js/src/generators/library/library.spec.ts b/packages/js/src/generators/library/library.spec.ts index 298c7c19e5..92660897c4 100644 --- a/packages/js/src/generators/library/library.spec.ts +++ b/packages/js/src/generators/library/library.spec.ts @@ -2320,5 +2320,23 @@ describe('lib', () => { 'packages/**', ]); }); + + it('should add nx.addTypecheckTarget to tsconfig.json when using tsc to build to avoid duplicated typechecks', async () => { + await libraryGenerator(tree, { + ...defaultOptions, + useProjectJson: false, + directory: 'my-ts-lib', + bundler: 'tsc', + unitTestRunner: 'none', + linter: 'none', + }); + + expect(readJson(tree, 'my-ts-lib/tsconfig.json').nx) + .toMatchInlineSnapshot(` + { + "addTypecheckTarget": false, + } + `); + }); }); }); diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 022a1f06b2..7686dd4339 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -1049,6 +1049,12 @@ function createProjectTsConfigs( json.references.push({ path: './tsconfig.lib.json', }); + // If using `tsc` to build, then we do not want a typecheck target that duplicates the work, since both run `tsc`. + // This applies to `@nx/js/typescript` plugin only. + if (options.bundler === 'tsc') { + json['nx'] ??= {}; + json['nx'].addTypecheckTarget = false; + } return json; }); } else { @@ -1059,6 +1065,12 @@ function createProjectTsConfigs( include: [], references: [{ path: './tsconfig.lib.json' }], }; + // If using `tsc` to build, then we do not want a typecheck target that duplicates the work, since both run `tsc`. + // This applies to `@nx/js/typescript` plugin only. + if (options.bundler === 'tsc') { + tsconfig['nx'] ??= {}; + tsconfig['nx'].addTypecheckTarget = false; + } writeJson( tree, joinPathFragments(options.projectRoot, 'tsconfig.json'), diff --git a/packages/js/src/plugins/typescript/plugin.spec.ts b/packages/js/src/plugins/typescript/plugin.spec.ts index bd240d6dc7..89120bf141 100644 --- a/packages/js/src/plugins/typescript/plugin.spec.ts +++ b/packages/js/src/plugins/typescript/plugin.spec.ts @@ -657,6 +657,77 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => { ).resolves.not.toThrow(); }); + it('should not infer typecheck target when nx.addTypecheckTarget is false in tsconfig.json', async () => { + await applyFilesToTempFsAndContext(tempFs, context, { + 'libs/my-lib/tsconfig.json': JSON.stringify({ + nx: { addTypecheckTarget: false }, + }), + 'libs/my-lib/tsconfig.lib.json': JSON.stringify({ + compilerOptions: { outDir: 'out-tsc/my-lib', rootDir: 'src' }, + files: ['src/main.ts'], + }), + 'libs/my-lib/package.json': `{}`, + }); + + await expect( + invokeCreateNodesOnMatchingFiles(context, { + build: { + configName: 'tsconfig.lib.json', + }, + }) + ).resolves.toMatchInlineSnapshot(` + { + "projects": { + "libs/my-lib": { + "projectType": "library", + "targets": { + "build": { + "cache": true, + "command": "tsc --build tsconfig.lib.json", + "dependsOn": [ + "^build", + ], + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "typescript", + ], + }, + ], + "metadata": { + "description": "Builds the project with \`tsc\`.", + "help": { + "command": "npx tsc --build --help", + "example": { + "args": [ + "--force", + ], + }, + }, + "technologies": [ + "typescript", + ], + }, + "options": { + "cwd": "libs/my-lib", + }, + "outputs": [ + "{projectRoot}/out-tsc/my-lib", + "{projectRoot}/out-tsc/*.tsbuildinfo", + ], + "syncGenerators": [ + "@nx/js:typescript-sync", + ], + }, + }, + }, + }, + } + `); + }); + describe('inputs', () => { it('should add the config file and the `include` and `exclude` patterns', async () => { await applyFilesToTempFsAndContext(tempFs, context, { diff --git a/packages/js/src/plugins/typescript/plugin.ts b/packages/js/src/plugins/typescript/plugin.ts index 810ebbb219..9d6e6dc72a 100644 --- a/packages/js/src/plugins/typescript/plugin.ts +++ b/packages/js/src/plugins/typescript/plugin.ts @@ -380,7 +380,11 @@ function buildTscTargets( let internalProjectReferences: Record; // Typecheck target - if (basename(configFilePath) === 'tsconfig.json' && options.typecheck) { + if ( + basename(configFilePath) === 'tsconfig.json' && + options.typecheck && + tsConfig.raw?.['nx']?.addTypecheckTarget !== false + ) { internalProjectReferences = resolveInternalProjectReferences( tsConfig, context.workspaceRoot, @@ -1291,6 +1295,9 @@ function toAbsolutePaths( updatedCache[key] = { data: { options: { noEmit: data.options.noEmit }, + raw: { + nx: { addTypecheckTarget: data.raw?.['nx']?.addTypecheckTarget }, + }, extendedConfigFile: data.extendedConfigFile, }, extendedFilesHash, @@ -1347,6 +1354,9 @@ function toRelativePaths( updatedCache[key] = { data: { options: { noEmit: data.options.noEmit }, + raw: { + nx: { addTypecheckTarget: data.raw?.['nx']?.addTypecheckTarget }, + }, extendedConfigFile: data.extendedConfigFile, }, extendedFilesHash, diff --git a/packages/plugin/src/generators/plugin/plugin.spec.ts b/packages/plugin/src/generators/plugin/plugin.spec.ts index fc0d643633..a82e7e4f20 100644 --- a/packages/plugin/src/generators/plugin/plugin.spec.ts +++ b/packages/plugin/src/generators/plugin/plugin.spec.ts @@ -463,6 +463,9 @@ describe('NxPlugin Plugin Generator', () => { "extends": "../tsconfig.base.json", "files": [], "include": [], + "nx": { + "addTypecheckTarget": false, + }, "references": [ { "path": "./tsconfig.lib.json",