Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(js): keep refs to ignored files and allow opting out of pruning stale refs in typescript sync generator #27636

Merged
merged 2 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
],
});

Expand All @@ -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
Expand Down
66 changes: 54 additions & 12 deletions packages/js/src/generators/typescript-sync/typescript-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +27,11 @@ interface Tsconfig {
rootDir?: string;
outDir?: string;
};
nx?: {
sync?: {
ignoredReferences?: string[];
};
};
}

const COMMON_RUNTIME_TS_CONFIG_FILE_NAMES = [
Expand All @@ -37,6 +43,12 @@ const COMMON_RUNTIME_TS_CONFIG_FILE_NAMES = [
'tsconfig.runtime.json',
];

type GeneratorOptions = {
runtimeTsConfigFileNames?: string[];
};

type NormalizedGeneratorOptions = Required<GeneratorOptions>;

export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
// Ensure that the plugin has been wired up in nx.json
const nxJson = readNxJson(tree);
Expand Down Expand Up @@ -151,23 +163,27 @@ export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
}
}

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<string, ProjectGraphProjectNode[]>();
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(
Expand All @@ -179,7 +195,7 @@ export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
) {
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;
Expand All @@ -188,7 +204,7 @@ export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
// Collect the dependencies of the source project
const dependencies = collectProjectDependencies(
tree,
name,
projectName,
projectGraph,
collectedDependencies
);
Expand Down Expand Up @@ -299,14 +315,23 @@ function updateTsConfigReferences(
tsConfigPath
);
const tsConfig = parseJson<Tsconfig>(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,
Expand All @@ -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);
}
Expand Down Expand Up @@ -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<string, string>,
Expand Down