Skip to content

Commit 5974851

Browse files
authored
fix(js): infer dependency between typecheck and build tasks and more granular outputs for typecheck (#30549)
## Current Behavior There is no dependency between the inferred `typecheck` and `build` tasks. Depending on their run order, this can result in duplicated processing (type-checking, `.d.ts` generation). Given there's no explicit dependency, the order would be non-deterministic. Additionally, when `outDir` is set in the tsconfig files, it's used as-is in the currently inferred outputs for `typecheck`. This can result in extra files being cached for the task. ## Expected Behavior For optimum performance, the inferred `typecheck` task should depend on the `build` task. The `typecheck` task's outputs should be more granular so that only the relevant files (declaration files and declaration map files if enabled) are cached. ### Explanation Consider a typical setup with specific tsconfig file for files with different concerns: - tsconfig.lib.json: TS configuration for the library runtime files - tsconfig.spec.json: TS configuration for the unit test files - tsconfig.json: TS solution configuration, a solution file that references the specific config files above When running `tsc -b tsconfig.lib.json --verbose` (build), we can see how the `tsconfig.lib.json` TS project is built: ```bash Projects in this build: * tsconfig.lib.json Project 'tsconfig.lib.json' is out of date because output file 'dist/tsconfig.lib.tsbuildinfo' does not exist Building project '<workspace root>/packages/pkg1/tsconfig.lib.json'... ``` After that, if we run `tsc -b tsconfig.json --emitDeclarationOnly --verbose` (typecheck), we'll see how the `tsc` output for `tsconfig.lib.json` is reused: ```bash Projects in this build: * tsconfig.lib.json * tsconfig.spec.json * tsconfig.json Project 'tsconfig.lib.json' is up to date because newest input 'src/lib/file.ts' is older than output 'dist/tsconfig.lib.tsbuildinfo' Project 'tsconfig.spec.json' is out of date because output file 'out-tsc/jest/tsconfig.spec.tsbuildinfo' does not exist Building project '<workspace root>/packages/pkg1/tsconfig.spec.json'... ``` The relevant bit above is `Project 'tsconfig.lib.json' is up to date because newest input 'src/lib/file.ts' is older than output 'dist/tsconfig.lib.tsbuildinfo'`. Because the initial `build` task already typechecks and produces `.d.ts` files for the `tsconfig.lib.json`, when the `typecheck` task runs, `tsc` identifies that the outputs for that config files were already produced and can be reused. If we were to run the tasks in the inverse order, the results would be different: ```bash > npx tsc -b tsconfig.json --emitDeclarationOnly --verbose Projects in this build: * tsconfig.lib.json * tsconfig.spec.json * tsconfig.json Project 'tsconfig.lib.json' is out of date because output file 'dist/tsconfig.lib.tsbuildinfo' does not exist Building project '<workspace root>/packages/pkg1/tsconfig.lib.json'... Project 'tsconfig.spec.json' is out of date because output file 'out-tsc/jest/tsconfig.spec.tsbuildinfo' does not exist Building project '<workspace root>/packages/pkg1/tsconfig.spec.json'... > npx tsc -b tsconfig.lib.json --verbose Projects in this build: * tsconfig.lib.json Project 'tsconfig.lib.json' is out of date because buildinfo file 'dist/tsconfig.lib.tsbuildinfo' indicates there is change in compilerOptions Building project '<workspace root>/packages/pkg1/tsconfig.lib.json'... ``` Note how when the `build` task is run, `tsc` identifies that there was a change in `compilerOptions` (`--emitDeclarationOnly`) and it requires building the project. This is because the `typecheck` task only generates declaration files and the `build` task must also emit the transpiled `.js` files. ### Benchmark Running those two different flows in a simple (non-Nx) project with a TS configuration structure like the one mentioned above and with 5000 TS files split in half for runtime and test files yields the following: ```bash hyperfine -r 5 -p "rm -rf dist out-tsc" \ -n "build => typecheck" "npx tsc -b tsconfig.lib.json && npx tsc -b --emitDeclarationOnly" \ -n "typecheck => build" "npx tsc -b tsconfig.json --emitDeclarationOnly && npx tsc -b tsconfig.lib.json" Benchmark 1: build => typecheck Time (mean ± σ): 6.832 s ± 0.094 s [User: 11.361 s, System: 1.060 s] Range (min … max): 6.734 s … 6.985 s 5 runs Benchmark 2: typecheck => build Time (mean ± σ): 8.789 s ± 0.015 s [User: 14.817 s, System: 1.267 s] Range (min … max): 8.771 s … 8.812 s 5 runs Summary build => typecheck ran 1.29 ± 0.02 times faster than typecheck => build ``` ## Related Issue(s) Fixes #
1 parent e29f8f0 commit 5974851

File tree

4 files changed

+111
-38
lines changed

4 files changed

+111
-38
lines changed

e2e/js/src/js-ts-solution.test.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -115,36 +115,36 @@ ${content}`
115115

116116
// check build
117117
expect(runCLI(`build ${esbuildParentLib}`)).toContain(
118-
`Successfully ran target build for project @proj/${esbuildParentLib} and 5 tasks it depends on`
118+
`Successfully ran target build for project @proj/${esbuildParentLib}`
119119
);
120120
expect(runCLI(`build ${rollupParentLib}`)).toContain(
121-
`Successfully ran target build for project @proj/${rollupParentLib} and 5 tasks it depends on`
121+
`Successfully ran target build for project @proj/${rollupParentLib}`
122122
);
123123
expect(runCLI(`build ${swcParentLib}`)).toContain(
124-
`Successfully ran target build for project @proj/${swcParentLib} and 5 tasks it depends on`
124+
`Successfully ran target build for project @proj/${swcParentLib}`
125125
);
126126
expect(runCLI(`build ${tscParentLib}`)).toContain(
127-
`Successfully ran target build for project @proj/${tscParentLib} and 5 tasks it depends on`
127+
`Successfully ran target build for project @proj/${tscParentLib}`
128128
);
129129
expect(runCLI(`build ${viteParentLib}`)).toContain(
130-
`Successfully ran target build for project @proj/${viteParentLib} and 5 tasks it depends on`
130+
`Successfully ran target build for project @proj/${viteParentLib}`
131131
);
132132

133133
// check typecheck
134134
expect(runCLI(`typecheck ${esbuildParentLib}`)).toContain(
135-
`Successfully ran target typecheck for project @proj/${esbuildParentLib} and 5 tasks it depends on`
135+
`Successfully ran target typecheck for project @proj/${esbuildParentLib}`
136136
);
137137
expect(runCLI(`typecheck ${rollupParentLib}`)).toContain(
138-
`Successfully ran target typecheck for project @proj/${rollupParentLib} and 5 tasks it depends on`
138+
`Successfully ran target typecheck for project @proj/${rollupParentLib}`
139139
);
140140
expect(runCLI(`typecheck ${swcParentLib}`)).toContain(
141-
`Successfully ran target typecheck for project @proj/${swcParentLib} and 5 tasks it depends on`
141+
`Successfully ran target typecheck for project @proj/${swcParentLib}`
142142
);
143143
expect(runCLI(`typecheck ${tscParentLib}`)).toContain(
144-
`Successfully ran target typecheck for project @proj/${tscParentLib} and 5 tasks it depends on`
144+
`Successfully ran target typecheck for project @proj/${tscParentLib}`
145145
);
146146
expect(runCLI(`typecheck ${viteParentLib}`)).toContain(
147-
`Successfully ran target typecheck for project @proj/${viteParentLib} and 5 tasks it depends on`
147+
`Successfully ran target typecheck for project @proj/${viteParentLib}`
148148
);
149149

150150
// check lint

e2e/vite/src/vite-ts-solution.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,12 @@ ${content}`
101101

102102
// check build
103103
expect(runCLI(`build ${reactApp}`)).toContain(
104-
`Successfully ran target build for project @proj/${reactApp} and 5 tasks it depends on`
104+
`Successfully ran target build for project @proj/${reactApp}`
105105
);
106106

107107
// check typecheck
108108
expect(runCLI(`typecheck ${reactApp}`)).toContain(
109-
`Successfully ran target typecheck for project @proj/${reactApp} and 6 tasks it depends on`
109+
`Successfully ran target typecheck for project @proj/${reactApp}`
110110
);
111111
}, 300_000);
112112
});

packages/js/src/plugins/typescript/plugin.spec.ts

+35-18
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
782782
"cwd": "libs/my-lib",
783783
},
784784
"outputs": [
785-
"{projectRoot}/dist",
785+
"{projectRoot}/dist/**/*.d.ts",
786+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
786787
],
787788
"syncGenerators": [
788789
"@nx/js:typescript-sync",
@@ -853,7 +854,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
853854
"cwd": "libs/my-lib",
854855
},
855856
"outputs": [
856-
"{projectRoot}/dist",
857+
"{projectRoot}/dist/**/*.d.ts",
858+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
857859
],
858860
"syncGenerators": [
859861
"@nx/js:typescript-sync",
@@ -928,7 +930,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
928930
"cwd": "libs/my-lib",
929931
},
930932
"outputs": [
931-
"{projectRoot}/dist",
933+
"{projectRoot}/dist/**/*.d.ts",
934+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
932935
],
933936
"syncGenerators": [
934937
"@nx/js:typescript-sync",
@@ -1003,7 +1006,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
10031006
"cwd": "libs/my-lib",
10041007
},
10051008
"outputs": [
1006-
"{projectRoot}/dist",
1009+
"{projectRoot}/dist/**/*.d.ts",
1010+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
10071011
],
10081012
"syncGenerators": [
10091013
"@nx/js:typescript-sync",
@@ -1083,7 +1087,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
10831087
"cwd": "libs/my-lib",
10841088
},
10851089
"outputs": [
1086-
"{projectRoot}/dist",
1090+
"{projectRoot}/dist/**/*.d.ts",
1091+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
10871092
],
10881093
"syncGenerators": [
10891094
"@nx/js:typescript-sync",
@@ -1158,7 +1163,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
11581163
"cwd": "libs/my-lib",
11591164
},
11601165
"outputs": [
1161-
"{projectRoot}/dist",
1166+
"{projectRoot}/dist/**/*.d.ts",
1167+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
11621168
],
11631169
"syncGenerators": [
11641170
"@nx/js:typescript-sync",
@@ -1240,7 +1246,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
12401246
"cwd": "libs/my-lib",
12411247
},
12421248
"outputs": [
1243-
"{projectRoot}/dist",
1249+
"{projectRoot}/dist/**/*.d.ts",
1250+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
12441251
],
12451252
"syncGenerators": [
12461253
"@nx/js:typescript-sync",
@@ -1348,8 +1355,10 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
13481355
"cwd": "libs/my-lib",
13491356
},
13501357
"outputs": [
1351-
"{projectRoot}/dist",
1352-
"{projectRoot}/cypress/dist",
1358+
"{projectRoot}/dist/**/*.d.ts",
1359+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
1360+
"{projectRoot}/cypress/dist/**/*.d.ts",
1361+
"{projectRoot}/cypress/dist/tsconfig.tsbuildinfo",
13531362
],
13541363
"syncGenerators": [
13551364
"@nx/js:typescript-sync",
@@ -1395,7 +1404,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
13951404
"cwd": "libs/my-lib/nested-project",
13961405
},
13971406
"outputs": [
1398-
"{projectRoot}/dist",
1407+
"{projectRoot}/dist/**/*.d.ts",
1408+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
13991409
],
14001410
"syncGenerators": [
14011411
"@nx/js:typescript-sync",
@@ -1492,7 +1502,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
14921502
"cwd": "libs/my-lib",
14931503
},
14941504
"outputs": [
1495-
"{projectRoot}/dist",
1505+
"{projectRoot}/dist/**/*.d.ts",
1506+
"{projectRoot}/dist/tsconfig.tsbuildinfo",
14961507
],
14971508
"syncGenerators": [
14981509
"@nx/js:typescript-sync",
@@ -1882,7 +1893,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
18821893
"cwd": "libs/my-lib",
18831894
},
18841895
"outputs": [
1885-
"{workspaceRoot}/dist/libs/my-lib",
1896+
"{workspaceRoot}/dist/libs/my-lib/**/*.d.ts",
1897+
"{workspaceRoot}/dist/libs/my-lib/tsconfig.tsbuildinfo",
18861898
],
18871899
"syncGenerators": [
18881900
"@nx/js:typescript-sync",
@@ -1964,6 +1976,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
19641976
"cache": true,
19651977
"command": "tsc --build --emitDeclarationOnly",
19661978
"dependsOn": [
1979+
"build",
19671980
"^typecheck",
19681981
],
19691982
"inputs": [
@@ -1993,7 +2006,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
19932006
"cwd": "libs/my-lib",
19942007
},
19952008
"outputs": [
1996-
"{projectRoot}/out-tsc/my-lib",
2009+
"{projectRoot}/out-tsc/my-lib/**/*.d.ts",
19972010
"{projectRoot}/out-tsc/*.tsbuildinfo",
19982011
],
19992012
"syncGenerators": [
@@ -2171,8 +2184,10 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
21712184
"{workspaceRoot}/dist/libs/my-lib/lib.d.ts",
21722185
"{workspaceRoot}/dist/libs/my-lib/lib.d.ts.map",
21732186
"{workspaceRoot}/dist/libs/my-lib/lib.tsbuildinfo",
2174-
"{workspaceRoot}/dist/out-tsc/libs/my-lib/specs",
2175-
"{workspaceRoot}/dist/out-tsc/libs/my-lib/cypress",
2187+
"{workspaceRoot}/dist/out-tsc/libs/my-lib/specs/**/*.d.ts",
2188+
"{workspaceRoot}/dist/out-tsc/libs/my-lib/specs/tsconfig.tsbuildinfo",
2189+
"{workspaceRoot}/dist/out-tsc/libs/my-lib/cypress/**/*.d.ts",
2190+
"{workspaceRoot}/dist/out-tsc/libs/my-lib/cypress/tsconfig.tsbuildinfo",
21762191
],
21772192
"syncGenerators": [
21782193
"@nx/js:typescript-sync",
@@ -2216,7 +2231,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
22162231
"cwd": "libs/my-lib/nested-project",
22172232
},
22182233
"outputs": [
2219-
"{workspaceRoot}/dist/out-tsc/libs/my-lib/nested-project",
2234+
"{workspaceRoot}/dist/out-tsc/libs/my-lib/nested-project/**/*.d.ts",
2235+
"{workspaceRoot}/dist/out-tsc/libs/my-lib/nested-project/tsconfig.tsbuildinfo",
22202236
],
22212237
"syncGenerators": [
22222238
"@nx/js:typescript-sync",
@@ -2424,7 +2440,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
24242440
"cwd": "libs/my-lib",
24252441
},
24262442
"outputs": [
2427-
"{projectRoot}/dist",
2443+
"{projectRoot}/dist/**/*.d.ts",
24282444
"{projectRoot}/my-lib.tsbuildinfo",
24292445
],
24302446
"syncGenerators": [
@@ -2490,7 +2506,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
24902506
"cwd": "libs/my-lib",
24912507
},
24922508
"outputs": [
2493-
"{projectRoot}/dist",
2509+
"{projectRoot}/dist/**/*.d.ts",
2510+
"{projectRoot}/dist/my-lib.tsbuildinfo",
24942511
],
24952512
"syncGenerators": [
24962513
"@nx/js:typescript-sync",

packages/js/src/plugins/typescript/plugin.ts

+64-8
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ async function getConfigFileHash(
322322
...(packageJson ? [hashObject(packageJson)] : []),
323323
// change this to bust the cache when making changes that would yield
324324
// different results for the same hash
325-
hashObject({ bust: 1 }),
325+
hashObject({ bust: 2 }),
326326
]);
327327
}
328328

@@ -415,8 +415,30 @@ function buildTscTargets(
415415
command = `echo "The 'typecheck' target is disabled because one or more project references set 'noEmit: true' in their tsconfig. Remove this property to resolve this issue."`;
416416
}
417417

418+
const dependsOn: string[] = [`^${targetName}`];
419+
if (options.build && targets[options.build.targetName]) {
420+
// we already processed and have a build target
421+
dependsOn.unshift(options.build.targetName);
422+
} else if (options.build) {
423+
// check if the project will have a build target
424+
const buildConfigPath = joinPathFragments(
425+
projectRoot,
426+
options.build.configName
427+
);
428+
if (
429+
context.configFiles.some((f) => f === buildConfigPath) &&
430+
isValidPackageJsonBuildConfig(
431+
retrieveTsConfigFromCache(buildConfigPath, context.workspaceRoot),
432+
context.workspaceRoot,
433+
projectRoot
434+
)
435+
) {
436+
dependsOn.unshift(options.build.targetName);
437+
}
438+
}
439+
418440
targets[targetName] = {
419-
dependsOn: [`^${targetName}`],
441+
dependsOn,
420442
command,
421443
options: { cwd: projectRoot },
422444
cache: true,
@@ -433,7 +455,8 @@ function buildTscTargets(
433455
tsConfig,
434456
internalProjectReferences,
435457
context.workspaceRoot,
436-
projectRoot
458+
projectRoot,
459+
/* emitDeclarationOnly */ true
437460
),
438461
syncGenerators: ['@nx/js:typescript-sync'],
439462
metadata: {
@@ -483,7 +506,9 @@ function buildTscTargets(
483506
tsConfig,
484507
internalProjectReferences,
485508
context.workspaceRoot,
486-
projectRoot
509+
projectRoot,
510+
// should be false for build target, but providing it just in case is set to true
511+
tsConfig.options.emitDeclarationOnly
487512
),
488513
syncGenerators: ['@nx/js:typescript-sync'],
489514
metadata: {
@@ -685,7 +710,8 @@ function getOutputs(
685710
tsConfig: ParsedTsconfigData,
686711
internalProjectReferences: Record<string, ParsedTsconfigData>,
687712
workspaceRoot: string,
688-
projectRoot: string
713+
projectRoot: string,
714+
emitDeclarationOnly: boolean
689715
): string[] {
690716
const outputs = new Set<string>();
691717

@@ -738,12 +764,32 @@ function getOutputs(
738764
)
739765
);
740766
} else if (config.options.outDir) {
741-
outputs.add(
742-
pathToInputOrOutput(config.options.outDir, workspaceRoot, projectRoot)
743-
);
767+
if (emitDeclarationOnly) {
768+
outputs.add(
769+
pathToInputOrOutput(
770+
joinPathFragments(config.options.outDir, '**/*.d.ts'),
771+
workspaceRoot,
772+
projectRoot
773+
)
774+
);
775+
if (tsConfig.options.declarationMap) {
776+
outputs.add(
777+
pathToInputOrOutput(
778+
joinPathFragments(config.options.outDir, '**/*.d.ts.map'),
779+
workspaceRoot,
780+
projectRoot
781+
)
782+
);
783+
}
784+
} else {
785+
outputs.add(
786+
pathToInputOrOutput(config.options.outDir, workspaceRoot, projectRoot)
787+
);
788+
}
744789

745790
if (config.options.tsBuildInfoFile) {
746791
if (
792+
emitDeclarationOnly ||
747793
!normalize(config.options.tsBuildInfoFile).startsWith(
748794
`${normalize(config.options.outDir)}${sep}`
749795
)
@@ -774,6 +820,16 @@ function getOutputs(
774820
projectRoot
775821
)
776822
);
823+
} else if (emitDeclarationOnly) {
824+
// https://www.typescriptlang.org/tsconfig#tsBuildInfoFile
825+
const name = basename(configFilePath, '.json');
826+
outputs.add(
827+
pathToInputOrOutput(
828+
joinPathFragments(config.options.outDir, `${name}.tsbuildinfo`),
829+
workspaceRoot,
830+
projectRoot
831+
)
832+
);
777833
}
778834
} else if (
779835
config.raw?.include?.length ||

0 commit comments

Comments
 (0)