fix(js): do not add typecheck target if tsc is used for build (#30211)

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.

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## 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)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #

---------

Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
This commit is contained in:
Jack Hsu 2025-02-28 16:08:45 -05:00 committed by GitHub
parent bd78ac25b8
commit b992e2586b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 316 additions and 13 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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}`,

View File

@ -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}`
);

View File

@ -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);
});

View File

@ -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,
}
`);
});
});
});

View File

@ -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'),

View File

@ -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, {

View File

@ -380,7 +380,11 @@ function buildTscTargets(
let internalProjectReferences: Record<string, ParsedTsconfigData>;
// 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,

View File

@ -463,6 +463,9 @@ describe('NxPlugin Plugin Generator', () => {
"extends": "../tsconfig.base.json",
"files": [],
"include": [],
"nx": {
"addTypecheckTarget": false,
},
"references": [
{
"path": "./tsconfig.lib.json",