diff --git a/packages/js/src/generators/typescript-sync/typescript-sync.spec.ts b/packages/js/src/generators/typescript-sync/typescript-sync.spec.ts index 5b6576439c8cc..a3e07268cd085 100644 --- a/packages/js/src/generators/typescript-sync/typescript-sync.spec.ts +++ b/packages/js/src/generators/typescript-sync/typescript-sync.spec.ts @@ -368,7 +368,7 @@ describe('syncGenerator()', () => { references: [ { path: './some/thing' }, { path: './another/one' }, - { path: '../packages/c' }, // this is not a dependency, should be pruned + { path: '../c' }, // this is not a dependency, should be pruned ], }); @@ -391,6 +391,86 @@ describe('syncGenerator()', () => { `); }); + it('should not prune existing external project references that are not dependencies but are git ignored', async () => { + writeJson(tree, 'packages/b/tsconfig.json', { + compilerOptions: { + composite: true, + }, + references: [ + { path: './some/thing' }, + { path: './another/one' }, + { path: '../../some-path/dir' }, // this is not a dependency but it's git ignored, should not be pruned + { path: '../c' }, // this is not a dependency and it's not git ignored, should be pruned + ], + }); + tree.write('some-path/dir/tsconfig.json', '{}'); + tree.write('.gitignore', 'some-path/dir'); + + await syncGenerator(tree); + + const rootTsconfig = readJson(tree, 'packages/b/tsconfig.json'); + // The dependency reference on "a" is added to the start of the array + expect(rootTsconfig.references).toMatchInlineSnapshot(` + [ + { + "path": "../a", + }, + { + "path": "./some/thing", + }, + { + "path": "./another/one", + }, + { + "path": "../../some-path/dir", + }, + ] + `); + }); + + it('should not prune stale project references from projects included in `nx.sync.ignoredReferences`', async () => { + writeJson(tree, 'packages/b/tsconfig.json', { + compilerOptions: { + composite: true, + }, + references: [ + { path: './some/thing' }, + { path: './another/one' }, + // this is not a dependency and it's not git ignored, it would normally be pruned, + // but it's included in `nx.sync.ignoredReferences`, so we don't prune it + { path: '../c' }, + ], + nx: { + sync: { + ignoredReferences: ['../c'], + }, + }, + }); + tree.write('some-path/dir/tsconfig.json', '{}'); + tree.write('.gitignore', 'some-path/dir'); + + await syncGenerator(tree); + + const rootTsconfig = readJson(tree, 'packages/b/tsconfig.json'); + // The dependency reference on "a" is added to the start of the array + expect(rootTsconfig.references).toMatchInlineSnapshot(` + [ + { + "path": "../a", + }, + { + "path": "./some/thing", + }, + { + "path": "./another/one", + }, + { + "path": "../c", + }, + ] + `); + }); + it('should collect transitive dependencies and sync project references to tsconfig.json files', async () => { // c => b => a // d => b => a diff --git a/packages/js/src/generators/typescript-sync/typescript-sync.ts b/packages/js/src/generators/typescript-sync/typescript-sync.ts index 652401a84f34d..2c54033d5a1dc 100644 --- a/packages/js/src/generators/typescript-sync/typescript-sync.ts +++ b/packages/js/src/generators/typescript-sync/typescript-sync.ts @@ -10,6 +10,7 @@ import { type ProjectGraphProjectNode, type Tree, } from '@nx/devkit'; +import ignore from 'ignore'; import { applyEdits, modify } from 'jsonc-parser'; import { dirname, normalize, relative } from 'node:path/posix'; import type { SyncGeneratorResult } from 'nx/src/utils/sync-generators'; @@ -26,6 +27,11 @@ interface Tsconfig { rootDir?: string; outDir?: string; }; + nx?: { + sync?: { + ignoredReferences?: string[]; + }; + }; } const COMMON_RUNTIME_TS_CONFIG_FILE_NAMES = [ @@ -37,6 +43,12 @@ const COMMON_RUNTIME_TS_CONFIG_FILE_NAMES = [ 'tsconfig.runtime.json', ]; +type GeneratorOptions = { + runtimeTsConfigFileNames?: string[]; +}; + +type NormalizedGeneratorOptions = Required; + export async function syncGenerator(tree: Tree): Promise { // Ensure that the plugin has been wired up in nx.json const nxJson = readNxJson(tree); @@ -151,23 +163,27 @@ export async function syncGenerator(tree: Tree): Promise { } } - const runtimeTsConfigFileNames = - (nxJson.sync?.generatorOptions?.['@nx/js:typescript-sync'] - ?.runtimeTsConfigFileNames as string[]) ?? - COMMON_RUNTIME_TS_CONFIG_FILE_NAMES; + const userOptions = nxJson.sync?.generatorOptions?.[ + '@nx/js:typescript-sync' + ] as GeneratorOptions | undefined; + const { runtimeTsConfigFileNames }: NormalizedGeneratorOptions = { + runtimeTsConfigFileNames: + userOptions?.runtimeTsConfigFileNames ?? + COMMON_RUNTIME_TS_CONFIG_FILE_NAMES, + }; const collectedDependencies = new Map(); - for (const [name, data] of Object.entries(projectGraph.dependencies)) { + for (const [projectName, data] of Object.entries(projectGraph.dependencies)) { if ( - !projectGraph.nodes[name] || - projectGraph.nodes[name].data.root === '.' || + !projectGraph.nodes[projectName] || + projectGraph.nodes[projectName].data.root === '.' || !data.length ) { continue; } // Get the source project nodes for the source and target - const sourceProjectNode = projectGraph.nodes[name]; + const sourceProjectNode = projectGraph.nodes[projectName]; // Find the relevant tsconfig file for the source project const sourceProjectTsconfigPath = joinPathFragments( @@ -179,7 +195,7 @@ export async function syncGenerator(tree: Tree): Promise { ) { if (process.env.NX_VERBOSE_LOGGING === 'true') { logger.warn( - `Skipping project "${name}" as there is no tsconfig.json file found in the project root "${sourceProjectNode.data.root}".` + `Skipping project "${projectName}" as there is no tsconfig.json file found in the project root "${sourceProjectNode.data.root}".` ); } continue; @@ -188,7 +204,7 @@ export async function syncGenerator(tree: Tree): Promise { // Collect the dependencies of the source project const dependencies = collectProjectDependencies( tree, - name, + projectName, projectGraph, collectedDependencies ); @@ -299,14 +315,23 @@ function updateTsConfigReferences( tsConfigPath ); const tsConfig = parseJson(stringifiedJsonContents); + const ignoredReferences = new Set(tsConfig.nx?.sync?.ignoredReferences ?? []); // We have at least one dependency so we can safely set it to an empty array if not already set const references = []; const originalReferencesSet = new Set(); const newReferencesSet = new Set(); + for (const ref of tsConfig.references ?? []) { const normalizedPath = normalizeReferencePath(ref.path); originalReferencesSet.add(normalizedPath); + if (ignoredReferences.has(ref.path)) { + // we keep the user-defined ignored references + references.push(ref); + newReferencesSet.add(normalizedPath); + continue; + } + // reference path is relative to the tsconfig file const resolvedRefPath = getTsConfigPathFromReferencePath( tree, @@ -320,9 +345,10 @@ function updateTsConfigReferences( resolvedRefPath, projectRoot, projectRoots - ) + ) || + isProjectReferenceIgnored(tree, resolvedRefPath) ) { - // we keep all references within the current Nx project + // we keep all references within the current Nx project or that are ignored references.push(ref); newReferencesSet.add(normalizedPath); } @@ -511,6 +537,22 @@ function isProjectReferenceWithinNxProject( return true; } +function isProjectReferenceIgnored( + tree: Tree, + refTsConfigPath: string +): boolean { + const ig = ignore(); + if (tree.exists('.gitignore')) { + ig.add('.git'); + ig.add(tree.read('.gitignore', 'utf-8')); + } + if (tree.exists('.nxignore')) { + ig.add(tree.read('.nxignore', 'utf-8')); + } + + return ig.ignores(refTsConfigPath); +} + function getTsConfigDirName( tree: Tree, rawTsconfigContentsCache: Map,