fix(js): handle ${configDir} in tsconfig files when inferring tasks (#31098)

## Current Behavior

The `${configDir}` template variable in tsconfig files is incorrectly
handled when inferring tasks with the `@nx/js/typescript` plugin.

## Expected Behavior

The `${configDir}` template variable in tsconfig files should be
correctly handled when inferring tasks with the `@nx/js/typescript`
plugin.

## Related Issue(s)

Fixes #30883
This commit is contained in:
Leosvel Pérez Espinosa 2025-05-07 12:15:56 +02:00 committed by GitHub
parent 30a7709d71
commit e6a3d77db3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 283 additions and 4 deletions

View File

@ -796,6 +796,73 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
`); `);
}); });
it('should add the config file and the `include` and `exclude` patterns using the "${configDir}" template', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': JSON.stringify({
include: ['${configDir}/src/**/*.ts'],
exclude: ['${configDir}/src/**/foo.ts'],
// set this to keep outputs smaller
compilerOptions: { outDir: 'dist' },
}),
'libs/my-lib/package.json': `{}`,
});
expect(await invokeCreateNodesOnMatchingFiles(context, {}))
.toMatchInlineSnapshot(`
{
"projects": {
"libs/my-lib": {
"projectType": "library",
"targets": {
"typecheck": {
"cache": true,
"command": "tsc --build --emitDeclarationOnly",
"dependsOn": [
"^typecheck",
],
"inputs": [
"{projectRoot}/package.json",
"{projectRoot}/tsconfig.json",
"{projectRoot}/src/**/*.ts",
"!{projectRoot}/src/**/foo.ts",
"^production",
{
"externalDependencies": [
"typescript",
],
},
],
"metadata": {
"description": "Runs type-checking for the project.",
"help": {
"command": "npx tsc --build --help",
"example": {
"args": [
"--force",
],
},
},
"technologies": [
"typescript",
],
},
"options": {
"cwd": "libs/my-lib",
},
"outputs": [
"{projectRoot}/dist/**/*.d.ts",
"{projectRoot}/dist/tsconfig.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
},
}
`);
});
it('should normalize and add directories in `include` with the ts extensions', async () => { it('should normalize and add directories in `include` with the ts extensions', async () => {
await applyFilesToTempFsAndContext(tempFs, context, { await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': JSON.stringify({ 'libs/my-lib/tsconfig.json': JSON.stringify({
@ -2608,6 +2675,68 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
} }
`); `);
}); });
it('should support the "${configDir}" template', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': JSON.stringify({
compilerOptions: { outDir: '${configDir}/dist' },
files: ['main.ts'],
}),
'libs/my-lib/package.json': `{}`,
});
expect(await invokeCreateNodesOnMatchingFiles(context, {}))
.toMatchInlineSnapshot(`
{
"projects": {
"libs/my-lib": {
"projectType": "library",
"targets": {
"typecheck": {
"cache": true,
"command": "tsc --build --emitDeclarationOnly",
"dependsOn": [
"^typecheck",
],
"inputs": [
"production",
"^production",
{
"externalDependencies": [
"typescript",
],
},
],
"metadata": {
"description": "Runs type-checking for the project.",
"help": {
"command": "npx tsc --build --help",
"example": {
"args": [
"--force",
],
},
},
"technologies": [
"typescript",
],
},
"options": {
"cwd": "libs/my-lib",
},
"outputs": [
"{projectRoot}/dist/**/*.d.ts",
"{projectRoot}/dist/tsconfig.tsbuildinfo",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
},
}
`);
});
}); });
}); });
@ -3351,6 +3480,78 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
`); `);
}); });
it('should add the config file and the `include` and `exclude` patterns using the "${configDir}" template', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
outDir: 'dist',
},
include: ['${configDir}/src/**/*.ts'],
exclude: ['${configDir}/src/**/*.spec.ts'],
}),
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/package.json': `{"main": "./dist/index.js"}`,
});
expect(
await invokeCreateNodesOnMatchingFiles(context, {
typecheck: false,
build: true,
})
).toMatchInlineSnapshot(`
{
"projects": {
"libs/my-lib": {
"projectType": "library",
"targets": {
"build": {
"cache": true,
"command": "tsc --build tsconfig.lib.json",
"dependsOn": [
"^build",
],
"inputs": [
"{projectRoot}/package.json",
"{projectRoot}/tsconfig.lib.json",
"{projectRoot}/src/**/*.ts",
"!{projectRoot}/src/**/*.spec.ts",
"^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}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
},
}
`);
});
it('should normalize and add directories in `include` with the ts extensions', async () => { it('should normalize and add directories in `include` with the ts extensions', async () => {
await applyFilesToTempFsAndContext(tempFs, context, { await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({ 'libs/my-lib/tsconfig.lib.json': JSON.stringify({
@ -4948,6 +5149,74 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
} }
`); `);
}); });
it('should support the "${configDir}" template', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
outDir: '${configDir}/dist',
},
files: ['main.ts'],
}),
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/package.json': `{"main": "./dist/index.js"}`,
});
expect(
await invokeCreateNodesOnMatchingFiles(context, {
typecheck: false,
build: true,
})
).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}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
},
}
`);
});
}); });
}); });
}); });

View File

@ -344,7 +344,7 @@ async function getConfigFileHash(
...(packageJson ? [hashObject(packageJson)] : []), ...(packageJson ? [hashObject(packageJson)] : []),
// change this to bust the cache when making changes that would yield // change this to bust the cache when making changes that would yield
// different results for the same hash // different results for the same hash
hashObject({ bust: 2 }), hashObject({ bust: 3 }),
]); ]);
} }
@ -636,11 +636,18 @@ function getInputs(
return [input]; return [input];
}; };
const configDirTemplate = '${configDir}';
const substituteConfigDir = (p: string) =>
p.startsWith(configDirTemplate) ? p.replace(configDirTemplate, './') : p;
projectTsConfigFiles.forEach(([configPath, config]) => { projectTsConfigFiles.forEach(([configPath, config]) => {
configFiles.add(configPath); configFiles.add(configPath);
const offset = relative(absoluteProjectRoot, dirname(configPath)); const offset = relative(absoluteProjectRoot, dirname(configPath));
(config.raw?.include ?? []).forEach((p: string) => { (config.raw?.include ?? []).forEach((p: string) => {
const normalized = normalizeInput(join(offset, p), config); const normalized = normalizeInput(
join(offset, substituteConfigDir(p)),
config
);
normalized.forEach((input) => includePaths.add(input)); normalized.forEach((input) => includePaths.add(input));
}); });
@ -653,11 +660,14 @@ function getInputs(
const otherFilesInclude: string[] = []; const otherFilesInclude: string[] = [];
projectTsConfigFiles.forEach(([path, c]) => { projectTsConfigFiles.forEach(([path, c]) => {
if (path !== configPath) { if (path !== configPath) {
otherFilesInclude.push(...(c.raw?.include ?? [])); otherFilesInclude.push(
...(c.raw?.include ?? []).map(substituteConfigDir)
);
} }
}); });
const normalize = (p: string) => (p.startsWith('./') ? p.slice(2) : p); const normalize = (p: string) => (p.startsWith('./') ? p.slice(2) : p);
config.raw.exclude.forEach((excludePath: string) => { config.raw.exclude.forEach((e: string) => {
const excludePath = substituteConfigDir(e);
if ( if (
!otherFilesInclude.some( !otherFilesInclude.some(
(includePath) => (includePath) =>