fix(misc): fix misc issues with move generators (#17814)

This commit is contained in:
Leosvel Pérez Espinosa 2023-06-28 15:26:45 +01:00 committed by GitHub
parent c0508e38e3
commit 7055c724dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 362 additions and 109 deletions

View File

@ -0,0 +1,4 @@
export * from './normalize-schema';
export * from './update-module-name';
export * from './update-ng-package';
export * from './update-secondary-entry-points';

View File

@ -0,0 +1,15 @@
import type { Tree } from '@nx/devkit';
import { readProjectConfiguration } from '@nx/devkit';
import type { NormalizedSchema, Schema } from '../schema';
import { getNewProjectName } from '../../utils/get-new-project-name';
export function normalizeSchema(tree: Tree, schema: Schema): NormalizedSchema {
const newProjectName = getNewProjectName(schema.destination);
const { root } = readProjectConfiguration(tree, schema.projectName);
return {
...schema,
newProjectName,
oldProjectRoot: root,
};
}

View File

@ -4,7 +4,7 @@ import { Linter } from '@nx/linter';
import { moveGenerator } from '@nx/workspace/generators';
import { UnitTestRunner } from '../../../utils/test-runners';
import { generateTestLibrary } from '../../utils/testing';
import { Schema } from '../schema';
import { NormalizedSchema } from '../schema';
import { updateModuleName } from './update-module-name';
describe('updateModuleName Rule', () => {
@ -20,17 +20,19 @@ describe('updateModuleName Rule', () => {
name: 'my-first',
simpleName: true,
});
const schema: Schema = {
const schema: NormalizedSchema = {
projectName: 'my-first',
destination: 'my/first',
updateImportPath: true,
newProjectName: 'my-first',
oldProjectRoot: 'libs/my-first',
};
await moveGenerator(tree, schema);
updateModuleName(tree, { ...schema, destination: 'my/first' });
expect(tree.exists(updatedModulePath)).toBe(true);
const moduleFile = tree.read(updatedModulePath).toString('utf-8');
const moduleFile = tree.read(updatedModulePath, 'utf-8');
expect(moduleFile).toContain(`export class MyFirstModule {}`);
});
@ -42,10 +44,12 @@ describe('updateModuleName Rule', () => {
const indexPath = '/libs/shared/my-first/src/index.ts';
const secondModulePath = '/libs/my-second/src/lib/my-second.module.ts';
const schema: Schema = {
const schema: NormalizedSchema = {
projectName: 'my-first',
destination: 'shared/my-first',
updateImportPath: true,
newProjectName: 'shared-my-first',
oldProjectRoot: 'libs/my-first',
};
beforeEach(async () => {
@ -111,10 +115,10 @@ describe('updateModuleName Rule', () => {
expect(tree.exists(updatedModulePath)).toBe(true);
expect(tree.exists(updatedModuleSpecPath)).toBe(true);
const moduleFile = tree.read(updatedModulePath).toString('utf-8');
const moduleFile = tree.read(updatedModulePath, 'utf-8');
expect(moduleFile).toContain(`export class SharedMyFirstModule {}`);
const moduleSpecFile = tree.read(updatedModuleSpecPath).toString('utf-8');
const moduleSpecFile = tree.read(updatedModuleSpecPath, 'utf-8');
expect(moduleSpecFile).toContain(
`import { SharedMyFirstModule } from './shared-my-first.module';`
);
@ -130,7 +134,7 @@ describe('updateModuleName Rule', () => {
it('should update any references to the module', async () => {
updateModuleName(tree, schema);
const importerFile = tree.read(secondModulePath).toString('utf-8');
const importerFile = tree.read(secondModulePath, 'utf-8');
expect(importerFile).toContain(
`import { SharedMyFirstModule } from '@proj/shared/my-first';`
);
@ -142,7 +146,7 @@ describe('updateModuleName Rule', () => {
it('should update the index.ts file which exports the module', async () => {
updateModuleName(tree, schema);
const indexFile = tree.read(indexPath).toString('utf-8');
const indexFile = tree.read(indexPath, 'utf-8');
expect(indexFile).toContain(
`export * from './lib/shared-my-first.module';`
);
@ -150,10 +154,12 @@ describe('updateModuleName Rule', () => {
});
describe('rename', () => {
const schema: Schema = {
const schema: NormalizedSchema = {
projectName: 'my-source',
destination: 'my-destination',
updateImportPath: true,
newProjectName: 'my-destination',
oldProjectRoot: 'libs/my-source',
};
const modulePath = '/libs/my-destination/src/lib/my-destination.module.ts';
@ -233,10 +239,10 @@ describe('updateModuleName Rule', () => {
expect(tree.exists(modulePath)).toBe(true);
expect(tree.exists(moduleSpecPath)).toBe(true);
const moduleFile = tree.read(modulePath).toString('utf-8');
const moduleFile = tree.read(modulePath, 'utf-8');
expect(moduleFile).toContain(`export class MyDestinationModule {}`);
const moduleSpecFile = tree.read(moduleSpecPath).toString('utf-8');
const moduleSpecFile = tree.read(moduleSpecPath, 'utf-8');
expect(moduleSpecFile).toContain(
`import { MyDestinationModule } from './my-destination.module';`
);
@ -252,7 +258,7 @@ describe('updateModuleName Rule', () => {
it('should update any references to the module', async () => {
updateModuleName(tree, schema);
const importerFile = tree.read(importerPath).toString('utf-8');
const importerFile = tree.read(importerPath, 'utf-8');
expect(importerFile).toContain(
`import { MyDestinationModule } from '@proj/my-destination';`
);
@ -264,10 +270,32 @@ describe('updateModuleName Rule', () => {
it('should update the index.ts file which exports the module', async () => {
updateModuleName(tree, schema);
const indexFile = tree.read(indexPath).toString('utf-8');
const indexFile = tree.read(indexPath, 'utf-8');
expect(indexFile).toContain(
`export * from './lib/my-destination.module';`
);
});
it('should not rename unrelated symbols with similar name in different projects', async () => {
// create different project whose main module name starts with the same
// name of the project we're moving
await generateTestLibrary(tree, {
name: 'my-source-demo',
buildable: false,
linter: Linter.EsLint,
publishable: false,
simpleName: true,
skipFormat: false,
unitTestRunner: UnitTestRunner.Jest,
});
updateModuleName(tree, schema);
const moduleFile = tree.read(
'/libs/my-source-demo/src/lib/my-source-demo.module.ts',
'utf-8'
);
expect(moduleFile).toContain(`export class MySourceDemoModule {}`);
});
});
});

View File

@ -1,13 +1,12 @@
import {
getProjects,
joinPathFragments,
names,
readProjectConfiguration,
Tree,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { getNewProjectName } from '../../utils/get-new-project-name';
import { join } from 'path';
import { Schema } from '../schema';
import type { NormalizedSchema } from '../schema';
/**
* Updates the Angular module name (including the spec file and index.ts)
@ -19,10 +18,8 @@ import { Schema } from '../schema';
*/
export function updateModuleName(
tree: Tree,
{ projectName, destination }: Schema
{ projectName: oldProjectName, newProjectName }: NormalizedSchema
): void {
const newProjectName = getNewProjectName(destination);
const project = readProjectConfiguration(tree, newProjectName);
if (project.projectType === 'application') {
@ -32,14 +29,14 @@ export function updateModuleName(
}
const moduleName = {
from: names(projectName).className,
to: names(newProjectName).className,
from: `${names(oldProjectName).className}Module`,
to: `${names(newProjectName).className}Module`,
};
const findModuleName = new RegExp(`\\b${moduleName.from}`, 'g');
const moduleFile = {
from: `${projectName}.module`,
from: `${oldProjectName}.module`,
to: `${newProjectName}.module`,
};
@ -81,7 +78,7 @@ export function updateModuleName(
});
// update index file
const indexFile = join(project.sourceRoot, 'index.ts');
const indexFile = joinPathFragments(project.sourceRoot, 'index.ts');
if (tree.exists(indexFile)) {
updateFileContent(tree, replacements, indexFile);
}

View File

@ -6,13 +6,11 @@ import {
updateJson,
workspaceRoot,
} from '@nx/devkit';
import { getNewProjectName } from '../../utils/get-new-project-name';
import { join, relative } from 'path';
import { Schema } from '../schema';
import type { NormalizedSchema } from '../schema';
export function updateNgPackage(tree: Tree, schema: Schema): void {
const newProjectName = getNewProjectName(schema.destination);
const project = readProjectConfiguration(tree, newProjectName);
export function updateNgPackage(tree: Tree, schema: NormalizedSchema): void {
const project = readProjectConfiguration(tree, schema.newProjectName);
if (project.projectType === 'application') {
return;
@ -29,13 +27,13 @@ export function updateNgPackage(tree: Tree, schema: Schema): void {
const outputs = getOutputsForTargetAndConfiguration(
{
target: {
project: newProjectName,
project: schema.newProjectName,
target: 'build',
},
overrides: {},
},
{
name: newProjectName,
name: schema.newProjectName,
type: 'lib',
data: {
root: project.root,

View File

@ -0,0 +1,71 @@
import type { Tree } from '@nx/devkit';
import {
joinPathFragments,
normalizePath,
readProjectConfiguration,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { basename, dirname } from 'path';
import type { NormalizedSchema } from '../schema';
const libraryExecutors = [
'@angular-devkit/build-angular:ng-packagr',
'@nx/angular:ng-packagr-lite',
'@nx/angular:package',
// TODO(v17): remove when @nrwl/* scope is removed
'@nrwl/angular:ng-packagr-lite',
'@nrwl/angular:package',
];
export function updateSecondaryEntryPoints(
tree: Tree,
schema: NormalizedSchema
): void {
const project = readProjectConfiguration(tree, schema.newProjectName);
if (project.projectType !== 'library') {
return;
}
if (
!Object.values(project.targets ?? {}).some((target) =>
libraryExecutors.includes(target.executor)
)
) {
return;
}
visitNotIgnoredFiles(tree, project.root, (filePath) => {
if (
basename(filePath) !== 'ng-package.json' ||
normalizePath(filePath) ===
joinPathFragments(project.root, 'ng-package.json')
) {
return;
}
updateReadme(
tree,
dirname(filePath),
schema.projectName,
schema.newProjectName
);
});
}
function updateReadme(
tree: Tree,
dir: string,
oldProjectName: string,
newProjectName: string
) {
const readmePath = joinPathFragments(dir, 'README.md');
if (!tree.exists(readmePath)) {
return;
}
const findName = new RegExp(`${oldProjectName}`, 'g');
const oldContent = tree.read(readmePath, 'utf-8');
const newContent = oldContent.replace(findName, newProjectName);
tree.write(readmePath, newContent);
}

View File

@ -3,6 +3,7 @@ import { readJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/linter';
import { UnitTestRunner } from '../../utils/test-runners';
import librarySecondaryEntryPointGenerator from '../library-secondary-entry-point/library-secondary-entry-point';
import { generateTestLibrary } from '../utils/testing';
import { angularMoveGenerator } from './move';
@ -50,6 +51,28 @@ describe('@nx/angular:move', () => {
expect(ngPackageJson.dest).toEqual('../../dist/libs/mynewlib2');
});
it('should update secondary entry points readme file', async () => {
await generateTestLibrary(tree, { name: 'mylib2', buildable: true });
await librarySecondaryEntryPointGenerator(tree, {
library: 'mylib2',
name: 'testing',
});
await angularMoveGenerator(tree, {
projectName: 'mylib2',
destination: 'mynewlib2',
updateImportPath: true,
});
const readme = tree.read('libs/mynewlib2/testing/README.md', 'utf-8');
expect(readme).toMatchInlineSnapshot(`
"# @proj/mynewlib2/testing
Secondary entry point of \`@proj/mynewlib2\`. It can be used by importing from \`@proj/mynewlib2/testing\`.
"
`);
});
it('should format files', async () => {
jest.spyOn(devkit, 'formatFiles');

View File

@ -1,8 +1,12 @@
import { formatFiles, Tree } from '@nx/devkit';
import { moveGenerator } from '@nx/workspace/generators';
import { updateModuleName } from './lib/update-module-name';
import { updateNgPackage } from './lib/update-ng-package';
import { Schema } from './schema';
import {
normalizeSchema,
updateModuleName,
updateNgPackage,
updateSecondaryEntryPoints,
} from './lib';
import type { Schema } from './schema';
/**
* Moves an Angular lib/app to another folder (and renames it in the process)
@ -15,11 +19,14 @@ export async function angularMoveGenerator(
tree: Tree,
schema: Schema
): Promise<void> {
await moveGenerator(tree, { ...schema, skipFormat: true });
updateModuleName(tree, schema);
updateNgPackage(tree, schema);
const normalizedSchema = normalizeSchema(tree, schema);
if (!schema.skipFormat) {
await moveGenerator(tree, { ...schema, skipFormat: true });
updateModuleName(tree, normalizedSchema);
updateNgPackage(tree, normalizedSchema);
updateSecondaryEntryPoints(tree, normalizedSchema);
if (!normalizedSchema.skipFormat) {
await formatFiles(tree);
}
}

View File

@ -5,3 +5,8 @@ export interface Schema {
importPath?: string;
skipFormat?: boolean;
}
export interface NormalizedSchema extends Schema {
oldProjectRoot: string;
newProjectName: string;
}

View File

@ -1,8 +1,15 @@
import { normalizePath } from '@nx/devkit';
/**
* Replaces slashes with dashes
* Joins path segments replacing slashes with dashes
*
* @param path
*/
export function getNewProjectName(path: string): string {
return path.replace(/\//g, '-');
// strip leading '/' or './' or '../' and trailing '/' and replaces '/' with '-'
return normalizePath(path)
.replace(/(^\.{0,2}\/|\.{1,2}\/|\/$)/g, '')
.split('/')
.filter((x) => !!x)
.join('-');
}

View File

@ -46,6 +46,25 @@ describe('normalizeSchema', () => {
expect(result).toEqual(expected);
});
it('should normalize destination and derive projectName correctly', () => {
const expected: NormalizedSchema = {
destination: 'my/library',
importPath: '@proj/my/library',
newProjectName: 'my-library',
projectName: 'my-library',
relativeToRootDestination: 'libs/my/library',
updateImportPath: true,
};
const result = normalizeSchema(
tree,
{ ...schema, destination: './my/library' },
projectConfiguration
);
expect(result).toEqual(expected);
});
it('should use provided import path', () => {
const expected: NormalizedSchema = {
destination: 'my/library',

View File

@ -1,6 +1,10 @@
import { ProjectConfiguration, Tree } from '@nx/devkit';
import type { NormalizedSchema, Schema } from '../schema';
import { getDestination, getNewProjectName, normalizeSlashes } from './utils';
import {
getDestination,
getNewProjectName,
normalizePathSlashes,
} from './utils';
import { getImportPath } from '../../../utilities/get-import-path';
export function normalizeSchema(
@ -8,9 +12,7 @@ export function normalizeSchema(
schema: Schema,
projectConfiguration: ProjectConfiguration
): NormalizedSchema {
const destination = schema.destination.startsWith('/')
? normalizeSlashes(schema.destination.slice(1))
: schema.destination;
const destination = normalizePathSlashes(schema.destination);
const newProjectName =
schema.newProjectName ?? getNewProjectName(destination);
@ -18,7 +20,8 @@ export function normalizeSchema(
...schema,
destination,
importPath:
schema.importPath ?? normalizeSlashes(getImportPath(tree, destination)),
schema.importPath ??
normalizePathSlashes(getImportPath(tree, destination)),
newProjectName,
relativeToRootDestination: getDestination(
tree,

View File

@ -1,4 +1,9 @@
import { readJson, readProjectConfiguration, Tree } from '@nx/devkit';
import {
readJson,
readProjectConfiguration,
Tree,
updateJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Schema } from '../schema';
import { updateImports } from './update-imports';
@ -292,6 +297,37 @@ export MyExtendedClass extends MyClass {};`
});
});
it('should update project ref in the root tsconfig.base.json for secondary entry points', async () => {
await libraryGenerator(tree, {
name: 'my-source',
});
updateJson(tree, '/tsconfig.base.json', (json) => {
json.compilerOptions.paths['@proj/my-source/testing'] = [
'libs/my-source/testing/src/index.ts',
];
json.compilerOptions.paths['@proj/different-alias'] = [
'libs/my-source/some-path/src/index.ts',
];
return json;
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
updateImports(
tree,
normalizeSchema(tree, schema, projectConfig),
projectConfig
);
const tsConfig = readJson(tree, '/tsconfig.base.json');
expect(tsConfig.compilerOptions.paths).toEqual({
'@proj/my-destination': ['libs/my-destination/src/index.ts'],
'@proj/my-destination/testing': [
'libs/my-destination/testing/src/index.ts',
],
'@proj/different-alias': ['libs/my-destination/some-path/src/index.ts'],
});
});
it('should update project ref of a project not under libs in the root tsconfig.base.json', async () => {
tree.delete('libs');
await libraryGenerator(tree, {

View File

@ -17,7 +17,7 @@ import {
findNodes,
} from '../../../utilities/ts-config';
import { NormalizedSchema } from '../schema';
import { normalizeSlashes } from './utils';
import { normalizePathSlashes } from './utils';
import { relative } from 'path';
import { ensureTypescript } from '../../../utilities/typescript';
import { getImportPath } from '../../../utilities/get-import-path';
@ -39,88 +39,119 @@ export function updateImports(
return;
}
const { npmScope, libsDir } = getWorkspaceLayout(tree);
const { libsDir } = getWorkspaceLayout(tree);
const projects = getProjects(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);
let tsConfig: any;
let fromPath: string;
let mainEntryPointImportPath: string;
let secondaryEntryPointImportPaths: string[];
if (tree.exists(tsConfigPath)) {
tsConfig = readJson(tree, tsConfigPath);
const sourceRoot =
project.sourceRoot ?? joinPathFragments(project.root, 'src');
fromPath = Object.keys(tsConfig.compilerOptions.paths).find((path) =>
mainEntryPointImportPath = Object.keys(
tsConfig.compilerOptions?.paths ?? {}
).find((path) =>
tsConfig.compilerOptions.paths[path].some((x) =>
x.startsWith(project.sourceRoot)
x.startsWith(ensureTrailingSlash(sourceRoot))
)
);
secondaryEntryPointImportPaths = Object.keys(
tsConfig.compilerOptions?.paths ?? {}
).filter((path) =>
tsConfig.compilerOptions.paths[path].some(
(x) =>
x.startsWith(ensureTrailingSlash(project.root)) &&
!x.startsWith(ensureTrailingSlash(sourceRoot))
)
);
}
const projectRef = {
from:
fromPath ||
normalizeSlashes(
getImportPath(
tree,
project.root.slice(libsDir.length).replace(/^\/|\\/, '')
)
),
to: schema.importPath,
};
mainEntryPointImportPath ??= normalizePathSlashes(
getImportPath(
tree,
project.root.slice(libsDir.length).replace(/^\/|\\/, '')
)
);
if (schema.updateImportPath) {
const replaceProjectRef = new RegExp(projectRef.from, 'g');
const projectRefs = [
{
from: mainEntryPointImportPath,
to: schema.importPath,
},
...secondaryEntryPointImportPaths.map((p) => ({
from: p,
// if the import path doesn't start with the main entry point import path,
// it's a custom import path we don't know how to update the name, we keep
// it as-is, but we'll update the path it points to
to: p.startsWith(mainEntryPointImportPath)
? p.replace(mainEntryPointImportPath, schema.importPath)
: null,
})),
];
for (const [name, definition] of Array.from(projects.entries())) {
if (name === schema.projectName) {
continue;
}
for (const projectRef of projectRefs) {
if (schema.updateImportPath && projectRef.to) {
const replaceProjectRef = new RegExp(projectRef.from, 'g');
visitNotIgnoredFiles(tree, definition.root, (file) => {
const contents = tree.read(file, 'utf-8');
replaceProjectRef.lastIndex = 0;
if (!replaceProjectRef.test(contents)) {
return;
for (const [name, definition] of Array.from(projects.entries())) {
if (name === schema.projectName) {
continue;
}
updateImportPaths(tree, file, projectRef.from, projectRef.to);
});
}
}
visitNotIgnoredFiles(tree, definition.root, (file) => {
const contents = tree.read(file, 'utf-8');
replaceProjectRef.lastIndex = 0;
if (!replaceProjectRef.test(contents)) {
return;
}
const projectRoot = {
from: project.root,
to: schema.relativeToRootDestination,
};
if (tsConfig) {
const path = tsConfig.compilerOptions.paths[projectRef.from] as string[];
if (!path) {
throw new Error(
[
`unable to find "${projectRef.from}" in`,
`${tsConfigPath} compilerOptions.paths`,
].join(' ')
);
}
const updatedPath = path.map((x) =>
joinPathFragments(projectRoot.to, relative(projectRoot.from, x))
);
if (schema.updateImportPath) {
tsConfig.compilerOptions.paths[projectRef.to] = updatedPath;
if (projectRef.from !== projectRef.to) {
delete tsConfig.compilerOptions.paths[projectRef.from];
updateImportPaths(tree, file, projectRef.from, projectRef.to);
});
}
}
const projectRoot = {
from: project.root,
to: schema.relativeToRootDestination,
};
if (tsConfig) {
const path = tsConfig.compilerOptions.paths[projectRef.from] as string[];
if (!path) {
throw new Error(
[
`unable to find "${projectRef.from}" in`,
`${tsConfigPath} compilerOptions.paths`,
].join(' ')
);
}
const updatedPath = path.map((x) =>
joinPathFragments(projectRoot.to, relative(projectRoot.from, x))
);
if (schema.updateImportPath && projectRef.to) {
tsConfig.compilerOptions.paths[projectRef.to] = updatedPath;
if (projectRef.from !== projectRef.to) {
delete tsConfig.compilerOptions.paths[projectRef.from];
}
} else {
tsConfig.compilerOptions.paths[projectRef.from] = updatedPath;
}
} else {
tsConfig.compilerOptions.paths[projectRef.from] = updatedPath;
}
writeJson(tree, tsConfigPath, tsConfig);
}
}
function ensureTrailingSlash(path: string): string {
return path.endsWith('/') ? path : `${path}/`;
}
/**
* Changes imports in a file from one import to another
*/

View File

@ -1,10 +1,10 @@
import {
getWorkspaceLayout,
joinPathFragments,
normalizePath,
ProjectConfiguration,
Tree,
} from '@nx/devkit';
import { Schema } from '../schema';
/**
@ -35,12 +35,17 @@ export function getDestination(
}
/**
* Replaces slashes with dashes
* Joins path segments replacing slashes with dashes
*
* @param path
*/
export function getNewProjectName(path: string): string {
return path.replace(/\//g, '-');
// strip leading '/' or './' or '../' and trailing '/' and replaces '/' with '-'
return normalizePath(path)
.replace(/(^\.{0,2}\/|\.{1,2}\/|\/$)/g, '')
.split('/')
.filter((x) => !!x)
.join('-');
}
/**
@ -48,9 +53,13 @@ export function getNewProjectName(path: string): string {
*
* @param input
*/
export function normalizeSlashes(input: string): string {
return input
.split('/')
.filter((x) => !!x)
.join('/');
export function normalizePathSlashes(input: string): string {
return (
normalizePath(input)
// strip leading ./ or /
.replace(/^\.?\//, '')
.split('/')
.filter((x) => !!x)
.join('/')
);
}