Compare commits

...

1 Commits

Author SHA1 Message Date
“JamesHenry” 8322864e48 fix(js): allow syncing references under solution configs, regardless of composite 2024-08-21 01:01:11 +04:00
2 changed files with 128 additions and 73 deletions
@@ -69,11 +69,10 @@ describe('syncGenerator()', () => {
plugins: ['@nx/js/typescript'],
});
// Root tsconfigs
// Root tsconfig, must be solution style with empty files array and references array
writeJson(tree, 'tsconfig.json', {
compilerOptions: {
composite: true,
},
files: [],
references: [],
});
writeJson(tree, 'tsconfig.options.json', {
compilerOptions: {
@@ -104,6 +103,41 @@ describe('syncGenerator()', () => {
);
});
it('should error if the root tsconfig.json is not a solution style config', async () => {
const errorSnapshot = `[Error: The workspace root tsconfig.json must be a "solution" style config, with no "files" of its own to check. All files should be referenced indirectly by "references" instead. Set "files": [] and "references": [] at the top level of the config.]`;
// Missing files and references arrays
writeJson(tree, 'tsconfig.json', {});
await expect(syncGenerator(tree)).rejects.toMatchInlineSnapshot(
errorSnapshot
);
// Missing files array
writeJson(tree, 'tsconfig.json', {
references: [],
});
await expect(syncGenerator(tree)).rejects.toMatchInlineSnapshot(
errorSnapshot
);
// files array is not empty
writeJson(tree, 'tsconfig.json', {
files: ['src/index.ts'],
references: [],
});
await expect(syncGenerator(tree)).rejects.toMatchInlineSnapshot(
errorSnapshot
);
// Missing references array
writeJson(tree, 'tsconfig.json', {
files: [],
});
await expect(syncGenerator(tree)).rejects.toMatchInlineSnapshot(
errorSnapshot
);
});
it('should not make changes when references are set regardless their order and/or there are unformatted files', async () => {
// c => b => a
// d => b => a
@@ -203,7 +237,7 @@ describe('syncGenerator()', () => {
describe('root tsconfig.json', () => {
it('should sync project references to the tsconfig.json', async () => {
expect(readJson(tree, 'tsconfig.json').references).toBeUndefined();
expect(readJson(tree, 'tsconfig.json').references).toEqual([]);
await syncGenerator(tree);
@@ -225,6 +259,7 @@ describe('syncGenerator()', () => {
compilerOptions: {
composite: true,
},
files: [],
// Swapped order and additional manual reference
references: [
{ path: './packages/b' },
@@ -266,7 +301,9 @@ describe('syncGenerator()', () => {
"composite": true,
// This is a nested comment
"target": "es5"
}
},
"files": [],
"references": []
}
`
);
@@ -282,31 +319,12 @@ describe('syncGenerator()', () => {
// This is a nested comment
"target": "es5"
},
"files": [],
"references": [{ "path": "./packages/a" }, { "path": "./packages/b" }]
}
"
`);
});
it('should not add a reference if the internally referenced tsconfig.json does not have composite: true', async () => {
// Delete composite from a, causing it to not show up in the final tsconfig.json snapshot
writeJson(tree, 'packages/a/tsconfig.json', {
compilerOptions: {},
});
await syncGenerator(tree);
expect(tree.read('tsconfig.json').toString('utf-8'))
.toMatchInlineSnapshot(`
"{
"compilerOptions": {
"composite": true
},
"references": [{ "path": "./packages/b" }]
}
"
`);
});
});
describe('project level tsconfig.json', () => {
@@ -486,32 +504,33 @@ describe('syncGenerator()', () => {
`);
});
it('should not add a reference if the dependency tsconfig.json does not have composite: true', async () => {
it('should not add a reference if the dependency tsconfig.json does not have composite: true (if the project tsconfig is not a solution config)', async () => {
addProject('foo', ['bar'], ['tsconfig.build.json']);
addProject('bar', [], ['tsconfig.build.json']);
// Delete composite from bar, causing it to not show up in the final tsconfig.json snapshots below
writeJson(tree, 'packages/bar/tsconfig.json', {
compilerOptions: {},
});
const originalFooTsconfig = readJson(tree, 'packages/foo/tsconfig.json');
await syncGenerator(tree);
expect(tree.read('tsconfig.json').toString('utf-8'))
expect(tree.read('packages/foo/tsconfig.json').toString('utf-8'))
.toMatchInlineSnapshot(`
"{
"compilerOptions": {
"composite": true
},
"references": [
{ "path": "./packages/a" },
{ "path": "./packages/b" },
{ "path": "./packages/foo" }
]
"references": [{ "path": "../bar" }]
}
"
`);
// Restore original foo tsconfig.json
writeJson(tree, 'packages/foo/tsconfig.json', originalFooTsconfig);
// Delete composite from bar, causing it to not show up in the final tsconfig.json snapshot below
writeJson(tree, 'packages/bar/tsconfig.json', {
compilerOptions: {},
});
expect(tree.read('packages/foo/tsconfig.json').toString('utf-8'))
.toMatchInlineSnapshot(`
"{
@@ -521,6 +540,23 @@ describe('syncGenerator()', () => {
}
"
`);
// Make foo tsconfig.json a solution style config, allowing bar to be referenced despite not having composite: true
writeJson(tree, 'packages/foo/tsconfig.json', {
files: [],
references: [],
});
await syncGenerator(tree);
expect(tree.read('packages/foo/tsconfig.json').toString('utf-8'))
.toMatchInlineSnapshot(`
"{
"files": [],
"references": [{ "path": "../bar" }]
}
"
`);
});
describe('without custom sync generator options', () => {
@@ -63,14 +63,26 @@ export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
`A "tsconfig.json" file must exist in the workspace root in order to use this sync generator.`
);
}
const rawTsconfigContentsCache = new Map<string, string>();
const stringifiedRootJsonContents = readRawTsconfigContents(
tree,
rawTsconfigContentsCache,
const tsSysFromTree: ts.System = {
...ts.sys,
readFile(path) {
return readRawTsconfigContents(tree, rawTsconfigContentsCache, path);
},
};
const parsedRootTsconfig = parseTsconfigUsingTS(
tsSysFromTree,
rootTsconfigPath
);
const rootTsconfig = parseJson<Tsconfig>(stringifiedRootJsonContents);
if (!isSolutionConfig(parsedRootTsconfig)) {
throw new Error(
`The workspace root tsconfig.json must be a "solution" style config, with no "files" of its own to check. All files should be referenced indirectly by "references" instead. Set "files": [] and "references": [] at the top level of the config.`
);
}
const rootTsconfig = parsedRootTsconfig.raw as Tsconfig;
const projectGraph = await createProjectGraphAsync();
const projectRoots = new Set<string>();
const tsconfigHasCompositeEnabledCache = new Map<string, boolean>();
@@ -90,13 +102,6 @@ export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
}
);
const tsSysFromTree: ts.System = {
...ts.sys,
readFile(path) {
return readRawTsconfigContents(tree, rawTsconfigContentsCache, path);
},
};
// Track if any changes were made to the tsconfig files. We check the changes
// made by this generator to know if the TS config is out of sync with the
// project graph. Therefore, we don't format the files if there were no changes
@@ -130,18 +135,9 @@ export async function syncGenerator(tree: Tree): Promise<SyncGeneratorResult> {
}
if (hasChanges) {
const updatedReferences = Array.from(referencesSet)
// Check composite is true in the internal reference before proceeding
.filter((ref) =>
hasCompositeEnabled(
tsSysFromTree,
tsconfigHasCompositeEnabledCache,
joinPathFragments(ref, 'tsconfig.json')
)
)
.map((ref) => ({
path: `./${ref}`,
}));
const updatedReferences = Array.from(referencesSet).map((ref) => ({
path: `./${ref}`,
}));
patchTsconfigJsonReferences(
tree,
rawTsconfigContentsCache,
@@ -293,12 +289,9 @@ function updateTsConfigReferences(
runtimeTsConfigFileName?: string,
possibleRuntimeTsConfigFileNames?: string[]
): boolean {
const stringifiedJsonContents = readRawTsconfigContents(
tree,
rawTsconfigContentsCache,
tsConfigPath
);
const tsConfig = parseJson<Tsconfig>(stringifiedJsonContents);
const parsedTsconfig = parseTsconfigUsingTS(tsSysFromTree, tsConfigPath);
const tsConfig = parsedTsconfig.raw as Tsconfig;
const isSolutionTsConfig = isSolutionConfig(parsedTsconfig);
// We have at least one dependency so we can safely set it to an empty array if not already set
const references = [];
@@ -338,8 +331,10 @@ function updateTsConfigReferences(
runtimeTsConfigFileName
);
if (tsconfigExists(tree, rawTsconfigContentsCache, runtimeTsConfigPath)) {
// Check composite is true in the dependency runtime tsconfig file before proceeding
// Check composite is true in the dependency runtime tsconfig file before proceeding,
// unless the project being updated is a solution tsconfig
if (
!isSolutionTsConfig &&
!hasCompositeEnabled(
tsSysFromTree,
tsconfigHasCompositeEnabledCache,
@@ -366,7 +361,9 @@ function updateTsConfigReferences(
)
) {
// Check composite is true in the dependency runtime tsconfig file before proceeding
// unless the project being updated is a solution tsconfig
if (
!isSolutionTsConfig &&
!hasCompositeEnabled(
tsSysFromTree,
tsconfigHasCompositeEnabledCache,
@@ -382,7 +379,9 @@ function updateTsConfigReferences(
}
} else {
// Check composite is true in the dependency tsconfig.json file before proceeding
// unless the project being updated is a solution tsconfig
if (
!isSolutionTsConfig &&
!hasCompositeEnabled(
tsSysFromTree,
tsconfigHasCompositeEnabledCache,
@@ -572,13 +571,33 @@ function hasCompositeEnabled(
tsconfigPath: string
): boolean {
if (!tsconfigHasCompositeEnabledCache.has(tsconfigPath)) {
const parsed = ts.parseJsonConfigFileContent(
ts.readConfigFile(tsconfigPath, tsSysFromTree.readFile).config,
tsSysFromTree,
dirname(tsconfigPath)
);
const parsed = parseTsconfigUsingTS(tsSysFromTree, tsconfigPath);
const enabledVal = parsed.options.composite === true;
tsconfigHasCompositeEnabledCache.set(tsconfigPath, enabledVal);
}
return tsconfigHasCompositeEnabledCache.get(tsconfigPath);
}
function parseTsconfigUsingTS(
tsSysFromTree: ts.System,
tsconfigPath: string
): ts.ParsedCommandLine {
return ts.parseJsonConfigFileContent(
ts.readConfigFile(tsconfigPath, tsSysFromTree.readFile).config,
tsSysFromTree,
dirname(tsconfigPath)
);
}
/**
* The TS Team informed us that "solution" configs are currently not well documented, but a first class
* use-case. They are tsconfigs without files of their own, but with references to other tsconfigs.
*
* By design, they can reference by composite and non composite projects.
*/
function isSolutionConfig(config: ts.ParsedCommandLine) {
return (
!config.fileNames.length &&
Object.hasOwnProperty.call(config.raw, 'references')
);
}