Skip to content

Commit

Permalink
fix(js): fix resolution of extended tsconfig files in plugin (#28535)
Browse files Browse the repository at this point in the history
  • Loading branch information
leosvelperez authored Oct 25, 2024
1 parent ff630a3 commit fae8474
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 14 deletions.
159 changes: 159 additions & 0 deletions packages/js/src/plugins/typescript/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,83 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
`);
});

it('should add extended config files supporting node.js style resolution and set npm packages as external dependencies', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'tsconfig.base.json': JSON.stringify({
extends: '@tsconfig/strictest/tsconfig.json',
exclude: ['node_modules', 'tmp'],
}),
'tsconfig.foo.json': JSON.stringify({
extends: './tsconfig.base', // extensionless relative path
}),
'libs/my-lib/tsconfig.json': JSON.stringify({
extends: '../../tsconfig.foo.json',
include: ['src/**/*.ts'],
}),
'libs/my-lib/package.json': `{}`,
});
// simulate @tsconfig/strictest package
tempFs.createFilesSync({
'node_modules/@tsconfig/strictest/tsconfig.json': '{}',
});

expect(await invokeCreateNodesOnMatchingFiles(context, {}))
.toMatchInlineSnapshot(`
{
"projects": {
"libs/my-lib": {
"projectType": "library",
"targets": {
"typecheck": {
"cache": true,
"command": "tsc --build --emitDeclarationOnly --pretty --verbose",
"dependsOn": [
"^typecheck",
],
"inputs": [
"{workspaceRoot}/tsconfig.foo.json",
"{workspaceRoot}/tsconfig.base.json",
"{projectRoot}/tsconfig.json",
"{projectRoot}/src/**/*.ts",
"!{workspaceRoot}/node_modules",
"!{workspaceRoot}/tmp",
"^production",
{
"externalDependencies": [
"typescript",
"@tsconfig/strictest",
],
},
],
"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": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
},
}
`);
});

it('should add files from internal project references', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': JSON.stringify({
Expand Down Expand Up @@ -1999,6 +2076,88 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
`);
});

it('should add extended config files supporting node.js style resolution and set npm packages as external dependencies', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'tsconfig.base.json': JSON.stringify({
extends: '@tsconfig/strictest/tsconfig.json',
exclude: ['node_modules', 'tmp'],
}),
'tsconfig.foo.json': JSON.stringify({
extends: './tsconfig.base', // extensionless relative path
}),
'libs/my-lib/tsconfig.json': '{}',
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
extends: '../../tsconfig.foo.json',
include: ['src/**/*.ts'],
}),
'libs/my-lib/package.json': `{}`,
});
// simulate @tsconfig/strictest package
tempFs.createFilesSync({
'node_modules/@tsconfig/strictest/tsconfig.json': '{}',
});

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 --pretty --verbose",
"dependsOn": [
"^build",
],
"inputs": [
"{workspaceRoot}/tsconfig.foo.json",
"{workspaceRoot}/tsconfig.base.json",
"{projectRoot}/tsconfig.lib.json",
"{projectRoot}/src/**/*.ts",
"!{workspaceRoot}/node_modules",
"!{workspaceRoot}/tmp",
"^production",
{
"externalDependencies": [
"typescript",
"@tsconfig/strictest",
],
},
],
"metadata": {
"description": "Builds the project with \`tsc\`.",
"help": {
"command": "npx tsc --build --help",
"example": {
"args": [
"--force",
],
},
},
"technologies": [
"typescript",
],
},
"options": {
"cwd": "libs/my-lib",
},
"outputs": [],
"syncGenerators": [
"@nx/js:typescript-sync",
],
},
},
},
},
}
`);
});

it('should add files from internal project references', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': '{}',
Expand Down
69 changes: 55 additions & 14 deletions packages/js/src/plugins/typescript/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ async function createNodesInternal(
readCachedTsConfig(fullConfigPath)
);
const nodeHash = hashArray([
...[configFilePath, ...extendedConfigFiles, lockFileName].map(hashFile),
...[configFilePath, ...extendedConfigFiles.files, lockFileName].map(
hashFile
),
hashObject(options),
]);
const cacheKey = `${nodeHash}_${configFilePath}`;
Expand Down Expand Up @@ -334,14 +336,16 @@ function getInputs(
projectRoot: string
): TargetConfiguration['inputs'] {
const configFiles = new Set<string>();
const includePaths = new Set<string>();
const excludePaths = new Set<string>();
const externalDependencies = ['typescript'];

const extendedConfigFiles = getExtendedConfigFiles(configFilePath, tsConfig);
extendedConfigFiles.forEach((configPath) => {
extendedConfigFiles.files.forEach((configPath) => {
configFiles.add(configPath);
});
externalDependencies.push(...extendedConfigFiles.packages);

const includePaths = new Set<string>();
const excludePaths = new Set<string>();
const projectTsConfigFiles: [string, ParsedCommandLine][] = [
[configFilePath, tsConfig],
...Object.entries(internalProjectReferences),
Expand Down Expand Up @@ -429,7 +433,7 @@ function getInputs(
inputs.push('production' in namedInputs ? '^production' : '^default');
}

inputs.push({ externalDependencies: ['typescript'] });
inputs.push({ externalDependencies });

return inputs;
}
Expand Down Expand Up @@ -537,23 +541,36 @@ function pathToInputOrOutput(
function getExtendedConfigFiles(
tsConfigPath: string,
tsConfig: ParsedCommandLine
): string[] {
): {
files: string[];
packages: string[];
} {
const extendedConfigFiles = new Set<string>();
const extendedExternalPackages = new Set<string>();

let currentConfigPath = tsConfigPath;
let currentConfig = tsConfig;
while (currentConfig.raw?.extends) {
const extendedConfigPath = join(
dirname(currentConfigPath),
currentConfig.raw.extends
const extendedConfigPath = resolveExtendedTsConfigPath(
currentConfig.raw.extends,
dirname(currentConfigPath)
);
extendedConfigFiles.add(extendedConfigPath);
const extendedConfig = readCachedTsConfig(extendedConfigPath);
currentConfigPath = extendedConfigPath;
currentConfig = extendedConfig;
if (!extendedConfigPath) {
break;
}
if (extendedConfigPath.externalPackage) {
extendedExternalPackages.add(extendedConfigPath.externalPackage);
break;
}
extendedConfigFiles.add(extendedConfigPath.filePath);
currentConfig = readCachedTsConfig(extendedConfigPath.filePath);
currentConfigPath = extendedConfigPath.filePath;
}

return Array.from(extendedConfigFiles);
return {
files: Array.from(extendedConfigFiles),
packages: Array.from(extendedExternalPackages),
};
}

function resolveInternalProjectReferences(
Expand Down Expand Up @@ -742,3 +759,27 @@ function normalizePluginOptions(
build,
};
}

function resolveExtendedTsConfigPath(
tsConfigPath: string,
directory?: string
): { filePath: string; externalPackage?: string } | null {
try {
const resolvedPath = require.resolve(tsConfigPath, {
paths: directory ? [directory] : undefined,
});

if (tsConfigPath.startsWith('.')) {
return { filePath: resolvedPath };
}

// parse the package from the tsconfig path
const packageName = tsConfigPath.startsWith('@')
? tsConfigPath.split('/').slice(0, 2).join('/')
: tsConfigPath.split('/')[0];

return { filePath: resolvedPath, externalPackage: packageName };
} catch {
return null;
}
}

0 comments on commit fae8474

Please sign in to comment.