diff --git a/packages/js/src/plugins/typescript/plugin.spec.ts b/packages/js/src/plugins/typescript/plugin.spec.ts index 27516ebe8fe75..78b13d7ba05a2 100644 --- a/packages/js/src/plugins/typescript/plugin.spec.ts +++ b/packages/js/src/plugins/typescript/plugin.spec.ts @@ -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({ @@ -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': '{}', diff --git a/packages/js/src/plugins/typescript/plugin.ts b/packages/js/src/plugins/typescript/plugin.ts index 1fa00a7359aa8..e2431a6689ddf 100644 --- a/packages/js/src/plugins/typescript/plugin.ts +++ b/packages/js/src/plugins/typescript/plugin.ts @@ -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}`; @@ -334,14 +336,16 @@ function getInputs( projectRoot: string ): TargetConfiguration['inputs'] { const configFiles = new Set(); - const includePaths = new Set(); - const excludePaths = new Set(); + 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(); + const excludePaths = new Set(); const projectTsConfigFiles: [string, ParsedCommandLine][] = [ [configFilePath, tsConfig], ...Object.entries(internalProjectReferences), @@ -429,7 +433,7 @@ function getInputs( inputs.push('production' in namedInputs ? '^production' : '^default'); } - inputs.push({ externalDependencies: ['typescript'] }); + inputs.push({ externalDependencies }); return inputs; } @@ -537,23 +541,36 @@ function pathToInputOrOutput( function getExtendedConfigFiles( tsConfigPath: string, tsConfig: ParsedCommandLine -): string[] { +): { + files: string[]; + packages: string[]; +} { const extendedConfigFiles = new Set(); + const extendedExternalPackages = new Set(); 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( @@ -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; + } +}