fix(core): Update move/remove workspace generators to work with ts project references (#29331)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> When a workspace is setup to use ts project references the move/remove generators from `@nx/workspace` do not work correctly or result in an incorrect state for the workspace. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> This should work OOTB. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
0888977c13
commit
0329cad1d1
@ -10,6 +10,7 @@ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { Schema } from '../schema';
|
||||
import { normalizeSchema } from './normalize-schema';
|
||||
import { updateImports } from './update-imports';
|
||||
import * as tsSolution from '../../../utilities/typescript/ts-solution-setup';
|
||||
|
||||
// nx-ignore-next-line
|
||||
const { libraryGenerator } = require('@nx/js');
|
||||
@ -530,4 +531,88 @@ export MyExtendedClass extends MyClass {};`
|
||||
'@proj/my-source/server': ['my-destination/src/server.ts'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypeScript project references', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(tsSolution, 'isUsingTsSolutionSetup').mockReturnValue(true);
|
||||
const tsconfigContent = {
|
||||
extends: './tsconfig.base.json',
|
||||
...readJson(tree, 'tsconfig.base.json'),
|
||||
};
|
||||
tree.write('tsconfig.json', JSON.stringify(tsconfigContent, null, 2));
|
||||
|
||||
const packageJson = readJson(tree, 'package.json');
|
||||
packageJson.workspaces = ['packages/**'];
|
||||
tree.write('package.json', JSON.stringify(packageJson, null, 2));
|
||||
});
|
||||
it('should work with updateImportPath=false', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'packages/my-source',
|
||||
});
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-source');
|
||||
|
||||
const tsconfigJson = readJson(tree, 'tsconfig.json');
|
||||
tsconfigJson.references = [{ path: './packages/my-source' }];
|
||||
tree.write('tsconfig.json', JSON.stringify(tsconfigJson, null, 2));
|
||||
|
||||
updateImports(
|
||||
tree,
|
||||
await normalizeSchema(
|
||||
tree,
|
||||
{
|
||||
...schema,
|
||||
updateImportPath: false,
|
||||
},
|
||||
projectConfig
|
||||
),
|
||||
|
||||
projectConfig
|
||||
);
|
||||
|
||||
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"path": "./packages/my-source",
|
||||
},
|
||||
{
|
||||
"path": "./my-destination",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should work with updateImportPath=true', async () => {
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'packages/my-source',
|
||||
});
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, 'my-source');
|
||||
|
||||
const tsconfigJson = readJson(tree, 'tsconfig.json');
|
||||
tsconfigJson.references = [{ path: './packages/my-source' }];
|
||||
tree.write('tsconfig.json', JSON.stringify(tsconfigJson, null, 2));
|
||||
|
||||
updateImports(
|
||||
tree,
|
||||
await normalizeSchema(
|
||||
tree,
|
||||
{
|
||||
...schema,
|
||||
},
|
||||
projectConfig
|
||||
),
|
||||
|
||||
projectConfig
|
||||
);
|
||||
|
||||
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"path": "./my-destination",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
visitNotIgnoredFiles,
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
import { relative } from 'path';
|
||||
import { isAbsolute, normalize, relative } from 'path';
|
||||
import type * as ts from 'typescript';
|
||||
import { getImportPath } from '../../../utilities/get-import-path';
|
||||
import {
|
||||
@ -21,6 +21,7 @@ import {
|
||||
import { ensureTypescript } from '../../../utilities/typescript';
|
||||
import { NormalizedSchema } from '../schema';
|
||||
import { normalizePathSlashes } from './utils';
|
||||
import { isUsingTsSolutionSetup } from '../../../utilities/typescript/ts-solution-setup';
|
||||
|
||||
let tsModule: typeof import('typescript');
|
||||
|
||||
@ -41,9 +42,14 @@ export function updateImports(
|
||||
const { libsDir } = getWorkspaceLayout(tree);
|
||||
const projects = getProjects(tree);
|
||||
|
||||
const isUsingTsSolution = isUsingTsSolutionSetup(tree);
|
||||
|
||||
// use the source root to find the from location
|
||||
// this attempts to account for libs that have been created with --importPath
|
||||
const tsConfigPath = getRootTsConfigPathInTree(tree);
|
||||
const tsConfigPath = isUsingTsSolution
|
||||
? 'tsconfig.json'
|
||||
: getRootTsConfigPathInTree(tree);
|
||||
// If we are using a ts solution setup, we need to use tsconfig.json instead of tsconfig.base.json
|
||||
let tsConfig: any;
|
||||
let mainEntryPointImportPath: string;
|
||||
let secondaryEntryPointImportPaths: string[];
|
||||
@ -149,6 +155,65 @@ export function updateImports(
|
||||
};
|
||||
|
||||
if (tsConfig) {
|
||||
if (!isUsingTsSolution) {
|
||||
updateTsConfigPaths(
|
||||
tsConfig,
|
||||
projectRef,
|
||||
tsConfigPath,
|
||||
projectRoot,
|
||||
schema
|
||||
);
|
||||
} else {
|
||||
updateTsConfigReferences(tsConfig, projectRoot, tsConfigPath, schema);
|
||||
}
|
||||
writeJson(tree, tsConfigPath, tsConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTsConfigReferences(
|
||||
tsConfig: any,
|
||||
projectRoot: { from: string; to: string },
|
||||
tsConfigPath: string,
|
||||
schema: NormalizedSchema
|
||||
) {
|
||||
// Since paths can be './path' or 'path' we check if both are the same relative path to the workspace root
|
||||
const projectRefIndex = tsConfig.references.findIndex(
|
||||
(ref) => relative(ref.path, projectRoot.from) === ''
|
||||
);
|
||||
if (projectRefIndex === -1) {
|
||||
throw new Error(
|
||||
`unable to find "${projectRoot.from}" in ${tsConfigPath} references`
|
||||
);
|
||||
}
|
||||
const updatedPath = joinPathFragments(
|
||||
projectRoot.to,
|
||||
relative(projectRoot.from, tsConfig.references[projectRefIndex].path)
|
||||
);
|
||||
|
||||
let normalizedPath = normalize(updatedPath);
|
||||
if (
|
||||
!normalizedPath.startsWith('.') &&
|
||||
!normalizedPath.startsWith('../') &&
|
||||
!isAbsolute(normalizedPath)
|
||||
) {
|
||||
normalizedPath = `./${normalizedPath}`;
|
||||
}
|
||||
|
||||
if (schema.updateImportPath && projectRoot.to) {
|
||||
tsConfig.references[projectRefIndex].path = normalizedPath;
|
||||
} else {
|
||||
tsConfig.references.push({ path: normalizedPath });
|
||||
}
|
||||
}
|
||||
|
||||
function updateTsConfigPaths(
|
||||
tsConfig: any,
|
||||
projectRef: { from: string; to: string },
|
||||
tsConfigPath: string,
|
||||
projectRoot: { from: string; to: string },
|
||||
schema: NormalizedSchema
|
||||
) {
|
||||
const path = tsConfig.compilerOptions.paths[projectRef.from] as string[];
|
||||
if (!path) {
|
||||
throw new Error(
|
||||
@ -172,10 +237,6 @@ export function updateImports(
|
||||
}
|
||||
}
|
||||
|
||||
writeJson(tree, tsConfigPath, tsConfig);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureTrailingSlash(path: string): string {
|
||||
return path.endsWith('/') ? path : `${path}/`;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { Schema } from '../schema';
|
||||
import { updateTsconfig } from './update-tsconfig';
|
||||
import * as tsSolution from '../../../utilities/typescript/ts-solution-setup';
|
||||
|
||||
// nx-ignore-next-line
|
||||
const { libraryGenerator } = require('@nx/js');
|
||||
@ -212,4 +213,44 @@ describe('updateTsconfig', () => {
|
||||
'@proj/nested/whatever-name': ['libs/my-lib/nested-lib/src/index.ts'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with tsSolution setup', async () => {
|
||||
jest.spyOn(tsSolution, 'isUsingTsSolutionSetup').mockReturnValue(true);
|
||||
|
||||
await libraryGenerator(tree, {
|
||||
directory: 'my-lib',
|
||||
});
|
||||
|
||||
const tsconfigContent = {
|
||||
extends: './tsconfig.base.json',
|
||||
compilerOptions: {},
|
||||
files: [],
|
||||
include: [],
|
||||
references: [
|
||||
{
|
||||
path: './my-lib',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
tree.write('tsconfig.json', JSON.stringify(tsconfigContent, null, 2));
|
||||
|
||||
graph = {
|
||||
nodes: {
|
||||
'my-lib': {
|
||||
name: 'my-lib',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: readProjectConfiguration(tree, 'my-lib').root,
|
||||
} as any,
|
||||
},
|
||||
},
|
||||
dependencies: {},
|
||||
};
|
||||
|
||||
await updateTsconfig(tree, schema);
|
||||
|
||||
const tsConfig = readJson(tree, 'tsconfig.json');
|
||||
expect(tsConfig.references).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
createProjectGraphAsync,
|
||||
normalizePath,
|
||||
ProjectGraph,
|
||||
readProjectsConfigurationFromProjectGraph,
|
||||
Tree,
|
||||
updateJson,
|
||||
} from '@nx/devkit';
|
||||
@ -11,6 +12,8 @@ import {
|
||||
createProjectRootMappings,
|
||||
findProjectForPath,
|
||||
} from 'nx/src/project-graph/utils/find-project-for-path';
|
||||
import { isUsingTsSolutionSetup } from '../../../utilities/typescript/ts-solution-setup';
|
||||
import { relative } from 'path';
|
||||
|
||||
/**
|
||||
* Updates the tsconfig paths to remove the project.
|
||||
@ -18,12 +21,27 @@ import {
|
||||
* @param schema The options provided to the schematic
|
||||
*/
|
||||
export async function updateTsconfig(tree: Tree, schema: Schema) {
|
||||
const tsConfigPath = getRootTsConfigPathInTree(tree);
|
||||
const isUsingTsSolution = isUsingTsSolutionSetup(tree);
|
||||
const tsConfigPath = isUsingTsSolution
|
||||
? 'tsconfig.json'
|
||||
: getRootTsConfigPathInTree(tree);
|
||||
|
||||
if (tree.exists(tsConfigPath)) {
|
||||
const graph: ProjectGraph = await createProjectGraphAsync();
|
||||
const projectMapping = createProjectRootMappings(graph.nodes);
|
||||
updateJson(tree, tsConfigPath, (json) => {
|
||||
if (isUsingTsSolution) {
|
||||
const projectConfigs = readProjectsConfigurationFromProjectGraph(graph);
|
||||
const project = projectConfigs.projects[schema.projectName];
|
||||
if (!project) {
|
||||
throw new Error(
|
||||
`Could not find project '${schema.project}'. Please choose a project that exists in the Nx Workspace.`
|
||||
);
|
||||
}
|
||||
json.references = json.references.filter(
|
||||
(ref) => relative(ref.path, project.root) !== ''
|
||||
);
|
||||
} else {
|
||||
for (const importPath in json.compilerOptions.paths) {
|
||||
for (const path of json.compilerOptions.paths[importPath]) {
|
||||
const project = findProjectForPath(
|
||||
@ -36,6 +54,7 @@ export async function updateTsconfig(tree: Tree, schema: Schema) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return json;
|
||||
});
|
||||
}
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
import {
|
||||
detectPackageManager,
|
||||
readJson,
|
||||
type Tree,
|
||||
workspaceRoot,
|
||||
} from '@nx/devkit';
|
||||
import { FsTree } from 'nx/src/generators/tree';
|
||||
import { type PackageJson } from 'nx/src/utils/package-json';
|
||||
|
||||
function isUsingPackageManagerWorkspaces(tree: Tree): boolean {
|
||||
return isWorkspacesEnabled(tree);
|
||||
}
|
||||
|
||||
function isWorkspacesEnabled(tree: Tree): boolean {
|
||||
const packageManager = detectPackageManager(tree.root);
|
||||
if (packageManager === 'pnpm') {
|
||||
return tree.exists('pnpm-workspace.yaml');
|
||||
}
|
||||
|
||||
// yarn and npm both use the same 'workspaces' property in package.json
|
||||
if (tree.exists('package.json')) {
|
||||
const packageJson = readJson<PackageJson>(tree, 'package.json');
|
||||
return !!packageJson?.workspaces;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isWorkspaceSetupWithTsSolution(tree: Tree): boolean {
|
||||
if (!tree.exists('tsconfig.base.json') || !tree.exists('tsconfig.json')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tsconfigJson = readJson(tree, 'tsconfig.json');
|
||||
if (tsconfigJson.extends !== './tsconfig.base.json') {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* New setup:
|
||||
* - `files` is defined and set to an empty array
|
||||
* - `references` is defined and set to an empty array
|
||||
* - `include` is not defined or is set to an empty array
|
||||
*/
|
||||
if (
|
||||
!tsconfigJson.files ||
|
||||
tsconfigJson.files.length > 0 ||
|
||||
!tsconfigJson.references ||
|
||||
!!tsconfigJson.include?.length
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const baseTsconfigJson = readJson(tree, 'tsconfig.base.json');
|
||||
if (
|
||||
!baseTsconfigJson.compilerOptions ||
|
||||
!baseTsconfigJson.compilerOptions.composite ||
|
||||
!baseTsconfigJson.compilerOptions.declaration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { compilerOptions, ...rest } = baseTsconfigJson;
|
||||
if (Object.keys(rest).length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isUsingTsSolutionSetup(tree?: Tree): boolean {
|
||||
tree ??= new FsTree(workspaceRoot, false);
|
||||
|
||||
return (
|
||||
isUsingPackageManagerWorkspaces(tree) &&
|
||||
isWorkspaceSetupWithTsSolution(tree)
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user