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 { Schema } from '../schema';
|
||||||
import { normalizeSchema } from './normalize-schema';
|
import { normalizeSchema } from './normalize-schema';
|
||||||
import { updateImports } from './update-imports';
|
import { updateImports } from './update-imports';
|
||||||
|
import * as tsSolution from '../../../utilities/typescript/ts-solution-setup';
|
||||||
|
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
const { libraryGenerator } = require('@nx/js');
|
const { libraryGenerator } = require('@nx/js');
|
||||||
@ -530,4 +531,88 @@ export MyExtendedClass extends MyClass {};`
|
|||||||
'@proj/my-source/server': ['my-destination/src/server.ts'],
|
'@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,
|
visitNotIgnoredFiles,
|
||||||
writeJson,
|
writeJson,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { relative } from 'path';
|
import { isAbsolute, normalize, relative } from 'path';
|
||||||
import type * as ts from 'typescript';
|
import type * as ts from 'typescript';
|
||||||
import { getImportPath } from '../../../utilities/get-import-path';
|
import { getImportPath } from '../../../utilities/get-import-path';
|
||||||
import {
|
import {
|
||||||
@ -21,6 +21,7 @@ import {
|
|||||||
import { ensureTypescript } from '../../../utilities/typescript';
|
import { ensureTypescript } from '../../../utilities/typescript';
|
||||||
import { NormalizedSchema } from '../schema';
|
import { NormalizedSchema } from '../schema';
|
||||||
import { normalizePathSlashes } from './utils';
|
import { normalizePathSlashes } from './utils';
|
||||||
|
import { isUsingTsSolutionSetup } from '../../../utilities/typescript/ts-solution-setup';
|
||||||
|
|
||||||
let tsModule: typeof import('typescript');
|
let tsModule: typeof import('typescript');
|
||||||
|
|
||||||
@ -41,9 +42,14 @@ export function updateImports(
|
|||||||
const { libsDir } = getWorkspaceLayout(tree);
|
const { libsDir } = getWorkspaceLayout(tree);
|
||||||
const projects = getProjects(tree);
|
const projects = getProjects(tree);
|
||||||
|
|
||||||
|
const isUsingTsSolution = isUsingTsSolutionSetup(tree);
|
||||||
|
|
||||||
// use the source root to find the from location
|
// use the source root to find the from location
|
||||||
// this attempts to account for libs that have been created with --importPath
|
// 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 tsConfig: any;
|
||||||
let mainEntryPointImportPath: string;
|
let mainEntryPointImportPath: string;
|
||||||
let secondaryEntryPointImportPaths: string[];
|
let secondaryEntryPointImportPaths: string[];
|
||||||
@ -149,6 +155,65 @@ export function updateImports(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (tsConfig) {
|
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[];
|
const path = tsConfig.compilerOptions.paths[projectRef.from] as string[];
|
||||||
if (!path) {
|
if (!path) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -172,10 +237,6 @@ export function updateImports(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJson(tree, tsConfigPath, tsConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureTrailingSlash(path: string): string {
|
function ensureTrailingSlash(path: string): string {
|
||||||
return path.endsWith('/') ? path : `${path}/`;
|
return path.endsWith('/') ? path : `${path}/`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
import { Schema } from '../schema';
|
import { Schema } from '../schema';
|
||||||
import { updateTsconfig } from './update-tsconfig';
|
import { updateTsconfig } from './update-tsconfig';
|
||||||
|
import * as tsSolution from '../../../utilities/typescript/ts-solution-setup';
|
||||||
|
|
||||||
// nx-ignore-next-line
|
// nx-ignore-next-line
|
||||||
const { libraryGenerator } = require('@nx/js');
|
const { libraryGenerator } = require('@nx/js');
|
||||||
@ -212,4 +213,44 @@ describe('updateTsconfig', () => {
|
|||||||
'@proj/nested/whatever-name': ['libs/my-lib/nested-lib/src/index.ts'],
|
'@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,
|
createProjectGraphAsync,
|
||||||
normalizePath,
|
normalizePath,
|
||||||
ProjectGraph,
|
ProjectGraph,
|
||||||
|
readProjectsConfigurationFromProjectGraph,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
updateJson,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
@ -11,6 +12,8 @@ import {
|
|||||||
createProjectRootMappings,
|
createProjectRootMappings,
|
||||||
findProjectForPath,
|
findProjectForPath,
|
||||||
} from 'nx/src/project-graph/utils/find-project-for-path';
|
} 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.
|
* Updates the tsconfig paths to remove the project.
|
||||||
@ -18,12 +21,27 @@ import {
|
|||||||
* @param schema The options provided to the schematic
|
* @param schema The options provided to the schematic
|
||||||
*/
|
*/
|
||||||
export async function updateTsconfig(tree: Tree, schema: Schema) {
|
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)) {
|
if (tree.exists(tsConfigPath)) {
|
||||||
const graph: ProjectGraph = await createProjectGraphAsync();
|
const graph: ProjectGraph = await createProjectGraphAsync();
|
||||||
const projectMapping = createProjectRootMappings(graph.nodes);
|
const projectMapping = createProjectRootMappings(graph.nodes);
|
||||||
updateJson(tree, tsConfigPath, (json) => {
|
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 importPath in json.compilerOptions.paths) {
|
||||||
for (const path of json.compilerOptions.paths[importPath]) {
|
for (const path of json.compilerOptions.paths[importPath]) {
|
||||||
const project = findProjectForPath(
|
const project = findProjectForPath(
|
||||||
@ -36,6 +54,7 @@ export async function updateTsconfig(tree: Tree, schema: Schema) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return json;
|
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