feat(misc): support new format to determine new project name and destination in move generators (#18878)

Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
This commit is contained in:
Leosvel Pérez Espinosa 2023-09-05 14:40:16 +01:00 committed by GitHub
parent 6b272ed4c4
commit 8564d9ba12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1382 additions and 820 deletions

View File

@ -1,6 +1,6 @@
{
"name": "move",
"factory": "./src/generators/move/move#angularMoveGenerator",
"factory": "./src/generators/move/move#angularMoveGeneratorInternal",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxAngularMove",
@ -22,12 +22,24 @@
"x-dropdown": "projects",
"x-priority": "important"
},
"newProjectName": {
"type": "string",
"alias": "project",
"description": "The new name of the project after the move.",
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
"x-priority": "important"
},
"destination": {
"type": "string",
"description": "The folder to move the Angular project into.",
"$default": { "$source": "argv", "index": 0 },
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the new project name and destination as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"importPath": {
"type": "string",
"description": "The new import path to use in the `tsconfig.base.json`."
@ -50,7 +62,8 @@
},
"aliases": ["mv"],
"description": "Moves an Angular application or library to another folder within the workspace and updates the project configuration.",
"implementation": "/packages/angular/src/generators/move/move#angularMoveGenerator.ts",
"x-deprecated": "Use the `@nx/workspace:move` generator instead. This generator will be removed in Nx v18.",
"implementation": "/packages/angular/src/generators/move/move#angularMoveGeneratorInternal.ts",
"hidden": false,
"path": "/packages/angular/src/generators/move/schema.json",
"type": "generator"

View File

@ -1,6 +1,6 @@
{
"name": "move",
"factory": "./src/generators/move/move#moveGenerator",
"factory": "./src/generators/move/move#moveGeneratorInternal",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxWorkspaceMove",
@ -21,11 +21,23 @@
"description": "The name of the project to move.",
"x-dropdown": "projects"
},
"newProjectName": {
"type": "string",
"alias": "project",
"description": "The new name of the project after the move.",
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
"x-priority": "important"
},
"destination": {
"type": "string",
"description": "The folder to move the project into.",
"$default": { "$source": "argv", "index": 0 }
},
"projectNameAndRootFormat": {
"description": "Whether to generate the new project name and destination as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"importPath": {
"type": "string",
"description": "The new import path to use in the `tsconfig.base.json`."
@ -48,7 +60,7 @@
},
"aliases": ["mv"],
"description": "Move an application or library to another folder.",
"implementation": "/packages/workspace/src/generators/move/move#moveGenerator.ts",
"implementation": "/packages/workspace/src/generators/move/move#moveGeneratorInternal.ts",
"hidden": false,
"path": "/packages/workspace/src/generators/move/schema.json",
"type": "generator"

View File

@ -20,7 +20,9 @@ describe('Move Angular Project', () => {
app1 = uniq('app1');
app2 = uniq('app2');
newPath = `subfolder/${app2}`;
runCLI(`generate @nx/angular:app ${app1} --no-interactive`);
runCLI(
`generate @nx/angular:app ${app1} --project-name-and-root-format=as-provided --no-interactive`
);
});
afterAll(() => cleanupProject());
@ -30,28 +32,26 @@ describe('Move Angular Project', () => {
*/
it('should work for apps', () => {
const moveOutput = runCLI(
`generate @nx/angular:move --project ${app1} ${newPath}`
`generate @nx/angular:move --project ${app1} ${newPath} --project-name-and-root-format=as-provided`
);
// just check the output
expect(moveOutput).toContain(`DELETE apps/${app1}`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/jest.config.ts`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/tsconfig.app.json`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/tsconfig.json`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/tsconfig.spec.json`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/.eslintrc.json`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/src/favicon.ico`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/src/index.html`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/src/main.ts`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/src/styles.css`);
expect(moveOutput).toContain(`CREATE apps/${newPath}/src/test-setup.ts`);
expect(moveOutput).toContain(`DELETE ${app1}`);
expect(moveOutput).toContain(`CREATE ${newPath}/jest.config.ts`);
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.app.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/tsconfig.spec.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/.eslintrc.json`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/favicon.ico`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/index.html`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/main.ts`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/styles.css`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/test-setup.ts`);
expect(moveOutput).toContain(
`CREATE apps/${newPath}/src/app/app.component.html`
`CREATE ${newPath}/src/app/app.component.html`
);
expect(moveOutput).toContain(
`CREATE apps/${newPath}/src/app/app.module.ts`
);
expect(moveOutput).toContain(`CREATE apps/${newPath}/src/assets/.gitkeep`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/app/app.module.ts`);
expect(moveOutput).toContain(`CREATE ${newPath}/src/assets/.gitkeep`);
});
/**
@ -61,7 +61,7 @@ describe('Move Angular Project', () => {
// by default the cypress config doesn't contain any app specific paths
// create a custom config with some app specific paths
updateFile(
`apps/${app1}-e2e/cypress.config.ts`,
`${app1}-e2e/cypress.config.ts`,
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
@ -69,27 +69,25 @@ describe('Move Angular Project', () => {
export default defineConfig({
e2e: {
...nxE2EPreset(__dirname),
videosFolder: '../../dist/cypress/apps/${app1}-e2e/videos',
screenshotsFolder: '../../dist/cypress/apps/${app1}-e2e/screenshots',
videosFolder: '../dist/cypress/${app1}-e2e/videos',
screenshotsFolder: '../dist/cypress/${app1}-e2e/screenshots',
},
});
`
);
const moveOutput = runCLI(
`generate @nx/angular:move --projectName=${app1}-e2e --destination=${newPath}-e2e`
`generate @nx/angular:move --projectName=${app1}-e2e --destination=${newPath}-e2e --project-name-and-root-format=as-provided`
);
// just check that the cypress.config.ts is updated correctly
const cypressConfigPath = `apps/${newPath}-e2e/cypress.config.ts`;
const cypressConfigPath = `${newPath}-e2e/cypress.config.ts`;
expect(moveOutput).toContain(`CREATE ${cypressConfigPath}`);
checkFilesExist(cypressConfigPath);
const cypressConfig = readFile(cypressConfigPath);
expect(cypressConfig).toContain(`../../dist/cypress/${newPath}-e2e/videos`);
expect(cypressConfig).toContain(
`../../../dist/cypress/apps/${newPath}-e2e/videos`
);
expect(cypressConfig).toContain(
`../../../dist/cypress/apps/${newPath}-e2e/screenshots`
`../../dist/cypress/${newPath}-e2e/screenshots`
);
});
@ -99,13 +97,73 @@ describe('Move Angular Project', () => {
it('should work for libraries', () => {
const lib1 = uniq('mylib');
const lib2 = uniq('mylib');
runCLI(`generate @nx/angular:lib ${lib1} --no-interactive`);
runCLI(
`generate @nx/angular:lib ${lib1} --project-name-and-root-format=as-provided --no-interactive`
);
/**
* Create a library which imports the module from the other lib
*/
runCLI(`generate @nx/angular:lib ${lib2} --no-interactive`);
runCLI(
`generate @nx/angular:lib ${lib2} --project-name-and-root-format=as-provided --no-interactive`
);
updateFile(
`${lib2}/src/lib/${lib2}.module.ts`,
`import { ${classify(lib1)}Module } from '@${proj}/${lib1}';
export class ExtendedModule extends ${classify(lib1)}Module { }`
);
const moveOutput = runCLI(
`generate @nx/angular:move --projectName=${lib1} --destination=shared/${lib1} --newProjectName=shared-${lib1} --project-name-and-root-format=as-provided`
);
const newPath = `shared/${lib1}`;
const newModule = `Shared${classify(lib1)}Module`;
const testSetupPath = `${newPath}/src/test-setup.ts`;
expect(moveOutput).toContain(`CREATE ${testSetupPath}`);
checkFilesExist(testSetupPath);
const modulePath = `${newPath}/src/lib/shared-${lib1}.module.ts`;
expect(moveOutput).toContain(`CREATE ${modulePath}`);
checkFilesExist(modulePath);
const moduleFile = readFile(modulePath);
expect(moduleFile).toContain(`export class ${newModule}`);
const indexPath = `${newPath}/src/index.ts`;
expect(moveOutput).toContain(`CREATE ${indexPath}`);
checkFilesExist(indexPath);
const index = readFile(indexPath);
expect(index).toContain(`export * from './lib/shared-${lib1}.module'`);
/**
* Check that the import in lib2 has been updated
*/
const lib2FilePath = `${lib2}/src/lib/${lib2}.module.ts`;
const lib2File = readFile(lib2FilePath);
expect(lib2File).toContain(
`import { ${newModule} } from '@${proj}/shared-${lib1}';`
);
expect(lib2File).toContain(`extends ${newModule}`);
});
it('should move projects correctly with --project-name-and-root-format=derived', () => {
const lib1 = uniq('mylib');
const lib2 = uniq('mylib');
runCLI(
`generate @nx/angular:lib ${lib1} --project-name-and-root-format=derived --no-interactive`
);
/**
* Create a library which imports the module from the other lib
*/
runCLI(
`generate @nx/angular:lib ${lib2} --project-name-and-root-format=derived --no-interactive`
);
updateFile(
`libs/${lib2}/src/lib/${lib2}.module.ts`,
@ -115,7 +173,7 @@ describe('Move Angular Project', () => {
);
const moveOutput = runCLI(
`generate @nx/angular:move --projectName=${lib1} --destination=shared/${lib1}`
`generate @nx/angular:move --projectName=${lib1} --destination=shared/${lib1} --project-name-and-root-format=derived`
);
const newPath = `libs/shared/${lib1}`;

View File

@ -83,15 +83,17 @@ describe('Workspace Tests', () => {
const lib1 = uniq('mylib');
const lib2 = uniq('mylib');
const lib3 = uniq('mylib');
runCLI(`generate @nx/js:lib ${lib1}/data-access --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib1}-data-access --directory=${lib1}/data-access --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
updateFile(
`libs/${lib1}/data-access/src/lib/${lib1}-data-access.ts`,
`${lib1}/data-access/src/lib/${lib1}-data-access.ts`,
`export function fromLibOne() { console.log('This is completely pointless'); }`
);
updateFile(
`libs/${lib1}/data-access/src/index.ts`,
`${lib1}/data-access/src/index.ts`,
`export * from './lib/${lib1}-data-access.ts'`
);
@ -99,11 +101,13 @@ describe('Workspace Tests', () => {
* Create a library which imports a class from lib1
*/
runCLI(`generate @nx/js:lib ${lib2}/ui --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
updateFile(
`libs/${lib2}/ui/src/lib/${lib2}-ui.ts`,
`import { fromLibOne } from '@${proj}/${lib1}/data-access';
`${lib2}/ui/src/lib/${lib2}-ui.ts`,
`import { fromLibOne } from '@${proj}/${lib1}-data-access';
export const fromLibTwo = () => fromLibOne();`
);
@ -112,7 +116,9 @@ describe('Workspace Tests', () => {
* Create a library which has an implicit dependency on lib1
*/
runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib3} --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
await updateProjectConfig(lib3, (config) => {
config.implicitDependencies = [`${lib1}-data-access`];
return config;
@ -123,13 +129,13 @@ describe('Workspace Tests', () => {
*/
const moveOutput = runCLI(
`generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access`
`generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --newProjectName=shared-${lib1}-data-access --project-name-and-root-format=as-provided`
);
expect(moveOutput).toContain(`DELETE libs/${lib1}/data-access`);
expect(exists(`libs/${lib1}/data-access`)).toBeFalsy();
expect(moveOutput).toContain(`DELETE ${lib1}/data-access`);
expect(exists(`${lib1}/data-access`)).toBeFalsy();
const newPath = `libs/shared/${lib1}/data-access`;
const newPath = `shared/${lib1}/data-access`;
const newName = `shared-${lib1}-data-access`;
const readmePath = `${newPath}/README.md`;
@ -141,8 +147,8 @@ describe('Workspace Tests', () => {
checkFilesExist(jestConfigPath);
const jestConfig = readFile(jestConfigPath);
expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`);
expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`);
expect(jestConfig).toContain(`'../../../../coverage/${newPath}'`);
expect(jestConfig).toContain(`preset: '../../../jest.preset.js'`);
expect(jestConfig).toContain(`'../../../coverage/${newPath}'`);
const tsConfigPath = `${newPath}/tsconfig.json`;
expect(moveOutput).toContain(`CREATE ${tsConfigPath}`);
@ -153,7 +159,7 @@ describe('Workspace Tests', () => {
checkFilesExist(tsConfigLibPath);
const tsConfigLib = readJson(tsConfigLibPath);
expect(tsConfigLib.compilerOptions.outDir).toEqual(
'../../../../dist/out-tsc'
'../../../dist/out-tsc'
);
const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`;
@ -161,7 +167,7 @@ describe('Workspace Tests', () => {
checkFilesExist(tsConfigSpecPath);
const tsConfigSpec = readJson(tsConfigSpecPath);
expect(tsConfigSpec.compilerOptions.outDir).toEqual(
'../../../../dist/out-tsc'
'../../../dist/out-tsc'
);
const indexPath = `${newPath}/src/index.ts`;
@ -186,13 +192,13 @@ describe('Workspace Tests', () => {
expect(moveOutput).toContain('UPDATE tsconfig.base.json');
const rootTsConfig = readJson('tsconfig.base.json');
expect(
rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}/data-access`]
rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`]
).toBeUndefined();
expect(
rootTsConfig.compilerOptions.paths[
`@${proj}/shared/${lib1}/data-access`
`@${proj}/shared-${lib1}-data-access`
]
).toEqual([`libs/shared/${lib1}/data-access/src/index.ts`]);
).toEqual([`shared/${lib1}/data-access/src/index.ts`]);
projects = await readResolvedConfiguration();
expect(projects[`${lib1}-data-access`]).toBeUndefined();
@ -200,17 +206,17 @@ describe('Workspace Tests', () => {
expect(project).toBeTruthy();
expect(project.sourceRoot).toBe(`${newPath}/src`);
expect(project.targets.lint.options.lintFilePatterns).toEqual([
`libs/shared/${lib1}/data-access/**/*.ts`,
`libs/shared/${lib1}/data-access/package.json`,
`shared/${lib1}/data-access/**/*.ts`,
`shared/${lib1}/data-access/package.json`,
]);
/**
* Check that the import in lib2 has been updated
*/
const lib2FilePath = `libs/${lib2}/ui/src/lib/${lib2}-ui.ts`;
const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`;
const lib2File = readFile(lib2FilePath);
expect(lib2File).toContain(
`import { fromLibOne } from '@${proj}/shared/${lib1}/data-access';`
`import { fromLibOne } from '@${proj}/shared-${lib1}-data-access';`
);
});
@ -220,16 +226,16 @@ describe('Workspace Tests', () => {
const lib2 = uniq('mylib');
const lib3 = uniq('mylib');
runCLI(
`generate @nx/js:lib ${lib1}/data-access --importPath=${importPath} --unitTestRunner=jest`
`generate @nx/js:lib ${lib1}-data-access --directory=${lib1}/data-access --importPath=${importPath} --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
updateFile(
`libs/${lib1}/data-access/src/lib/${lib1}-data-access.ts`,
`${lib1}/data-access/src/lib/${lib1}-data-access.ts`,
`export function fromLibOne() { console.log('This is completely pointless'); }`
);
updateFile(
`libs/${lib1}/data-access/src/index.ts`,
`${lib1}/data-access/src/index.ts`,
`export * from './lib/${lib1}-data-access.ts'`
);
@ -237,10 +243,12 @@ describe('Workspace Tests', () => {
* Create a library which imports a class from lib1
*/
runCLI(`generate @nx/js:lib ${lib2}/ui --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
updateFile(
`libs/${lib2}/ui/src/lib/${lib2}-ui.ts`,
`${lib2}/ui/src/lib/${lib2}-ui.ts`,
`import { fromLibOne } from '${importPath}';
export const fromLibTwo = () => fromLibOne();`
@ -250,7 +258,9 @@ describe('Workspace Tests', () => {
* Create a library which has an implicit dependency on lib1
*/
runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib3} --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
await updateProjectConfig(lib3, (config) => {
config.implicitDependencies = [`${lib1}-data-access`];
return config;
@ -261,13 +271,13 @@ describe('Workspace Tests', () => {
*/
const moveOutput = runCLI(
`generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access`
`generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --newProjectName=shared-${lib1}-data-access --project-name-and-root-format=as-provided`
);
expect(moveOutput).toContain(`DELETE libs/${lib1}/data-access`);
expect(exists(`libs/${lib1}/data-access`)).toBeFalsy();
expect(moveOutput).toContain(`DELETE ${lib1}/data-access`);
expect(exists(`${lib1}/data-access`)).toBeFalsy();
const newPath = `libs/shared/${lib1}/data-access`;
const newPath = `shared/${lib1}/data-access`;
const newName = `shared-${lib1}-data-access`;
const readmePath = `${newPath}/README.md`;
@ -279,8 +289,8 @@ describe('Workspace Tests', () => {
checkFilesExist(jestConfigPath);
const jestConfig = readFile(jestConfigPath);
expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`);
expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`);
expect(jestConfig).toContain(`'../../../../coverage/${newPath}'`);
expect(jestConfig).toContain(`preset: '../../../jest.preset.js'`);
expect(jestConfig).toContain(`'../../../coverage/${newPath}'`);
const tsConfigPath = `${newPath}/tsconfig.json`;
expect(moveOutput).toContain(`CREATE ${tsConfigPath}`);
@ -291,7 +301,7 @@ describe('Workspace Tests', () => {
checkFilesExist(tsConfigLibPath);
const tsConfigLib = readJson(tsConfigLibPath);
expect(tsConfigLib.compilerOptions.outDir).toEqual(
'../../../../dist/out-tsc'
'../../../dist/out-tsc'
);
const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`;
@ -299,7 +309,7 @@ describe('Workspace Tests', () => {
checkFilesExist(tsConfigSpecPath);
const tsConfigSpec = readJson(tsConfigSpecPath);
expect(tsConfigSpec.compilerOptions.outDir).toEqual(
'../../../../dist/out-tsc'
'../../../dist/out-tsc'
);
const indexPath = `${newPath}/src/index.ts`;
@ -313,13 +323,13 @@ describe('Workspace Tests', () => {
expect(moveOutput).toContain('UPDATE tsconfig.base.json');
const rootTsConfig = readJson('tsconfig.base.json');
expect(
rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}/data-access`]
rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`]
).toBeUndefined();
expect(
rootTsConfig.compilerOptions.paths[
`@${proj}/shared/${lib1}/data-access`
`@${proj}/shared-${lib1}-data-access`
]
).toEqual([`libs/shared/${lib1}/data-access/src/index.ts`]);
).toEqual([`shared/${lib1}/data-access/src/index.ts`]);
const projects = await readResolvedConfiguration();
expect(projects[`${lib1}-data-access`]).toBeUndefined();
@ -331,21 +341,21 @@ describe('Workspace Tests', () => {
expect(lib3Config.implicitDependencies).toEqual([newName]);
expect(project.targets.lint.options.lintFilePatterns).toEqual([
`libs/shared/${lib1}/data-access/**/*.ts`,
`libs/shared/${lib1}/data-access/package.json`,
`shared/${lib1}/data-access/**/*.ts`,
`shared/${lib1}/data-access/package.json`,
]);
/**
* Check that the import in lib2 has been updated
*/
const lib2FilePath = `libs/${lib2}/ui/src/lib/${lib2}-ui.ts`;
const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`;
const lib2File = readFile(lib2FilePath);
expect(lib2File).toContain(
`import { fromLibOne } from '@${proj}/shared/${lib1}/data-access';`
`import { fromLibOne } from '@${proj}/shared-${lib1}-data-access';`
);
});
it('should work for custom workspace layouts', async () => {
it('should work for custom workspace layouts with --project-name-and-root-format=derived', async () => {
const lib1 = uniq('mylib');
const lib2 = uniq('mylib');
const lib3 = uniq('mylib');
@ -354,7 +364,9 @@ describe('Workspace Tests', () => {
nxJson.workspaceLayout = { libsDir: 'packages' };
updateFile('nx.json', JSON.stringify(nxJson));
runCLI(`generate @nx/js:lib ${lib1}/data-access --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib1}/data-access --unitTestRunner=jest --project-name-and-root-format=derived`
);
updateFile(
`packages/${lib1}/data-access/src/lib/${lib1}-data-access.ts`,
@ -370,7 +382,9 @@ describe('Workspace Tests', () => {
* Create a library which imports a class from lib1
*/
runCLI(`generate @nx/js:lib ${lib2}/ui --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib2}/ui --unitTestRunner=jest --project-name-and-root-format=derived`
);
updateFile(
`packages/${lib2}/ui/src/lib/${lib2}-ui.ts`,
@ -383,7 +397,9 @@ describe('Workspace Tests', () => {
* Create a library which has an implicit dependency on lib1
*/
runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib3} --unitTestRunner=jest --project-name-and-root-format=derived`
);
await updateProjectConfig(lib3, (config) => {
config.implicitDependencies = [`${lib1}-data-access`];
return config;
@ -394,7 +410,7 @@ describe('Workspace Tests', () => {
*/
const moveOutput = runCLI(
`generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access`
`generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --project-name-and-root-format=derived`
);
expect(moveOutput).toContain(`DELETE packages/${lib1}/data-access`);
@ -483,26 +499,27 @@ describe('Workspace Tests', () => {
const lib1 = uniq('lib1');
const lib2 = uniq('lib2');
const lib3 = uniq('lib3');
runCLI(`generate @nx/js:lib ${lib1} --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib1} --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
updateFile(
`libs/${lib1}/src/lib/${lib1}.ts`,
`${lib1}/src/lib/${lib1}.ts`,
`export function fromLibOne() { console.log('This is completely pointless'); }`
);
updateFile(
`libs/${lib1}/src/index.ts`,
`export * from './lib/${lib1}.ts'`
);
updateFile(`${lib1}/src/index.ts`, `export * from './lib/${lib1}.ts'`);
/**
* Create a library which imports a class from lib1
*/
runCLI(`generate @nx/js:lib ${lib2}/ui --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
updateFile(
`libs/${lib2}/ui/src/lib/${lib2}-ui.ts`,
`${lib2}/ui/src/lib/${lib2}-ui.ts`,
`import { fromLibOne } from '@${proj}/${lib1}';
export const fromLibTwo = () => fromLibOne();`
@ -512,7 +529,9 @@ describe('Workspace Tests', () => {
* Create a library which has an implicit dependency on lib1
*/
runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib3} --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
await updateProjectConfig(lib3, (config) => {
config.implicitDependencies = [lib1];
return config;
@ -523,13 +542,13 @@ describe('Workspace Tests', () => {
*/
const moveOutput = runCLI(
`generate @nx/workspace:move --project ${lib1} ${lib1}/data-access`
`generate @nx/workspace:move --project ${lib1} ${lib1}/data-access --newProjectName=${lib1}-data-access --project-name-and-root-format=as-provided`
);
expect(moveOutput).toContain(`DELETE libs/${lib1}/project.json`);
expect(exists(`libs/${lib1}/project.json`)).toBeFalsy();
expect(moveOutput).toContain(`DELETE ${lib1}/project.json`);
expect(exists(`${lib1}/project.json`)).toBeFalsy();
const newPath = `libs/${lib1}/data-access`;
const newPath = `${lib1}/data-access`;
const newName = `${lib1}-data-access`;
const readmePath = `${newPath}/README.md`;
@ -541,8 +560,8 @@ describe('Workspace Tests', () => {
checkFilesExist(jestConfigPath);
const jestConfig = readFile(jestConfigPath);
expect(jestConfig).toContain(`displayName: '${lib1}-data-access'`);
expect(jestConfig).toContain(`preset: '../../../jest.preset.js'`);
expect(jestConfig).toContain(`'../../../coverage/${newPath}'`);
expect(jestConfig).toContain(`preset: '../../jest.preset.js'`);
expect(jestConfig).toContain(`'../../coverage/${newPath}'`);
const tsConfigPath = `${newPath}/tsconfig.json`;
expect(moveOutput).toContain(`CREATE ${tsConfigPath}`);
@ -552,17 +571,13 @@ describe('Workspace Tests', () => {
expect(moveOutput).toContain(`CREATE ${tsConfigLibPath}`);
checkFilesExist(tsConfigLibPath);
const tsConfigLib = readJson(tsConfigLibPath);
expect(tsConfigLib.compilerOptions.outDir).toEqual(
'../../../dist/out-tsc'
);
expect(tsConfigLib.compilerOptions.outDir).toEqual('../../dist/out-tsc');
const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`;
expect(moveOutput).toContain(`CREATE ${tsConfigSpecPath}`);
checkFilesExist(tsConfigSpecPath);
const tsConfigSpec = readJson(tsConfigSpecPath);
expect(tsConfigSpec.compilerOptions.outDir).toEqual(
'../../../dist/out-tsc'
);
expect(tsConfigSpec.compilerOptions.outDir).toEqual('../../dist/out-tsc');
const indexPath = `${newPath}/src/index.ts`;
expect(moveOutput).toContain(`CREATE ${indexPath}`);
@ -587,8 +602,8 @@ describe('Workspace Tests', () => {
rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}`]
).toBeUndefined();
expect(
rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}/data-access`]
).toEqual([`libs/${lib1}/data-access/src/index.ts`]);
rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`]
).toEqual([`${lib1}/data-access/src/index.ts`]);
projects = await readResolvedConfiguration();
expect(projects[lib1]).toBeUndefined();
@ -596,17 +611,17 @@ describe('Workspace Tests', () => {
expect(project).toBeTruthy();
expect(project.sourceRoot).toBe(`${newPath}/src`);
expect(project.targets.lint.options.lintFilePatterns).toEqual([
`libs/${lib1}/data-access/**/*.ts`,
`libs/${lib1}/data-access/package.json`,
`${lib1}/data-access/**/*.ts`,
`${lib1}/data-access/package.json`,
]);
/**
* Check that the import in lib2 has been updated
*/
const lib2FilePath = `libs/${lib2}/ui/src/lib/${lib2}-ui.ts`;
const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`;
const lib2File = readFile(lib2FilePath);
expect(lib2File).toContain(
`import { fromLibOne } from '@${proj}/${lib1}/data-access';`
`import { fromLibOne } from '@${proj}/${lib1}-data-access';`
);
});
@ -618,22 +633,24 @@ describe('Workspace Tests', () => {
const lib1 = uniq('mylib');
const lib2 = uniq('mylib');
const lib3 = uniq('mylib');
runCLI(`generate @nx/js:lib ${lib1}/data-access --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib1}-data-access --directory=${lib1}/data-access --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
let rootTsConfig = readJson('tsconfig.base.json');
expect(
rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}/data-access`]
rootTsConfig.compilerOptions.paths[`@${proj}/${lib1}-data-access`]
).toBeUndefined();
expect(
rootTsConfig.compilerOptions.paths[`${lib1}/data-access`]
rootTsConfig.compilerOptions.paths[`${lib1}-data-access`]
).toBeDefined();
updateFile(
`libs/${lib1}/data-access/src/lib/${lib1}-data-access.ts`,
`${lib1}/data-access/src/lib/${lib1}-data-access.ts`,
`export function fromLibOne() { console.log('This is completely pointless'); }`
);
updateFile(
`libs/${lib1}/data-access/src/index.ts`,
`${lib1}/data-access/src/index.ts`,
`export * from './lib/${lib1}-data-access.ts'`
);
@ -641,11 +658,13 @@ describe('Workspace Tests', () => {
* Create a library which imports a class from lib1
*/
runCLI(`generate @nx/js:lib ${lib2}/ui --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib2}-ui --directory=${lib2}/ui --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
updateFile(
`libs/${lib2}/ui/src/lib/${lib2}-ui.ts`,
`import { fromLibOne } from '${lib1}/data-access';
`${lib2}/ui/src/lib/${lib2}-ui.ts`,
`import { fromLibOne } from '${lib1}-data-access';
export const fromLibTwo = () => fromLibOne();`
);
@ -654,7 +673,9 @@ describe('Workspace Tests', () => {
* Create a library which has an implicit dependency on lib1
*/
runCLI(`generate @nx/js:lib ${lib3} --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib ${lib3} --unitTestRunner=jest --project-name-and-root-format=as-provided`
);
await updateProjectConfig(lib3, (config) => {
config.implicitDependencies = [`${lib1}-data-access`];
return config;
@ -665,13 +686,13 @@ describe('Workspace Tests', () => {
*/
const moveOutput = runCLI(
`generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access`
`generate @nx/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access --newProjectName=shared-${lib1}-data-access --project-name-and-root-format=as-provided`
);
expect(moveOutput).toContain(`DELETE libs/${lib1}/data-access`);
expect(exists(`libs/${lib1}/data-access`)).toBeFalsy();
expect(moveOutput).toContain(`DELETE ${lib1}/data-access`);
expect(exists(`${lib1}/data-access`)).toBeFalsy();
const newPath = `libs/shared/${lib1}/data-access`;
const newPath = `shared/${lib1}/data-access`;
const newName = `shared-${lib1}-data-access`;
const readmePath = `${newPath}/README.md`;
@ -698,11 +719,11 @@ describe('Workspace Tests', () => {
expect(moveOutput).toContain('UPDATE tsconfig.base.json');
rootTsConfig = readJson('tsconfig.base.json');
expect(
rootTsConfig.compilerOptions.paths[`${lib1}/data-access`]
rootTsConfig.compilerOptions.paths[`${lib1}-data-access`]
).toBeUndefined();
expect(
rootTsConfig.compilerOptions.paths[`shared/${lib1}/data-access`]
).toEqual([`libs/shared/${lib1}/data-access/src/index.ts`]);
rootTsConfig.compilerOptions.paths[`shared-${lib1}-data-access`]
).toEqual([`shared/${lib1}/data-access/src/index.ts`]);
const projects = await readResolvedConfiguration();
expect(projects[`${lib1}-data-access`]).toBeUndefined();
@ -710,17 +731,17 @@ describe('Workspace Tests', () => {
expect(project).toBeTruthy();
expect(project.sourceRoot).toBe(`${newPath}/src`);
expect(project.targets.lint.options.lintFilePatterns).toEqual([
`libs/shared/${lib1}/data-access/**/*.ts`,
`libs/shared/${lib1}/data-access/package.json`,
`shared/${lib1}/data-access/**/*.ts`,
`shared/${lib1}/data-access/package.json`,
]);
/**
* Check that the import in lib2 has been updated
*/
const lib2FilePath = `libs/${lib2}/ui/src/lib/${lib2}-ui.ts`;
const lib2FilePath = `${lib2}/ui/src/lib/${lib2}-ui.ts`;
const lib2File = readFile(lib2FilePath);
expect(lib2File).toContain(
`import { fromLibOne } from 'shared/${lib1}/data-access';`
`import { fromLibOne } from 'shared-${lib1}-data-access';`
);
});
});

View File

@ -74,7 +74,8 @@
"factory": "./src/generators/move/compat",
"schema": "./src/generators/move/schema.json",
"aliases": ["mv"],
"description": "Moves an Angular application or library to another folder within the workspace and updates the project configuration."
"description": "Moves an Angular application or library to another folder within the workspace and updates the project configuration.",
"x-deprecated": "Use the `@nx/workspace:move` generator instead. This generator will be removed in Nx v18."
},
"convert-to-with-mf": {
"factory": "./src/generators/convert-to-with-mf/convert-to-with-mf.compat",
@ -230,10 +231,11 @@
"description": "Generate a Remote Angular Module Federation Application."
},
"move": {
"factory": "./src/generators/move/move#angularMoveGenerator",
"factory": "./src/generators/move/move#angularMoveGeneratorInternal",
"schema": "./src/generators/move/schema.json",
"aliases": ["mv"],
"description": "Moves an Angular application or library to another folder within the workspace and updates the project configuration."
"description": "Moves an Angular application or library to another folder within the workspace and updates the project configuration.",
"x-deprecated": "Use the `@nx/workspace:move` generator instead. This generator will be removed in Nx v18."
},
"convert-to-with-mf": {
"factory": "./src/generators/convert-to-with-mf/convert-to-with-mf",

View File

@ -26,6 +26,7 @@
"./src/utils": "./src/utils/public-api.js",
"./plugins/component-testing": "./plugins/component-testing.js",
"./src/generators/utils": "./src/generators/utils/index.js",
"./src/generators/move/move-impl": "./src/generators/move/move-impl.js",
"./src/builders/*/schema.json": "./src/builders/*/schema.json",
"./src/builders/*.impl": "./src/builders/*.impl.js",
"./src/executors/*/schema.json": "./src/executors/*/schema.json",

View File

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

View File

@ -1,15 +0,0 @@
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

@ -0,0 +1,4 @@
export type MoveImplOptions = {
oldProjectName: string;
newProjectName: string;
};

View File

@ -1,301 +0,0 @@
import { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/linter';
import { moveGenerator } from '@nx/workspace/generators';
import { UnitTestRunner } from '../../../utils/test-runners';
import { generateTestLibrary } from '../../utils/testing';
import { NormalizedSchema } from '../schema';
import { updateModuleName } from './update-module-name';
describe('updateModuleName Rule', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should handle nesting resulting in the same project name', async () => {
const updatedModulePath = '/libs/my/first/src/lib/my-first.module.ts';
await generateTestLibrary(tree, {
name: 'my-first',
simpleName: true,
});
const schema: NormalizedSchema = {
projectName: 'my-first',
destination: 'my/first',
updateImportPath: true,
newProjectName: 'my-first',
oldProjectRoot: 'my-first',
};
await moveGenerator(tree, schema);
updateModuleName(tree, { ...schema, destination: 'my/first' });
expect(tree.exists(updatedModulePath)).toBe(true);
const moduleFile = tree.read(updatedModulePath, 'utf-8');
expect(moduleFile).toContain(`export class MyFirstModule {}`);
});
describe('move to subfolder', () => {
const updatedModulePath =
'/libs/shared/my-first/src/lib/shared-my-first.module.ts';
const updatedModuleSpecPath =
'/libs/shared/my-first/src/lib/shared-my-first.module.spec.ts';
const indexPath = '/libs/shared/my-first/src/index.ts';
const secondModulePath = 'my-second/src/lib/my-second.module.ts';
const schema: NormalizedSchema = {
projectName: 'my-first',
destination: 'shared/my-first',
updateImportPath: true,
newProjectName: 'shared-my-first',
oldProjectRoot: 'my-first',
};
beforeEach(async () => {
await generateTestLibrary(tree, {
name: 'my-first',
buildable: false,
linter: Linter.EsLint,
publishable: false,
simpleName: true,
skipFormat: false,
unitTestRunner: UnitTestRunner.Jest,
});
await generateTestLibrary(tree, {
name: 'my-second',
buildable: false,
linter: Linter.EsLint,
publishable: false,
simpleName: true,
skipFormat: false,
unitTestRunner: UnitTestRunner.Jest,
});
tree.write(
'my-first/src/lib/my-first.module.ts',
`import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [CommonModule]
})
export class MyFirstModule {}`
);
tree.write(
'my-first/src/lib/my-first.module.spec.ts',
`import { async, TestBed } from '@angular/core/testing';
import { MyFirstModule } from './my-first.module';
describe('MyFirstModule', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MyFirstModule]
}).compileComponents();
}));
it('should create', () => {
expect(MyFirstModule).toBeDefined();
});
});`
);
tree.write(
secondModulePath,
`import { MyFirstModule } from '@proj/my-first';
export class MySecondModule extends MyFirstModule {}
`
);
await moveGenerator(tree, schema);
});
it('should rename the module files and update the module name', async () => {
updateModuleName(tree, schema);
expect(tree.exists(updatedModulePath)).toBe(true);
expect(tree.exists(updatedModuleSpecPath)).toBe(true);
const moduleFile = tree.read(updatedModulePath, 'utf-8');
expect(moduleFile).toContain(`export class SharedMyFirstModule {}`);
const moduleSpecFile = tree.read(updatedModuleSpecPath, 'utf-8');
expect(moduleSpecFile).toContain(
`import { SharedMyFirstModule } from './shared-my-first.module';`
);
expect(moduleSpecFile).toContain(
`describe('SharedMyFirstModule', () => {`
);
expect(moduleSpecFile).toContain(`imports: [SharedMyFirstModule]`);
expect(moduleSpecFile).toContain(
`expect(SharedMyFirstModule).toBeDefined();`
);
});
it('should update any references to the module', async () => {
updateModuleName(tree, schema);
const importerFile = tree.read(secondModulePath, 'utf-8');
expect(importerFile).toContain(
`import { SharedMyFirstModule } from '@proj/shared/my-first';`
);
expect(importerFile).toContain(
`export class MySecondModule extends SharedMyFirstModule {}`
);
});
it('should update the index.ts file which exports the module', async () => {
updateModuleName(tree, schema);
const indexFile = tree.read(indexPath, 'utf-8');
expect(indexFile).toContain(
`export * from './lib/shared-my-first.module';`
);
});
});
describe('rename', () => {
const schema: NormalizedSchema = {
projectName: 'my-source',
destination: 'my-destination',
updateImportPath: true,
newProjectName: 'my-destination',
oldProjectRoot: 'my-source',
};
const modulePath = 'my-destination/src/lib/my-destination.module.ts';
const moduleSpecPath =
'my-destination/src/lib/my-destination.module.spec.ts';
const indexPath = 'my-destination/src/index.ts';
const importerPath = 'my-importer/src/lib/my-importing-file.ts';
beforeEach(async () => {
// fake a mid-move tree:
await generateTestLibrary(tree, {
name: 'my-destination',
buildable: false,
linter: Linter.EsLint,
publishable: false,
simpleName: true,
skipFormat: false,
unitTestRunner: UnitTestRunner.Jest,
});
tree.write(
'my-destination/src/lib/my-source.module.ts',
`import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [CommonModule]
})
export class MySourceModule {}`
);
tree.write(
'my-destination/src/lib/my-source.module.spec.ts',
`import { async, TestBed } from '@angular/core/testing';
import { MySourceModule } from './my-source.module';
describe('MySourceModule', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MySourceModule]
}).compileComponents();
}));
it('should create', () => {
expect(MySourceModule).toBeDefined();
});
});`
);
tree.write(
indexPath,
`export * from './lib/my-source.module';
`
);
tree.delete(modulePath);
tree.delete(moduleSpecPath);
await generateTestLibrary(tree, {
name: 'my-importer',
buildable: false,
linter: Linter.EsLint,
publishable: false,
simpleName: true,
skipFormat: false,
unitTestRunner: UnitTestRunner.Jest,
});
tree.write(
importerPath,
`import { MySourceModule } from '@proj/my-destination';
export class MyExtendedSourceModule extends MySourceModule {}
`
);
});
it('should rename the module files and update the module name', async () => {
updateModuleName(tree, schema);
expect(tree.exists(modulePath)).toBe(true);
expect(tree.exists(moduleSpecPath)).toBe(true);
const moduleFile = tree.read(modulePath, 'utf-8');
expect(moduleFile).toContain(`export class MyDestinationModule {}`);
const moduleSpecFile = tree.read(moduleSpecPath, 'utf-8');
expect(moduleSpecFile).toContain(
`import { MyDestinationModule } from './my-destination.module';`
);
expect(moduleSpecFile).toContain(
`describe('MyDestinationModule', () => {`
);
expect(moduleSpecFile).toContain(`imports: [MyDestinationModule]`);
expect(moduleSpecFile).toContain(
`expect(MyDestinationModule).toBeDefined();`
);
});
it('should update any references to the module', async () => {
updateModuleName(tree, schema);
const importerFile = tree.read(importerPath, 'utf-8');
expect(importerFile).toContain(
`import { MyDestinationModule } from '@proj/my-destination';`
);
expect(importerFile).toContain(
`export class MyExtendedSourceModule extends MyDestinationModule {}`
);
});
it('should update the index.ts file which exports the module', async () => {
updateModuleName(tree, schema);
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(
'my-source-demo/src/lib/my-source-demo.module.ts',
'utf-8'
);
expect(moduleFile).toContain(`export class MySourceDemoModule {}`);
});
});
});

View File

@ -7,7 +7,7 @@ import {
Tree,
visitNotIgnoredFiles,
} from '@nx/devkit';
import type { NormalizedSchema } from '../schema';
import type { MoveImplOptions } from './types';
/**
* Updates the Angular module name (including the spec file and index.ts)
@ -19,8 +19,16 @@ import type { NormalizedSchema } from '../schema';
*/
export function updateModuleName(
tree: Tree,
{ projectName: oldProjectName, newProjectName }: NormalizedSchema
{ oldProjectName, newProjectName }: MoveImplOptions
): void {
const unscopedNewProjectName = newProjectName.startsWith('@')
? newProjectName.split('/')[1]
: newProjectName;
if (oldProjectName === unscopedNewProjectName) {
return;
}
const project = readProjectConfiguration(tree, newProjectName);
if (project.projectType === 'application') {
@ -31,14 +39,14 @@ export function updateModuleName(
const moduleName = {
from: `${names(oldProjectName).className}Module`,
to: `${names(newProjectName).className}Module`,
to: `${names(unscopedNewProjectName).className}Module`,
};
const findModuleName = new RegExp(`\\b${moduleName.from}`, 'g');
const moduleFile = {
from: `${oldProjectName}.module`,
to: `${newProjectName}.module`,
to: `${unscopedNewProjectName}.module`,
};
const findFileName = new RegExp(`\\b${moduleFile.from}`, 'g');

View File

@ -7,9 +7,9 @@ import {
workspaceRoot,
} from '@nx/devkit';
import { join, relative } from 'path';
import type { NormalizedSchema } from '../schema';
import type { MoveImplOptions } from './types';
export function updateNgPackage(tree: Tree, schema: NormalizedSchema): void {
export function updateNgPackage(tree: Tree, schema: MoveImplOptions): void {
const project = readProjectConfiguration(tree, schema.newProjectName);
if (project.projectType === 'application') {

View File

@ -6,7 +6,7 @@ import {
visitNotIgnoredFiles,
} from '@nx/devkit';
import { basename, dirname } from 'path';
import type { NormalizedSchema } from '../schema';
import type { MoveImplOptions } from './types';
const libraryExecutors = [
'@angular-devkit/build-angular:ng-packagr',
@ -19,8 +19,12 @@ const libraryExecutors = [
export function updateSecondaryEntryPoints(
tree: Tree,
schema: NormalizedSchema
schema: MoveImplOptions
): void {
if (schema.oldProjectName === schema.newProjectName) {
return;
}
const project = readProjectConfiguration(tree, schema.newProjectName);
if (project.projectType !== 'library') {
@ -47,7 +51,7 @@ export function updateSecondaryEntryPoints(
updateReadme(
tree,
dirname(filePath),
schema.projectName,
schema.oldProjectName,
schema.newProjectName
);
});

View File

@ -0,0 +1,35 @@
import { createProjectGraphAsync, type Tree } from '@nx/devkit';
import {
updateModuleName,
updateNgPackage,
updateSecondaryEntryPoints,
} from './lib';
import type { MoveImplOptions } from './lib/types';
/**
* Angular-specific logic to move a project to another directory.
* This is invoked by the `@nx/workspace:move` generator.
*/
export async function move(
tree: Tree,
options: MoveImplOptions
): Promise<void> {
// while the project has already being moved at this point, the changes are
// still in the virtual tree and haven't been committed, so the project graph
// still contains the old project name
if (!(await isAngularProject(options.oldProjectName))) {
return;
}
updateModuleName(tree, options);
updateNgPackage(tree, options);
updateSecondaryEntryPoints(tree, options);
}
async function isAngularProject(project: string): Promise<boolean> {
const projectGraph = await createProjectGraphAsync();
return projectGraph.dependencies[project]?.some(
(dependency) => dependency.target === 'npm:@angular/core'
);
}

View File

@ -1,20 +1,38 @@
import * as devkit from '@nx/devkit';
import { readJson, Tree } from '@nx/devkit';
import { ProjectGraph, 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 { librarySecondaryEntryPointGenerator } from '../library-secondary-entry-point/library-secondary-entry-point';
import { generateTestLibrary } from '../utils/testing';
import { angularMoveGenerator } from './move';
describe('@nx/angular:move', () => {
let tree: Tree;
let projectGraph: ProjectGraph;
function addProjectToGraph(project: string): void {
projectGraph = {
dependencies: {
[project]: [
{ source: project, target: 'npm:@angular/core', type: 'static' },
],
},
nodes: {
[project]: {
name: project,
type: 'lib',
data: { root: project, targets: {} },
},
},
};
}
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await generateTestLibrary(tree, {
name: 'mylib',
name: 'my-lib',
buildable: false,
linter: Linter.EsLint,
publishable: false,
@ -23,32 +41,38 @@ describe('@nx/angular:move', () => {
unitTestRunner: UnitTestRunner.Jest,
});
jest.clearAllMocks();
jest
.spyOn(devkit, 'createProjectGraphAsync')
.mockImplementation(() => Promise.resolve(projectGraph));
});
it('should move a project', async () => {
addProjectToGraph('my-lib');
await angularMoveGenerator(tree, {
projectName: 'mylib',
projectName: 'my-lib',
newProjectName: 'mynewlib',
destination: 'mynewlib',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
expect(tree.exists('libs/mynewlib/src/lib/mynewlib.module.ts')).toEqual(
true
);
expect(tree.exists('mynewlib/src/lib/mynewlib.module.ts')).toEqual(true);
});
it('should update ng-package.json dest property', async () => {
await generateTestLibrary(tree, { name: 'mylib2', buildable: true });
addProjectToGraph('mylib2');
await angularMoveGenerator(tree, {
projectName: 'mylib2',
destination: 'mynewlib2',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
const ngPackageJson = readJson(tree, 'libs/mynewlib2/ng-package.json');
expect(ngPackageJson.dest).toEqual('../../dist/libs/mynewlib2');
const ngPackageJson = readJson(tree, 'mynewlib2/ng-package.json');
expect(ngPackageJson.dest).toEqual('../dist/mynewlib2');
});
it('should update secondary entry points readme file', async () => {
@ -57,14 +81,17 @@ describe('@nx/angular:move', () => {
library: 'mylib2',
name: 'testing',
});
addProjectToGraph('mylib2');
await angularMoveGenerator(tree, {
projectName: 'mylib2',
newProjectName: 'mynewlib2',
destination: 'mynewlib2',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
const readme = tree.read('libs/mynewlib2/testing/README.md', 'utf-8');
const readme = tree.read('mynewlib2/testing/README.md', 'utf-8');
expect(readme).toMatchInlineSnapshot(`
"# @proj/mynewlib2/testing
@ -73,28 +100,275 @@ describe('@nx/angular:move', () => {
`);
});
it('should format files', async () => {
jest.spyOn(devkit, 'formatFiles');
it('should handle nesting resulting in the same project name', async () => {
addProjectToGraph('my-lib');
await angularMoveGenerator(tree, {
projectName: 'mylib',
destination: 'mynewlib',
projectName: 'my-lib',
destination: 'my/lib',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
expect(devkit.formatFiles).toHaveBeenCalled();
expect(tree.exists('my/lib/src/lib/my-lib.module.ts')).toBe(true);
const moduleFile = tree.read('my/lib/src/lib/my-lib.module.ts', 'utf-8');
expect(moduleFile).toContain(`export class MyLibModule {}`);
});
it('should not format files when --skipFormat=true', async () => {
jest.spyOn(devkit, 'formatFiles');
describe('move to subfolder', () => {
beforeEach(async () => {
await generateTestLibrary(tree, {
name: 'my-lib2',
buildable: false,
linter: Linter.EsLint,
publishable: false,
simpleName: true,
skipFormat: false,
unitTestRunner: UnitTestRunner.Jest,
});
tree.write(
'my-lib/src/lib/my-lib.module.ts',
`import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
await angularMoveGenerator(tree, {
projectName: 'mylib',
destination: 'mynewlib',
updateImportPath: true,
skipFormat: true,
@NgModule({
imports: [CommonModule]
})
export class MyLibModule {}`
);
tree.write(
'my-lib/src/lib/my-lib.module.spec.ts',
`import { async, TestBed } from '@angular/core/testing';
import { MyLibModule } from './my-lib.module';
describe('MyLibModule', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MyLibModule]
}).compileComponents();
}));
it('should create', () => {
expect(MyLibModule).toBeDefined();
});
});`
);
tree.write(
'my-lib2/src/lib/my-lib2.module.ts',
`import { MyLibModule } from '@proj/my-lib';
export class MyLib2Module extends MyLibModule {}
`
);
});
expect(devkit.formatFiles).not.toHaveBeenCalled();
it('should rename the module files and update the module name', async () => {
addProjectToGraph('my-lib');
await angularMoveGenerator(tree, {
projectName: 'my-lib',
newProjectName: 'shared-my-lib',
destination: 'shared/my-lib',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
expect(tree.exists('shared/my-lib/src/lib/shared-my-lib.module.ts')).toBe(
true
);
expect(
tree.exists('shared/my-lib/src/lib/shared-my-lib.module.spec.ts')
).toBe(true);
const moduleFile = tree.read(
'shared/my-lib/src/lib/shared-my-lib.module.ts',
'utf-8'
);
expect(moduleFile).toContain(`export class SharedMyLibModule {}`);
const moduleSpecFile = tree.read(
'shared/my-lib/src/lib/shared-my-lib.module.spec.ts',
'utf-8'
);
expect(moduleSpecFile).toContain(
`import { SharedMyLibModule } from './shared-my-lib.module';`
);
expect(moduleSpecFile).toContain(`describe('SharedMyLibModule', () => {`);
expect(moduleSpecFile).toContain(`imports: [SharedMyLibModule]`);
expect(moduleSpecFile).toContain(
`expect(SharedMyLibModule).toBeDefined();`
);
});
it('should update any references to the module', async () => {
addProjectToGraph('my-lib');
await angularMoveGenerator(tree, {
projectName: 'my-lib',
newProjectName: 'shared-my-lib',
destination: 'shared/my-lib',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
const importerFile = tree.read(
'my-lib2/src/lib/my-lib2.module.ts',
'utf-8'
);
expect(importerFile).toContain(
`import { SharedMyLibModule } from '@proj/shared-my-lib';`
);
expect(importerFile).toContain(
`export class MyLib2Module extends SharedMyLibModule {}`
);
});
it('should update the index.ts file which exports the module', async () => {
addProjectToGraph('my-lib');
await angularMoveGenerator(tree, {
projectName: 'my-lib',
newProjectName: 'shared-my-lib',
destination: 'shared/my-lib',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
const indexFile = tree.read('shared/my-lib/src/index.ts', 'utf-8');
expect(indexFile).toContain(
`export * from './lib/shared-my-lib.module';`
);
});
});
describe('rename', () => {
beforeEach(async () => {
await generateTestLibrary(tree, {
name: 'my-importer',
buildable: false,
linter: Linter.EsLint,
publishable: false,
simpleName: true,
skipFormat: false,
unitTestRunner: UnitTestRunner.Jest,
});
tree.write(
'my-importer/src/lib/my-importing-file.ts',
`import { MyLibModule } from '@proj/my-lib';
export class MyExtendedLibModule extends MyLibModule {}
`
);
});
it('should rename the module file and update the module name', async () => {
addProjectToGraph('my-lib');
await angularMoveGenerator(tree, {
projectName: 'my-lib',
newProjectName: 'my-destination',
destination: 'my-destination',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
expect(
tree.exists('my-destination/src/lib/my-destination.module.ts')
).toBe(true);
const moduleFile = tree.read(
'my-destination/src/lib/my-destination.module.ts',
'utf-8'
);
expect(moduleFile).toContain(`export class MyDestinationModule {}`);
});
it('should update any references to the module', async () => {
addProjectToGraph('my-lib');
await angularMoveGenerator(tree, {
projectName: 'my-lib',
newProjectName: 'my-destination',
destination: 'my-destination',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
const importerFile = tree.read(
'my-importer/src/lib/my-importing-file.ts',
'utf-8'
);
expect(importerFile).toContain(
`import { MyDestinationModule } from '@proj/my-destination';`
);
expect(importerFile).toContain(
`export class MyExtendedLibModule extends MyDestinationModule {}`
);
});
it('should update the index.ts file which exports the module', async () => {
addProjectToGraph('my-lib');
await angularMoveGenerator(tree, {
projectName: 'my-lib',
newProjectName: 'my-destination',
destination: 'my-destination',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
const indexFile = tree.read('my-destination/src/index.ts', '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-lib-demo',
buildable: false,
linter: Linter.EsLint,
publishable: false,
simpleName: true,
skipFormat: false,
unitTestRunner: UnitTestRunner.Jest,
});
addProjectToGraph('my-lib');
await angularMoveGenerator(tree, {
projectName: 'my-lib',
newProjectName: 'my-destination',
destination: 'my-destination',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
});
const moduleFile = tree.read(
'my-lib-demo/src/lib/my-lib-demo.module.ts',
'utf-8'
);
expect(moduleFile).toContain(`export class MyLibDemoModule {}`);
});
});
it('should move project correctly when --project-name-and-root-format=derived', async () => {
await generateTestLibrary(tree, { name: 'mylib2', buildable: true });
addProjectToGraph('mylib2');
await angularMoveGenerator(tree, {
projectName: 'mylib2',
destination: 'mynewlib',
updateImportPath: true,
projectNameAndRootFormat: 'derived',
});
expect(tree.exists('libs/mynewlib/src/lib/mynewlib.module.ts')).toEqual(
true
);
const ngPackageJson = readJson(tree, 'libs/mynewlib/ng-package.json');
expect(ngPackageJson.dest).toEqual('../../dist/libs/mynewlib');
});
});

View File

@ -1,32 +1,21 @@
import { formatFiles, Tree } from '@nx/devkit';
import { moveGenerator } from '@nx/workspace/generators';
import {
normalizeSchema,
updateModuleName,
updateNgPackage,
updateSecondaryEntryPoints,
} from './lib';
import type { Tree } from '@nx/devkit';
import { moveGeneratorInternal } from '@nx/workspace/src/generators/move/move';
import type { Schema } from './schema';
/**
* Moves an Angular lib/app to another folder (and renames it in the process)
*
* @remarks It's important to note that `updateModuleName` is done after the update
* to the workspace, so it can't use the same tricks as the `@nx/workspace` rules
* to get the before and after names and paths.
*/
export async function angularMoveGenerator(
tree: Tree,
schema: Schema
): Promise<void> {
const normalizedSchema = normalizeSchema(tree, schema);
await moveGenerator(tree, { ...schema, skipFormat: true });
updateModuleName(tree, normalizedSchema);
updateNgPackage(tree, normalizedSchema);
updateSecondaryEntryPoints(tree, normalizedSchema);
if (!normalizedSchema.skipFormat) {
await formatFiles(tree);
}
await angularMoveGeneratorInternal(tree, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function angularMoveGeneratorInternal(
tree: Tree,
schema: Schema
): Promise<void> {
process.env.NX_ANGULAR_MOVE_INVOKED = 'true';
await moveGeneratorInternal(tree, schema);
}

View File

@ -1,12 +1,11 @@
import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
export interface Schema {
projectName: string;
destination: string;
updateImportPath: boolean;
importPath?: string;
skipFormat?: boolean;
}
export interface NormalizedSchema extends Schema {
oldProjectRoot: string;
newProjectName: string;
newProjectName?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
}

View File

@ -19,6 +19,13 @@
"x-dropdown": "projects",
"x-priority": "important"
},
"newProjectName": {
"type": "string",
"alias": "project",
"description": "The new name of the project after the move.",
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
"x-priority": "important"
},
"destination": {
"type": "string",
"description": "The folder to move the Angular project into.",
@ -28,6 +35,11 @@
},
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the new project name and destination as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"importPath": {
"type": "string",
"description": "The new import path to use in the `tsconfig.base.json`."

View File

@ -47,7 +47,7 @@
"hidden": true
},
"move": {
"factory": "./src/generators/move/move#moveGenerator",
"factory": "./src/generators/move/move#moveGeneratorInternal",
"schema": "./src/generators/move/schema.json",
"aliases": ["mv"],
"description": "Move an application or library to another folder."

View File

@ -61,12 +61,13 @@
}
},
"dependencies": {
"@nx/devkit": "file:../devkit",
"chalk": "^4.1.0",
"enquirer": "~2.3.6",
"ignore": "^5.0.4",
"rxjs": "^7.8.0",
"tslib": "^2.3.0",
"yargs-parser": "21.1.1",
"@nx/devkit": "file:../devkit"
"yargs-parser": "21.1.1"
},
"publishConfig": {
"access": "public"

View File

@ -45,8 +45,8 @@ export async function monorepoGenerator(tree: Tree, options: {}) {
libsDir,
project.root === '.' ? project.name : project.root
),
destinationRelativeToRoot: true,
updateImportPath: project.projectType === 'library',
projectNameAndRootFormat: 'as-provided',
});
}
}

View File

@ -4,7 +4,7 @@ import {
Tree,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Schema } from '../schema';
import { NormalizedSchema } from '../schema';
import { checkDestination } from './check-destination';
// nx-ignore-next-line
@ -16,20 +16,24 @@ describe('checkDestination', () => {
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await libraryGenerator(tree, { name: 'my-lib' });
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
projectConfig = readProjectConfiguration(tree, 'my-lib');
});
it('should throw an error if the path is not explicit', async () => {
const schema: Schema = {
const schema: NormalizedSchema = {
projectName: 'my-lib',
destination: '../apps/not-an-app',
importPath: undefined,
updateImportPath: true,
relativeToRootDestination: '',
};
expect(() => {
checkDestination(tree, schema, projectConfig);
checkDestination(tree, schema, schema.destination);
}).toThrow(
`Invalid destination: [${schema.destination}] - Please specify explicit path.`
);
@ -38,32 +42,35 @@ describe('checkDestination', () => {
it('should throw an error if the path already exists', async () => {
await libraryGenerator(tree, {
name: 'my-other-lib',
projectNameAndRootFormat: 'as-provided',
});
const schema: Schema = {
const schema: NormalizedSchema = {
projectName: 'my-lib',
destination: 'my-other-lib',
importPath: undefined,
updateImportPath: true,
relativeToRootDestination: 'my-other-lib',
};
expect(() => {
checkDestination(tree, schema, projectConfig);
checkDestination(tree, schema, schema.destination);
}).toThrow(
`Invalid destination: [${schema.destination}] - Path is not empty.`
);
});
it('should NOT throw an error if the path is available', async () => {
const schema: Schema = {
const schema: NormalizedSchema = {
projectName: 'my-lib',
destination: 'my-other-lib',
importPath: undefined,
updateImportPath: true,
relativeToRootDestination: 'my-other-lib',
};
expect(() => {
checkDestination(tree, schema, projectConfig);
checkDestination(tree, schema, schema.destination);
}).not.toThrow();
});
});

View File

@ -1,6 +1,5 @@
import { ProjectConfiguration, Tree } from '@nx/devkit';
import { Schema } from '../schema';
import { getDestination } from './utils';
import type { Tree } from '@nx/devkit';
import type { NormalizedSchema } from '../schema';
/**
* Checks whether the destination folder is valid
@ -12,18 +11,16 @@ import { getDestination } from './utils';
*/
export function checkDestination(
tree: Tree,
schema: Schema,
projectConfig: ProjectConfiguration
schema: NormalizedSchema,
providedDestination: string
) {
const INVALID_DESTINATION = `Invalid destination: [${schema.destination}]`;
const INVALID_DESTINATION = `Invalid destination: [${providedDestination}]`;
if (schema.destination.includes('..')) {
if (providedDestination.includes('..')) {
throw new Error(`${INVALID_DESTINATION} - Please specify explicit path.`);
}
const destination = getDestination(tree, schema, projectConfig);
if (tree.children(destination).length > 0) {
if (tree.children(schema.relativeToRootDestination).length > 0) {
throw new Error(`${INVALID_DESTINATION} - Path is not empty.`);
}
}

View File

@ -16,7 +16,10 @@ describe('moveProject', () => {
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await libraryGenerator(tree, { name: 'my-lib' });
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
projectConfig = readProjectConfiguration(tree, 'my-lib');
});
@ -27,14 +30,13 @@ describe('moveProject', () => {
importPath: '@proj/my-destination',
updateImportPath: true,
newProjectName: 'my-destination',
relativeToRootDestination: 'libs/my-destination',
relativeToRootDestination: 'my-destination',
};
moveProjectFiles(tree, schema, projectConfig);
const destinationChildren = tree.children('libs/my-destination');
const destinationChildren = tree.children('my-destination');
expect(destinationChildren.length).toBeGreaterThan(0);
expect(tree.exists('libs/my-lib')).toBeFalsy();
expect(tree.children('libs')).not.toContain('my-lib');
expect(tree.exists('my-lib')).toBeFalsy();
});
});

View File

@ -31,7 +31,7 @@ describe('normalizeSchema', () => {
projectConfiguration = readProjectConfiguration(tree, schema.projectName);
});
it('should calculate importPath, projectName and relativeToRootDestination correctly', () => {
it('should calculate importPath, projectName and relativeToRootDestination correctly', async () => {
const expected: NormalizedSchema = {
destination: 'my/library',
importPath: '@proj/my/library',
@ -41,12 +41,12 @@ describe('normalizeSchema', () => {
updateImportPath: true,
};
const result = normalizeSchema(tree, schema, projectConfiguration);
const result = await normalizeSchema(tree, schema, projectConfiguration);
expect(result).toEqual(expected);
});
it('should normalize destination and derive projectName correctly', () => {
it('should normalize destination and derive projectName correctly', async () => {
const expected: NormalizedSchema = {
destination: 'my/library',
importPath: '@proj/my/library',
@ -56,7 +56,7 @@ describe('normalizeSchema', () => {
updateImportPath: true,
};
const result = normalizeSchema(
const result = await normalizeSchema(
tree,
{ ...schema, destination: './my/library' },
projectConfiguration
@ -65,7 +65,7 @@ describe('normalizeSchema', () => {
expect(result).toEqual(expected);
});
it('should use provided import path', () => {
it('should use provided import path', async () => {
const expected: NormalizedSchema = {
destination: 'my/library',
importPath: '@proj/my-awesome-library',
@ -75,7 +75,7 @@ describe('normalizeSchema', () => {
updateImportPath: true,
};
const result = normalizeSchema(
const result = await normalizeSchema(
tree,
{ ...schema, importPath: expected.importPath },
projectConfiguration
@ -90,7 +90,7 @@ describe('normalizeSchema', () => {
return json;
});
const result = normalizeSchema(tree, schema, projectConfiguration);
const result = await normalizeSchema(tree, schema, projectConfiguration);
expect(result.relativeToRootDestination).toEqual('packages/my/library');
});

View File

@ -1,32 +1,267 @@
import { ProjectConfiguration, Tree } from '@nx/devkit';
import {
ProjectConfiguration,
Tree,
logger,
names,
readNxJson,
stripIndents,
updateNxJson,
} from '@nx/devkit';
import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { prompt } from 'enquirer';
import { getImportPath, getNpmScope } from '../../../utilities/get-import-path';
import type { NormalizedSchema, Schema } from '../schema';
import {
getDestination,
getNewProjectName,
normalizePathSlashes,
} from './utils';
import { getImportPath } from '../../../utilities/get-import-path';
export function normalizeSchema(
export async function normalizeSchema(
tree: Tree,
schema: Schema,
projectConfiguration: ProjectConfiguration
): NormalizedSchema {
const destination = normalizePathSlashes(schema.destination);
const newProjectName =
schema.newProjectName ?? getNewProjectName(destination);
return {
...schema,
destination,
importPath:
schema.importPath ??
normalizePathSlashes(getImportPath(tree, destination)),
newProjectName,
relativeToRootDestination: getDestination(
): Promise<NormalizedSchema> {
const { destination, newProjectName, importPath } =
await determineProjectNameAndRootOptions(
tree,
schema,
projectConfiguration
),
);
return {
...schema,
destination: normalizePathSlashes(schema.destination),
importPath,
newProjectName,
relativeToRootDestination: destination,
};
}
type ProjectNameAndRootOptions = {
destination: string;
newProjectName: string;
importPath?: string;
};
type ProjectNameAndRootFormats = {
'as-provided': ProjectNameAndRootOptions;
derived?: ProjectNameAndRootOptions;
};
async function determineProjectNameAndRootOptions(
tree: Tree,
options: Schema,
projectConfiguration: ProjectConfiguration
): Promise<ProjectNameAndRootOptions> {
validateName(
options.newProjectName,
options.projectNameAndRootFormat,
projectConfiguration
);
const formats = getProjectNameAndRootFormats(
tree,
options,
projectConfiguration
);
const format =
options.projectNameAndRootFormat ??
(await determineFormat(tree, formats, options));
return formats[format];
}
function validateName(
name: string | undefined,
projectNameAndRootFormat: ProjectNameAndRootFormat | undefined,
projectConfiguration: ProjectConfiguration
): void {
if (!name) {
return;
}
if (projectNameAndRootFormat === 'derived' && name.startsWith('@')) {
throw new Error(
`The new project name "${name}" cannot start with "@" when the "projectNameAndRootFormat" is "derived".`
);
}
/**
* Matches two types of project names:
*
* 1. Valid npm package names (e.g., '@scope/name' or 'name').
* 2. Names starting with a letter and can contain any character except whitespace and ':'.
*
* The second case is to support the legacy behavior (^[a-zA-Z].*$) with the difference
* that it doesn't allow the ":" character. It was wrong to allow it because it would
* conflict with the notation for tasks.
*/
const libraryPattern =
'(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$';
const appPattern = '^[a-zA-Z][^:]*$';
if (projectConfiguration.projectType === 'application') {
const validationRegex = new RegExp(appPattern);
if (!validationRegex.test(name)) {
throw new Error(
`The new project name should match the pattern "${appPattern}". The provided value "${name}" does not match.`
);
}
} else if (projectConfiguration.projectType === 'library') {
const validationRegex = new RegExp(libraryPattern);
if (!validationRegex.test(name)) {
throw new Error(
`The new project name should match the pattern "${libraryPattern}". The provided value "${name}" does not match.`
);
}
}
}
function getProjectNameAndRootFormats(
tree: Tree,
schema: Schema,
projectConfiguration: ProjectConfiguration
): ProjectNameAndRootFormats {
let destination = normalizePathSlashes(schema.destination);
const normalizedNewProjectName = schema.newProjectName
? names(schema.newProjectName).fileName
: undefined;
const asProvidedProjectName = normalizedNewProjectName ?? schema.projectName;
const asProvidedDestination = destination;
if (normalizedNewProjectName?.startsWith('@')) {
return {
'as-provided': {
destination: asProvidedDestination,
importPath:
schema.importPath ??
// keep the existing import path if the name didn't change
(normalizedNewProjectName &&
schema.projectName !== normalizedNewProjectName
? asProvidedProjectName
: undefined),
newProjectName: asProvidedProjectName,
},
};
}
let npmScope: string;
let asProvidedImportPath = schema.importPath;
if (
!asProvidedImportPath &&
schema.newProjectName &&
projectConfiguration.projectType === 'library'
) {
npmScope = getNpmScope(tree);
asProvidedImportPath = npmScope
? `${npmScope === '@' ? '' : '@'}${npmScope}/${asProvidedProjectName}`
: asProvidedProjectName;
}
const derivedProjectName =
schema.newProjectName ?? getNewProjectName(destination);
const derivedDestination = getDestination(tree, schema, projectConfiguration);
let derivedImportPath: string;
if (projectConfiguration.projectType === 'library') {
derivedImportPath =
schema.importPath ??
normalizePathSlashes(getImportPath(tree, destination));
}
return {
'as-provided': {
destination: asProvidedDestination,
newProjectName: asProvidedProjectName,
importPath: asProvidedImportPath,
},
derived: {
destination: derivedDestination,
newProjectName: derivedProjectName,
importPath: derivedImportPath,
},
};
}
async function determineFormat(
tree: Tree,
formats: ProjectNameAndRootFormats,
schema: Schema
): Promise<ProjectNameAndRootFormat> {
if (!formats.derived) {
return 'as-provided';
}
if (process.env.NX_INTERACTIVE !== 'true' || !isTTY()) {
return 'derived';
}
const asProvidedDescription = `As provided:
Name: ${formats['as-provided'].newProjectName}
Destination: ${formats['as-provided'].destination}`;
const asProvidedSelectedValue = `${formats['as-provided'].newProjectName} @ ${formats['as-provided'].destination}`;
const derivedDescription = `Derived:
Name: ${formats['derived'].newProjectName}
Destination: ${formats['derived'].destination}`;
const derivedSelectedValue = `${formats['derived'].newProjectName} @ ${formats['derived'].destination}`;
const result = await prompt<{ format: ProjectNameAndRootFormat }>({
type: 'select',
name: 'format',
message:
'What should be the new project name and where should it be moved to?',
choices: [
{
message: asProvidedDescription,
name: asProvidedSelectedValue,
},
{
message: derivedDescription,
name: derivedSelectedValue,
},
],
initial: 'as-provided' as any,
}).then(({ format }) =>
format === asProvidedSelectedValue ? 'as-provided' : 'derived'
);
const callingGenerator =
process.env.NX_ANGULAR_MOVE_INVOKED === 'true'
? '@nx/angular:move'
: '@nx/workspace:move';
const deprecationWarning = stripIndents`
In Nx 18, the project name and destination will no longer be derived.
Please provide the exact new project name and destination in the future.`;
if (result === 'as-provided') {
const { saveDefault } = await prompt<{ saveDefault: boolean }>({
type: 'confirm',
message: `Would you like to configure Nx to always take the project name and destination as provided for ${callingGenerator}?`,
name: 'saveDefault',
initial: true,
});
if (saveDefault) {
const nxJson = readNxJson(tree);
nxJson.generators ??= {};
nxJson.generators[callingGenerator] ??= {};
nxJson.generators[callingGenerator].projectNameAndRootFormat = result;
updateNxJson(tree, nxJson);
} else {
logger.warn(deprecationWarning);
}
} else {
const example =
`Example: nx g ${callingGenerator} --projectName ${schema.projectName} --destination ${formats[result].destination}` +
(schema.projectName !== formats[result].newProjectName
? ` --newProjectName ${formats[result].newProjectName}`
: '');
logger.warn(deprecationWarning + '\n' + example);
}
return result;
}
function isTTY(): boolean {
return !!process.stdout.isTTY && process.env['CI'] !== 'true';
}

View File

@ -0,0 +1,24 @@
import type { Tree } from '@nx/devkit';
import type { Schema } from '../schema';
type PluginOptions = {
oldProjectName: string;
newProjectName: string;
};
export async function runAngularPlugin(tree: Tree, schema: Schema) {
let move: (tree: Tree, schema: PluginOptions) => Promise<void>;
try {
// nx-ignore-next-line
move = require('@nx/angular/src/generators/move/move-impl').move;
} catch {}
if (!move) {
return;
}
await move(tree, {
oldProjectName: schema.projectName,
newProjectName: schema.newProjectName,
});
}

View File

@ -24,11 +24,14 @@ describe('updateCypressConfig', () => {
importPath: '@proj/my-destination',
updateImportPath: true,
newProjectName: 'my-destination',
relativeToRootDestination: 'libs/my-destination',
relativeToRootDestination: 'my-destination',
};
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await libraryGenerator(tree, { name: 'my-lib' });
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
projectConfig = readProjectConfiguration(tree, 'my-lib');
});
@ -46,18 +49,18 @@ describe('updateCypressConfig', () => {
pluginsFile: './src/plugins/index',
supportFile: false,
video: true,
videosFolder: '../../dist/cypress/libs/my-lib/videos',
screenshotsFolder: '../../dist/cypress/libs/my-lib/screenshots',
videosFolder: '../../dist/cypress/my-lib/videos',
screenshotsFolder: '../../dist/cypress/my-lib/screenshots',
chromeWebSecurity: false,
};
writeJson(tree, '/libs/my-destination/cypress.json', cypressJson);
writeJson(tree, 'my-destination/cypress.json', cypressJson);
updateCypressConfig(tree, schema, projectConfig);
expect(readJson(tree, '/libs/my-destination/cypress.json')).toEqual({
expect(readJson(tree, 'my-destination/cypress.json')).toEqual({
...cypressJson,
videosFolder: '../../dist/cypress/libs/my-destination/videos',
screenshotsFolder: '../../dist/cypress/libs/my-destination/screenshots',
videosFolder: '../../dist/cypress/my-destination/videos',
screenshotsFolder: '../../dist/cypress/my-destination/screenshots',
});
});
@ -71,18 +74,16 @@ describe('updateCypressConfig', () => {
video: false,
chromeWebSecurity: false,
};
writeJson(tree, '/libs/my-destination/cypress.json', cypressJson);
writeJson(tree, 'my-destination/cypress.json', cypressJson);
updateCypressConfig(tree, schema, projectConfig);
expect(readJson(tree, '/libs/my-destination/cypress.json')).toEqual(
cypressJson
);
expect(readJson(tree, 'my-destination/cypress.json')).toEqual(cypressJson);
});
it('should handle updating cypress.config.ts', async () => {
tree.write(
'/libs/my-destination/cypress.config.ts',
'my-destination/cypress.config.ts',
`
import { defineConfig } from 'cypress';
import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
@ -90,23 +91,20 @@ import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
export default defineConfig({
e2e: {
nxE2EPreset(__dirname),
videosFolder: '../../dist/cypress/libs/my-lib/videos',
screenshotsFolder: '../../dist/cypress/libs/my-lib/screenshots',
videosFolder: '../../dist/cypress/my-lib/videos',
screenshotsFolder: '../../dist/cypress/my-lib/screenshots',
}
});
`
);
updateCypressConfig(tree, schema, projectConfig);
const fileContent = tree.read(
'/libs/my-destination/cypress.config.ts',
'utf-8'
const fileContent = tree.read('my-destination/cypress.config.ts', 'utf-8');
expect(fileContent).toContain(
`videosFolder: '../../dist/cypress/my-destination/videos'`
);
expect(fileContent).toContain(
`videosFolder: '../../dist/cypress/libs/my-destination/videos'`
);
expect(fileContent).toContain(
`screenshotsFolder: '../../dist/cypress/libs/my-destination/screenshots'`
`screenshotsFolder: '../../dist/cypress/my-destination/screenshots'`
);
});
});

View File

@ -25,7 +25,7 @@ describe('updateEslint', () => {
importPath: '@proj/shared-my-destination',
updateImportPath: true,
newProjectName: 'shared-my-destination',
relativeToRootDestination: 'libs/shared/my-destination',
relativeToRootDestination: 'shared/my-destination',
};
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -35,6 +35,7 @@ describe('updateEslint', () => {
await libraryGenerator(tree, {
name: 'my-lib',
linter: Linter.None,
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-lib');
@ -48,33 +49,33 @@ describe('updateEslint', () => {
await libraryGenerator(tree, {
name: 'my-lib',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
});
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/.eslintrc.json',
'libs/shared/my-destination/.eslintrc.json'
'my-lib/.eslintrc.json',
'shared/my-destination/.eslintrc.json'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
updateEslintConfig(tree, schema, projectConfig);
expect(
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
).toEqual(
expect(readJson(tree, 'shared/my-destination/.eslintrc.json')).toEqual(
expect.objectContaining({
extends: ['../../../.eslintrc.json'],
extends: ['../../.eslintrc.json'],
})
);
});
it('should update .eslintrc.json extends path when project is moved from subdirectory', async () => {
await libraryGenerator(tree, {
name: 'test',
directory: 'api',
name: 'api-test',
directory: 'api/test',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
});
// This step is usually handled elsewhere
tree.rename('libs/api/test/.eslintrc.json', 'libs/test/.eslintrc.json');
tree.rename('api/test/.eslintrc.json', 'test/.eslintrc.json');
const projectConfig = readProjectConfiguration(tree, 'api-test');
const newSchema = {
@ -83,14 +84,14 @@ describe('updateEslint', () => {
importPath: '@proj/test',
updateImportPath: true,
newProjectName: 'test',
relativeToRootDestination: 'libs/test',
relativeToRootDestination: 'test',
};
updateEslintConfig(tree, newSchema, projectConfig);
expect(readJson(tree, '/libs/test/.eslintrc.json')).toEqual(
expect(readJson(tree, 'test/.eslintrc.json')).toEqual(
expect.objectContaining({
extends: ['../../.eslintrc.json'],
extends: ['../.eslintrc.json'],
})
);
});
@ -99,31 +100,30 @@ describe('updateEslint', () => {
await libraryGenerator(tree, {
name: 'my-lib',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
});
updateJson(tree, 'libs/my-lib/.eslintrc.json', (eslintRcJson) => {
updateJson(tree, 'my-lib/.eslintrc.json', (eslintRcJson) => {
eslintRcJson.extends = [
'plugin:@nx/react',
'../../.eslintrc.json',
'../.eslintrc.json',
'./customrc.json',
];
return eslintRcJson;
});
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/.eslintrc.json',
'libs/shared/my-destination/.eslintrc.json'
'my-lib/.eslintrc.json',
'shared/my-destination/.eslintrc.json'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
updateEslintConfig(tree, schema, projectConfig);
expect(
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
).toEqual(
expect(readJson(tree, 'shared/my-destination/.eslintrc.json')).toEqual(
expect.objectContaining({
extends: [
'plugin:@nx/react',
'../../../.eslintrc.json',
'../../.eslintrc.json',
'./customrc.json',
],
})
@ -135,24 +135,23 @@ describe('updateEslint', () => {
name: 'my-lib',
linter: Linter.EsLint,
setParserOptionsProject: true,
projectNameAndRootFormat: 'as-provided',
});
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/.eslintrc.json',
'libs/shared/my-destination/.eslintrc.json'
'my-lib/.eslintrc.json',
'shared/my-destination/.eslintrc.json'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
updateEslintConfig(tree, schema, projectConfig);
expect(
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
).toEqual(
expect(readJson(tree, 'shared/my-destination/.eslintrc.json')).toEqual(
expect.objectContaining({
overrides: expect.arrayContaining([
expect.objectContaining({
parserOptions: expect.objectContaining({
project: ['libs/shared/my-destination/tsconfig.*?.json'],
project: ['shared/my-destination/tsconfig.*?.json'],
}),
}),
]),
@ -165,36 +164,35 @@ describe('updateEslint', () => {
name: 'my-lib',
linter: Linter.EsLint,
setParserOptionsProject: true,
projectNameAndRootFormat: 'as-provided',
});
// Add another parser project to eslint.json
const storybookProject = '.storybook/tsconfig.json';
updateJson(tree, '/libs/my-lib/.eslintrc.json', (eslintRcJson) => {
updateJson(tree, 'my-lib/.eslintrc.json', (eslintRcJson) => {
eslintRcJson.overrides[0].parserOptions.project.push(
`libs/my-lib/${storybookProject}`
`my-lib/${storybookProject}`
);
return eslintRcJson;
});
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/.eslintrc.json',
'libs/shared/my-destination/.eslintrc.json'
'my-lib/.eslintrc.json',
'shared/my-destination/.eslintrc.json'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
updateEslintConfig(tree, schema, projectConfig);
expect(
readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
).toEqual(
expect(readJson(tree, 'shared/my-destination/.eslintrc.json')).toEqual(
expect.objectContaining({
overrides: expect.arrayContaining([
expect.objectContaining({
parserOptions: expect.objectContaining({
project: [
'libs/shared/my-destination/tsconfig.*?.json',
`libs/shared/my-destination/${storybookProject}`,
'shared/my-destination/tsconfig.*?.json',
`shared/my-destination/${storybookProject}`,
],
}),
}),
@ -208,28 +206,29 @@ describe('updateEslint', () => {
name: 'my-lib',
linter: Linter.EsLint,
setParserOptionsProject: true,
projectNameAndRootFormat: 'as-provided',
});
// Add another parser project to eslint.json
const storybookProject = '.storybook/tsconfig.json';
updateJson(tree, '/libs/my-lib/.eslintrc.json', (eslintRcJson) => {
eslintRcJson.overrides[0].parserOptions.project = `libs/my-lib/${storybookProject}`;
updateJson(tree, 'my-lib/.eslintrc.json', (eslintRcJson) => {
eslintRcJson.overrides[0].parserOptions.project = `my-lib/${storybookProject}`;
return eslintRcJson;
});
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/.eslintrc.json',
'libs/shared/my-destination/.eslintrc.json'
'my-lib/.eslintrc.json',
'shared/my-destination/.eslintrc.json'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
updateEslintConfig(tree, schema, projectConfig);
expect(
readJson(tree, '/libs/shared/my-destination/.eslintrc.json').overrides[0]
readJson(tree, 'shared/my-destination/.eslintrc.json').overrides[0]
.parserOptions
).toEqual({ project: `libs/shared/my-destination/${storybookProject}` });
).toEqual({ project: `shared/my-destination/${storybookProject}` });
});
});
@ -244,7 +243,7 @@ describe('updateEslint (flat config)', () => {
importPath: '@proj/shared-my-destination',
updateImportPath: true,
newProjectName: 'shared-my-destination',
relativeToRootDestination: 'libs/shared/my-destination',
relativeToRootDestination: 'shared/my-destination',
};
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -256,6 +255,7 @@ describe('updateEslint (flat config)', () => {
await libraryGenerator(tree, {
name: 'my-lib',
linter: Linter.None,
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-lib');
@ -269,31 +269,33 @@ describe('updateEslint (flat config)', () => {
await libraryGenerator(tree, {
name: 'my-lib',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
});
convertToFlat(tree, 'libs/my-lib');
convertToFlat(tree, 'my-lib');
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/eslint.config.js',
'libs/shared/my-destination/eslint.config.js'
'my-lib/eslint.config.js',
'shared/my-destination/eslint.config.js'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
updateEslintConfig(tree, schema, projectConfig);
expect(
tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8')
).toEqual(expect.stringContaining(`require('../../../eslint.config.js')`));
tree.read('shared/my-destination/eslint.config.js', 'utf-8')
).toEqual(expect.stringContaining(`require('../../eslint.config.js')`));
});
it('should update config extends path when project is moved from subdirectory', async () => {
await libraryGenerator(tree, {
name: 'test',
directory: 'api',
name: 'api-test',
directory: 'api/test',
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
});
convertToFlat(tree, 'libs/api/test');
convertToFlat(tree, 'api/test');
// This step is usually handled elsewhere
tree.rename('libs/api/test/eslint.config.js', 'libs/test/eslint.config.js');
tree.rename('api/test/eslint.config.js', 'test/eslint.config.js');
const projectConfig = readProjectConfiguration(tree, 'api-test');
@ -303,13 +305,13 @@ describe('updateEslint (flat config)', () => {
importPath: '@proj/test',
updateImportPath: true,
newProjectName: 'test',
relativeToRootDestination: 'libs/test',
relativeToRootDestination: 'test',
};
updateEslintConfig(tree, newSchema, projectConfig);
expect(tree.read('libs/test/eslint.config.js', 'utf-8')).toEqual(
expect.stringContaining(`require('../../eslint.config.js')`)
expect(tree.read('test/eslint.config.js', 'utf-8')).toEqual(
expect.stringContaining(`require('../eslint.config.js')`)
);
});
@ -318,22 +320,23 @@ describe('updateEslint (flat config)', () => {
name: 'my-lib',
linter: Linter.EsLint,
setParserOptionsProject: true,
projectNameAndRootFormat: 'as-provided',
});
convertToFlat(tree, 'libs/my-lib', { hasParser: true });
convertToFlat(tree, 'my-lib', { hasParser: true });
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/eslint.config.js',
'libs/shared/my-destination/eslint.config.js'
'my-lib/eslint.config.js',
'shared/my-destination/eslint.config.js'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
updateEslintConfig(tree, schema, projectConfig);
expect(
tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8')
tree.read('shared/my-destination/eslint.config.js', 'utf-8')
).toEqual(
expect.stringContaining(
`project: ["libs/shared/my-destination/tsconfig.*?.json"]`
`project: ["shared/my-destination/tsconfig.*?.json"]`
)
);
});
@ -343,27 +346,28 @@ describe('updateEslint (flat config)', () => {
name: 'my-lib',
linter: Linter.EsLint,
setParserOptionsProject: true,
projectNameAndRootFormat: 'as-provided',
});
// Add another parser project to eslint.json
const storybookProject = '.storybook/tsconfig.json';
convertToFlat(tree, 'libs/my-lib', {
convertToFlat(tree, 'my-lib', {
hasParser: true,
anotherProject: storybookProject,
});
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/eslint.config.js',
'libs/shared/my-destination/eslint.config.js'
'my-lib/eslint.config.js',
'shared/my-destination/eslint.config.js'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
updateEslintConfig(tree, schema, projectConfig);
expect(
tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8')
tree.read('shared/my-destination/eslint.config.js', 'utf-8')
).toEqual(
expect.stringContaining(
`project: ["libs/shared/my-destination/tsconfig.*?.json", "libs/shared/my-destination/${storybookProject}"]`
`project: ["shared/my-destination/tsconfig.*?.json", "shared/my-destination/${storybookProject}"]`
)
);
});
@ -373,23 +377,24 @@ describe('updateEslint (flat config)', () => {
name: 'my-lib',
linter: Linter.EsLint,
setParserOptionsProject: true,
projectNameAndRootFormat: 'as-provided',
});
convertToFlat(tree, 'libs/my-lib', { hasParser: true, isString: true });
convertToFlat(tree, 'my-lib', { hasParser: true, isString: true });
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/eslint.config.js',
'libs/shared/my-destination/eslint.config.js'
'my-lib/eslint.config.js',
'shared/my-destination/eslint.config.js'
);
const projectConfig = readProjectConfiguration(tree, 'my-lib');
updateEslintConfig(tree, schema, projectConfig);
expect(
tree.read('libs/shared/my-destination/eslint.config.js', 'utf-8')
tree.read('shared/my-destination/eslint.config.js', 'utf-8')
).toEqual(
expect.stringContaining(
`project: "libs/shared/my-destination/tsconfig.*?.json"`
`project: "shared/my-destination/tsconfig.*?.json"`
)
);
});

View File

@ -21,8 +21,10 @@ describe('updateImports', () => {
schema = {
projectName: 'my-source',
newProjectName: 'my-destination',
destination: 'my-destination',
updateImportPath: true,
projectNameAndRootFormat: 'as-provided',
};
});
@ -33,14 +35,17 @@ describe('updateImports', () => {
await libraryGenerator(tree, {
name: 'my-destination',
config: 'project',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'my-importer',
projectNameAndRootFormat: 'as-provided',
});
const importerFilePath = 'libs/my-importer/src/importer.ts';
const importerFilePath = 'my-importer/src/importer.ts';
tree.write(
importerFilePath,
`
@ -53,7 +58,7 @@ describe('updateImports', () => {
updateImports(
tree,
normalizeSchema(tree, schema, projectConfig),
await normalizeSchema(tree, schema, projectConfig),
projectConfig
);
@ -66,12 +71,19 @@ describe('updateImports', () => {
* be updated.
*/
it('should not update import paths when they contain a partial match', async () => {
await libraryGenerator(tree, { name: 'table' });
await libraryGenerator(tree, { name: 'tab' });
await libraryGenerator(tree, {
name: 'table',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'tab',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'my-importer',
projectNameAndRootFormat: 'as-provided',
});
const importerFilePath = 'libs/my-importer/src/importer.ts';
const importerFilePath = 'my-importer/src/importer.ts';
tree.write(
importerFilePath,
`
@ -92,7 +104,7 @@ describe('updateImports', () => {
importPath: '@proj/tabs',
updateImportPath: true,
newProjectName: 'tabs',
relativeToRootDestination: 'libs/tabs',
relativeToRootDestination: 'tabs',
},
projectConfig
);
@ -107,12 +119,19 @@ describe('updateImports', () => {
});
it('should correctly update deep imports', async () => {
await libraryGenerator(tree, { name: 'table' });
await libraryGenerator(tree, { name: 'tab' });
await libraryGenerator(tree, {
name: 'table',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'tab',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'my-importer',
projectNameAndRootFormat: 'as-provided',
});
const importerFilePath = 'libs/my-importer/src/importer.ts';
const importerFilePath = 'my-importer/src/importer.ts';
tree.write(
importerFilePath,
`
@ -133,7 +152,7 @@ describe('updateImports', () => {
importPath: '@proj/tabs',
updateImportPath: true,
newProjectName: 'tabs',
relativeToRootDestination: 'libs/tabs',
relativeToRootDestination: 'tabs',
},
projectConfig
);
@ -148,12 +167,19 @@ describe('updateImports', () => {
});
it('should update dynamic imports', async () => {
await libraryGenerator(tree, { name: 'table' });
await libraryGenerator(tree, { name: 'tab' });
await libraryGenerator(tree, {
name: 'table',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'tab',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'my-importer',
projectNameAndRootFormat: 'as-provided',
});
const importerFilePath = 'libs/my-importer/src/importer.ts';
const importerFilePath = 'my-importer/src/importer.ts';
tree.write(
importerFilePath,
`
@ -173,7 +199,7 @@ describe('updateImports', () => {
importPath: '@proj/tabs',
updateImportPath: true,
newProjectName: 'tabs',
relativeToRootDestination: 'libs/tabs',
relativeToRootDestination: 'tabs',
},
projectConfig
);
@ -194,12 +220,19 @@ describe('updateImports', () => {
});
it('should update require imports', async () => {
await libraryGenerator(tree, { name: 'table' });
await libraryGenerator(tree, { name: 'tab' });
await libraryGenerator(tree, {
name: 'table',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'tab',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'my-importer',
projectNameAndRootFormat: 'as-provided',
});
const importerFilePath = 'libs/my-importer/src/importer.ts';
const importerFilePath = 'my-importer/src/importer.ts';
tree.write(
importerFilePath,
`
@ -219,7 +252,7 @@ describe('updateImports', () => {
importPath: '@proj/tabs',
updateImportPath: true,
newProjectName: 'tabs',
relativeToRootDestination: 'libs/tabs',
relativeToRootDestination: 'tabs',
},
projectConfig
);
@ -244,14 +277,17 @@ describe('updateImports', () => {
// source and destination to make sure that the workspace has libraries with those names.
await libraryGenerator(tree, {
name: 'my-destination',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
await libraryGenerator(tree, {
name: 'my-importer',
projectNameAndRootFormat: 'as-provided',
});
const importerFilePath = 'libs/my-importer/src/importer.ts';
const importerFilePath = 'my-importer/src/importer.ts';
tree.write(
importerFilePath,
`import { MyClass } from '@proj/my-source';
@ -263,7 +299,7 @@ export MyExtendedClass extends MyClass {};`
updateImports(
tree,
normalizeSchema(
await normalizeSchema(
tree,
{
...schema,
@ -282,31 +318,33 @@ export MyExtendedClass extends MyClass {};`
it('should update project ref in the root tsconfig.base.json', async () => {
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
updateImports(
tree,
normalizeSchema(tree, schema, projectConfig),
await 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': ['my-destination/src/index.ts'],
});
});
it('should update project ref in the root tsconfig.base.json for secondary entry points', async () => {
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
updateJson(tree, '/tsconfig.base.json', (json) => {
json.compilerOptions.paths['@proj/my-source/testing'] = [
'libs/my-source/testing/src/index.ts',
'my-source/testing/src/index.ts',
];
json.compilerOptions.paths['@proj/different-alias'] = [
'libs/my-source/some-path/src/index.ts',
'my-source/some-path/src/index.ts',
];
return json;
});
@ -314,17 +352,15 @@ export MyExtendedClass extends MyClass {};`
updateImports(
tree,
normalizeSchema(tree, schema, projectConfig),
await 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'],
'@proj/my-destination': ['my-destination/src/index.ts'],
'@proj/my-destination/testing': ['my-destination/testing/src/index.ts'],
'@proj/different-alias': ['my-destination/some-path/src/index.ts'],
});
});
@ -332,12 +368,13 @@ export MyExtendedClass extends MyClass {};`
tree.delete('libs');
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
updateImports(
tree,
normalizeSchema(tree, schema, projectConfig),
await normalizeSchema(tree, schema, projectConfig),
projectConfig
);
@ -351,18 +388,19 @@ export MyExtendedClass extends MyClass {};`
tree.rename('tsconfig.base.json', 'tsconfig.json');
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
updateImports(
tree,
normalizeSchema(tree, schema, projectConfig),
await normalizeSchema(tree, schema, projectConfig),
projectConfig
);
const tsConfig = readJson(tree, '/tsconfig.json');
expect(tsConfig.compilerOptions.paths).toEqual({
'@proj/my-destination': ['libs/my-destination/src/index.ts'],
'@proj/my-destination': ['my-destination/src/index.ts'],
});
});
@ -374,30 +412,32 @@ export MyExtendedClass extends MyClass {};`
);
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
updateImports(
tree,
normalizeSchema(tree, schema, projectConfig),
await normalizeSchema(tree, schema, projectConfig),
projectConfig
);
const tsConfig = readJson(tree, '/tsconfig.json');
expect(tsConfig.compilerOptions.paths).toEqual({
'@proj/my-destination': ['libs/my-destination/src/index.ts'],
'@proj/my-destination': ['my-destination/src/index.ts'],
});
});
it('should only update the project ref paths in the tsconfig file when --updateImportPath=false', async () => {
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
updateImports(
tree,
normalizeSchema(
await normalizeSchema(
tree,
{
...schema,
@ -411,7 +451,7 @@ export MyExtendedClass extends MyClass {};`
const tsConfig = readJson(tree, '/tsconfig.base.json');
expect(tsConfig.compilerOptions.paths).toEqual({
'@proj/my-source': ['libs/my-destination/src/index.ts'],
'@proj/my-source': ['my-destination/src/index.ts'],
});
});
});

View File

@ -35,7 +35,6 @@ export function updateImports(
project: ProjectConfiguration
) {
if (project.projectType === 'application') {
// These shouldn't be imported anywhere?
return;
}
@ -88,9 +87,10 @@ export function updateImports(
// 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,
to:
schema.importPath && p.startsWith(mainEntryPointImportPath)
? p.replace(mainEntryPointImportPath, schema.importPath)
: null,
})),
];

View File

@ -16,6 +16,7 @@ describe('updateJestConfig', () => {
it('should handle jest config not existing', async () => {
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
const schema: NormalizedSchema = {
@ -24,7 +25,7 @@ describe('updateJestConfig', () => {
importPath: '@proj/my-destination',
updateImportPath: true,
newProjectName: 'my-destination',
relativeToRootDestination: 'libs/my-destination',
relativeToRootDestination: 'my-destination',
};
expect(() => updateJestConfig(tree, schema, projectConfig)).not.toThrow();
@ -34,16 +35,17 @@ describe('updateJestConfig', () => {
const jestConfig = `module.exports = {
name: 'my-source',
preset: '../../jest.config.ts',
coverageDirectory: '../../coverage/libs/my-source',
coverageDirectory: '../coverage/my-source',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
]
};`;
const jestConfigPath = '/libs/my-destination/jest.config.ts';
const jestConfigPath = 'my-destination/jest.config.ts';
const rootJestConfigPath = '/jest.config.ts';
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(jestConfigPath, jestConfig);
@ -53,7 +55,7 @@ describe('updateJestConfig', () => {
importPath: '@proj/my-destination',
updateImportPath: true,
newProjectName: 'my-destination',
relativeToRootDestination: 'libs/my-destination',
relativeToRootDestination: 'my-destination',
};
updateJestConfig(tree, schema, projectConfig);
@ -62,25 +64,62 @@ describe('updateJestConfig', () => {
const rootJestConfigAfter = tree.read(rootJestConfigPath, 'utf-8');
expect(jestConfigAfter).toContain(`name: 'my-destination'`);
expect(jestConfigAfter).toContain(
`coverageDirectory: '../../coverage/libs/my-destination'`
`coverageDirectory: '../coverage/my-destination'`
);
expect(rootJestConfigAfter).toContain('getJestProjects()');
});
it('should update jest configs properly even if project is in many layers of subfolders', async () => {
it('should update the name and dir correctly when moving to a nested dir', async () => {
const jestConfig = `module.exports = {
name: 'some-test-dir-my-source',
name: 'my-source',
preset: '../../jest.config.ts',
coverageDirectory: '../../coverage/libs/some/test/dir/my-source',
coverageDirectory: '../coverage/my-source',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
]
};`;
const jestConfigPath = '/libs/other/test/dir/my-destination/jest.config.ts';
const jestConfigPath = 'my-source/data-access/jest.config.ts';
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(jestConfigPath, jestConfig);
const schema: NormalizedSchema = {
projectName: 'my-source',
destination: 'my-source/data-access',
importPath: '@proj/my-soource-data-access',
updateImportPath: true,
newProjectName: 'my-source-data-access',
relativeToRootDestination: 'my-source/data-access',
};
updateJestConfig(tree, schema, projectConfig);
const jestConfigAfter = tree.read(jestConfigPath, 'utf-8');
expect(jestConfigAfter).toContain(`name: 'my-source-data-access'`);
expect(jestConfigAfter).toContain(
`coverageDirectory: '../coverage/my-source/data-access'`
);
});
it('should update jest configs properly even if project is in many layers of subfolders', async () => {
const jestConfig = `module.exports = {
name: 'some-test-dir-my-source',
preset: '../jest.config.ts',
coverageDirectory: '../coverage/some/test/dir/my-source',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
]
};`;
const jestConfigPath = 'other/test/dir/my-destination/jest.config.ts';
const rootJestConfigPath = '/jest.config.ts';
await libraryGenerator(tree, {
name: 'some/test/dir/my-source',
name: 'some-test-dir-my-source',
directory: 'some/test/dir/my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(
tree,
@ -93,7 +132,7 @@ describe('updateJestConfig', () => {
importPath: '@proj/other-test-dir-my-destination',
updateImportPath: true,
newProjectName: 'other-test-dir-my-destination',
relativeToRootDestination: 'libs/other/test/dir/my-destination',
relativeToRootDestination: 'other/test/dir/my-destination',
};
updateJestConfig(tree, schema, projectConfig);
@ -101,7 +140,7 @@ describe('updateJestConfig', () => {
const rootJestConfigAfter = tree.read(rootJestConfigPath, 'utf-8');
expect(jestConfigAfter).toContain(`name: 'other-test-dir-my-destination'`);
expect(jestConfigAfter).toContain(
`coverageDirectory: '../../coverage/libs/other/test/dir/my-destination'`
`coverageDirectory: '../coverage/other/test/dir/my-destination'`
);
expect(rootJestConfigAfter).toContain('getJestProjects()');
});
@ -109,12 +148,14 @@ describe('updateJestConfig', () => {
it('updates the root config if not using `getJestProjects()`', async () => {
const rootJestConfigPath = '/jest.config.ts';
await libraryGenerator(tree, {
name: 'some/test/dir/my-source',
name: 'some-test-dir-my-source',
directory: 'some/test/dir/my-source',
projectNameAndRootFormat: 'as-provided',
});
tree.write(
rootJestConfigPath,
`module.exports = {
projects: ['<rootDir>/libs/some/test/dir/my-source']
projects: ['<rootDir>/some/test/dir/my-source']
};
`
);
@ -128,31 +169,33 @@ describe('updateJestConfig', () => {
importPath: '@proj/other-test-dir-my-destination',
updateImportPath: true,
newProjectName: 'other-test-dir-my-destination',
relativeToRootDestination: 'libs/other/test/dir/my-destination',
relativeToRootDestination: 'other/test/dir/my-destination',
};
updateJestConfig(tree, schema, projectConfig);
const rootJestConfigAfter = tree.read(rootJestConfigPath, 'utf-8');
expect(rootJestConfigAfter).not.toContain(
'<rootDir>/libs/some/test/dir/my-source'
'<rootDir>/some/test/dir/my-source'
);
expect(rootJestConfigAfter).toContain(
'<rootDir>/libs/other/test/dir/my-destination'
'<rootDir>/other/test/dir/my-destination'
);
});
it('updates the root config if `getJestProjects()` is used but old path exists', async () => {
const rootJestConfigPath = '/jest.config.ts';
await libraryGenerator(tree, {
name: 'some/test/dir/my-source',
name: 'some-test-dir-my-source',
directory: 'some/test/dir/my-source',
projectNameAndRootFormat: 'as-provided',
});
tree.write(
rootJestConfigPath,
`const { getJestProjects } = require('@nx/jest');
module.exports = {
projects: [...getJestProjects(), '<rootDir>/libs/some/test/dir/my-source']
projects: [...getJestProjects(), '<rootDir>/some/test/dir/my-source']
};
`
);
@ -166,17 +209,17 @@ module.exports = {
importPath: '@proj/other-test-dir-my-destination',
updateImportPath: true,
newProjectName: 'other-test-dir-my-destination',
relativeToRootDestination: 'libs/other/test/dir/my-destination',
relativeToRootDestination: 'other/test/dir/my-destination',
};
updateJestConfig(tree, schema, projectConfig);
const rootJestConfigAfter = tree.read(rootJestConfigPath, 'utf-8');
expect(rootJestConfigAfter).not.toContain(
'<rootDir>/libs/some/test/dir/my-source'
'<rootDir>/some/test/dir/my-source'
);
expect(rootJestConfigAfter).not.toContain(
'<rootDir>/libs/other/test/dir/my-destination'
'<rootDir>/other/test/dir/my-destination'
);
expect(rootJestConfigAfter).toContain('getJestProjects()');
});
@ -184,14 +227,16 @@ module.exports = {
it('updates the root config if `getJestProjects()` is used with other projects in the array', async () => {
const rootJestConfigPath = '/jest.config.ts';
await libraryGenerator(tree, {
name: 'some/test/dir/my-source',
name: 'some-test-dir-my-source',
directory: 'some/test/dir/my-source',
projectNameAndRootFormat: 'as-provided',
});
tree.write(
rootJestConfigPath,
`const { getJestProjects } = require('@nx/jest');
module.exports = {
projects: [...getJestProjects(), '<rootDir>/libs/some/test/dir/my-source', '<rootDir>/libs/foo']
projects: [...getJestProjects(), '<rootDir>/some/test/dir/my-source', '<rootDir>/foo']
};
`
);
@ -205,19 +250,19 @@ module.exports = {
importPath: '@proj/other-test-dir-my-destination',
updateImportPath: true,
newProjectName: 'other-test-dir-my-destination',
relativeToRootDestination: 'libs/other/test/dir/my-destination',
relativeToRootDestination: 'other/test/dir/my-destination',
};
updateJestConfig(tree, schema, projectConfig);
const rootJestConfigAfter = tree.read(rootJestConfigPath, 'utf-8');
expect(rootJestConfigAfter).not.toContain(
'<rootDir>/libs/some/test/dir/my-source'
'<rootDir>/some/test/dir/my-source'
);
expect(rootJestConfigAfter).not.toContain(
'<rootDir>/libs/other/test/dir/my-destination'
'<rootDir>/other/test/dir/my-destination'
);
expect(rootJestConfigAfter).toContain('<rootDir>/libs/foo');
expect(rootJestConfigAfter).toContain('<rootDir>/foo');
expect(rootJestConfigAfter).toContain('getJestProjects()');
});
});

View File

@ -22,16 +22,38 @@ export function updateJestConfig(
if (tree.exists(jestConfigPath)) {
const oldContent = tree.read(jestConfigPath, 'utf-8');
// ensure both single and double quotes are replaced
const findName = new RegExp(
`'${schema.projectName}'|"${schema.projectName}"|\`${schema.projectName}\``,
'g'
);
const findDir = new RegExp(project.root, 'g');
let newContent = oldContent;
if (schema.projectName !== schema.newProjectName) {
// ensure both single and double quotes are replaced
const findName = new RegExp(
`'${schema.projectName}'|"${schema.projectName}"|\`${schema.projectName}\``,
'g'
);
newContent = oldContent.replace(findName, `'${schema.newProjectName}'`);
}
let dirRegex = new RegExp(`\\/${project.root}\\/`, 'g');
if (dirRegex.test(newContent)) {
newContent = newContent.replace(
dirRegex,
`/${schema.relativeToRootDestination}/`
);
}
dirRegex = new RegExp(`\\/${project.root}['"\`]`, 'g');
if (dirRegex.test(newContent)) {
newContent = newContent.replace(
dirRegex,
`/${schema.relativeToRootDestination}'`
);
}
dirRegex = new RegExp(`['"\`]${project.root}\\/`, 'g');
if (dirRegex.test(newContent)) {
newContent = newContent.replace(
dirRegex,
`'${schema.relativeToRootDestination}/`
);
}
const newContent = oldContent
.replace(findName, `'${schema.newProjectName}'`)
.replace(findDir, schema.relativeToRootDestination);
tree.write(jestConfigPath, newContent);
}

View File

@ -17,11 +17,14 @@ describe('updatePackageJson', () => {
importPath: '@proj/my-destination',
updateImportPath: true,
newProjectName: 'my-destination',
relativeToRootDestination: 'libs/my-destination',
relativeToRootDestination: 'my-destination',
};
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await libraryGenerator(tree, { name: 'my-lib' });
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
});
it('should handle package.json not existing', async () => {
@ -34,11 +37,11 @@ describe('updatePackageJson', () => {
const packageJson = {
name: '@proj/my-lib',
};
writeJson(tree, '/libs/my-destination/package.json', packageJson);
writeJson(tree, 'my-destination/package.json', packageJson);
updatePackageJson(tree, schema);
expect(readJson(tree, '/libs/my-destination/package.json')).toEqual({
expect(readJson(tree, 'my-destination/package.json')).toEqual({
...packageJson,
name: '@proj/my-destination',
});

View File

@ -12,6 +12,10 @@ interface PartialPackageJson {
* @param schema The options provided to the schematic
*/
export function updatePackageJson(tree: Tree, schema: NormalizedSchema) {
if (!schema.importPath) {
return;
}
const packageJsonPath = path.join(
schema.relativeToRootDestination,
'package.json'

View File

@ -16,16 +16,17 @@ describe('updateProjectRootFiles', () => {
it('should update the relative root in files at the root of the project', async () => {
const testFile = `module.exports = {
name: 'my-source',
preset: '../../jest.config.js',
coverageDirectory: '../../coverage/libs/my-source',
preset: '../jest.config.js',
coverageDirectory: '../coverage/my-source',
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',
'jest-preset-angular/HTMLCommentSerializer.js'
]
};`;
const testFilePath = '/libs/subfolder/my-destination/jest.config.js';
const testFilePath = 'subfolder/my-destination/jest.config.js';
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(testFilePath, testFile);
@ -35,15 +36,15 @@ describe('updateProjectRootFiles', () => {
importPath: '@proj/subfolder-my-destination',
updateImportPath: true,
newProjectName: 'subfolder-my-destination',
relativeToRootDestination: 'libs/subfolder/my-destination',
relativeToRootDestination: 'subfolder/my-destination',
};
updateProjectRootFiles(tree, schema, projectConfig);
const testFileAfter = tree.read(testFilePath, 'utf-8');
expect(testFileAfter).toContain(`preset: '../../../jest.config.js'`);
expect(testFileAfter).toContain(`preset: '../../jest.config.js'`);
expect(testFileAfter).toContain(
`coverageDirectory: '../../../coverage/libs/my-source'`
`coverageDirectory: '../../coverage/my-source'`
);
});
});

View File

@ -18,7 +18,7 @@ describe('updateReadme', () => {
importPath: '@proj/shared-my-destination',
updateImportPath: true,
newProjectName: 'shared-my-destination',
relativeToRootDestination: 'libs/shared/my-destination',
relativeToRootDestination: 'shared/my-destination',
};
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
@ -27,6 +27,7 @@ describe('updateReadme', () => {
it('should handle README.md not existing', async () => {
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
const readmePath = join(schema.relativeToRootDestination, 'README.md');
tree.delete(readmePath);
@ -39,17 +40,15 @@ describe('updateReadme', () => {
it('should update README.md contents', async () => {
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
// This step is usually handled elsewhere
tree.rename(
'libs/my-lib/README.md',
'libs/shared/my-destination/README.md'
);
tree.rename('my-lib/README.md', 'shared/my-destination/README.md');
updateReadme(tree, schema);
const content = tree
.read('/libs/shared/my-destination/README.md')
.read('shared/my-destination/README.md')
.toString('utf8');
expect(content).toMatch('# shared-my-destination');
expect(content).toMatch('nx test shared-my-destination');

View File

@ -16,6 +16,7 @@ describe('updateStorybookConfig', () => {
it('should handle storybook config not existing', async () => {
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
const schema: NormalizedSchema = {
@ -24,7 +25,7 @@ describe('updateStorybookConfig', () => {
importPath: '@proj/my-destination',
updateImportPath: true,
newProjectName: 'my-destination',
relativeToRootDestination: 'libs/my-destination',
relativeToRootDestination: 'my-destination',
};
expect(() =>
@ -34,14 +35,14 @@ describe('updateStorybookConfig', () => {
it('should update the import path for main.js', async () => {
const storybookMain = `
const rootMain = require('../../../.storybook/main');
const rootMain = require('../../.storybook/main');
module.exports = rootMain;
`;
const storybookMainPath =
'/libs/namespace/my-destination/.storybook/main.js';
const storybookMainPath = 'namespace/my-destination/.storybook/main.js';
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(storybookMainPath, storybookMain);
@ -51,25 +52,26 @@ describe('updateStorybookConfig', () => {
importPath: '@proj/namespace-my-destination',
updateImportPath: true,
newProjectName: 'namespace-my-destination',
relativeToRootDestination: 'libs/namespace/my-destination',
relativeToRootDestination: 'namespace/my-destination',
};
updateStorybookConfig(tree, schema, projectConfig);
const storybookMainAfter = tree.read(storybookMainPath, 'utf-8');
expect(storybookMainAfter).toContain(
`const rootMain = require('../../../../.storybook/main');`
`const rootMain = require('../../../.storybook/main');`
);
});
it('should update the import path for webpack.config.json', async () => {
const storybookWebpackConfig = `
const rootWebpackConfig = require('../../../.storybook/webpack.config');
const rootWebpackConfig = require('../../.storybook/webpack.config');
`;
const storybookWebpackConfigPath =
'/libs/namespace/my-destination/.storybook/webpack.config.js';
'namespace/my-destination/.storybook/webpack.config.js';
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(storybookWebpackConfigPath, storybookWebpackConfig);
@ -79,7 +81,7 @@ describe('updateStorybookConfig', () => {
importPath: '@proj/namespace-my-destination',
updateImportPath: true,
newProjectName: 'namespace-my-destination',
relativeToRootDestination: 'libs/namespace/my-destination',
relativeToRootDestination: 'namespace/my-destination',
};
updateStorybookConfig(tree, schema, projectConfig);
@ -89,28 +91,28 @@ describe('updateStorybookConfig', () => {
'utf-8'
);
expect(storybookWebpackConfigAfter).toContain(
`const rootWebpackConfig = require('../../../../.storybook/webpack.config');`
`const rootWebpackConfig = require('../../../.storybook/webpack.config');`
);
});
describe('directory', () => {
it('should update the import path for directory/main.js', async () => {
const storybookMain = `
const rootMain = require('../../.storybook/main');
module.exports = rootMain;
`;
const storybookMainPath = 'namespace/my-destination/.storybook/main.js';
const storybookNestedMain = `
const rootMain = require('../../../.storybook/main');
module.exports = rootMain;
`;
const storybookMainPath =
'/libs/namespace/my-destination/.storybook/main.js';
const storybookNestedMain = `
const rootMain = require('../../../../.storybook/main');
module.exports = rootMain;
`;
const storybookNestedMainPath =
'/libs/namespace/my-destination/.storybook/nested/main.js';
'namespace/my-destination/.storybook/nested/main.js';
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(storybookMainPath, storybookMain);
@ -121,39 +123,40 @@ describe('updateStorybookConfig', () => {
importPath: '@proj/namespace-my-destination',
updateImportPath: true,
newProjectName: 'namespace-my-destination',
relativeToRootDestination: 'libs/namespace/my-destination',
relativeToRootDestination: 'namespace/my-destination',
};
updateStorybookConfig(tree, schema, projectConfig);
const storybookMainAfter = tree.read(storybookMainPath, 'utf-8');
expect(storybookMainAfter).toContain(
`const rootMain = require('../../../../.storybook/main');`
`const rootMain = require('../../../.storybook/main');`
);
const storybookNestedMainAfter = tree.read(
storybookNestedMainPath,
'utf-8'
);
expect(storybookNestedMainAfter).toContain(
`const rootMain = require('../../../../../.storybook/main');`
`const rootMain = require('../../../../.storybook/main');`
);
});
it('should update the import path for directory/webpack.config.json', async () => {
const storybookWebpackConfig = `
const rootWebpackConfig = require('../../../.storybook/webpack.config');
const rootWebpackConfig = require('../../.storybook/webpack.config');
`;
const storybookWebpackConfigPath =
'/libs/namespace/my-destination/.storybook/webpack.config.js';
'namespace/my-destination/.storybook/webpack.config.js';
const storybookNestedWebpackConfig = `
const rootWebpackConfig = require('../../../../.storybook/webpack.config');
const rootWebpackConfig = require('../../../.storybook/webpack.config');
`;
const storybookNestedWebpackConfigPath =
'/libs/namespace/my-destination/.storybook/nested/webpack.config.js';
'namespace/my-destination/.storybook/nested/webpack.config.js';
await libraryGenerator(tree, {
name: 'my-source',
projectNameAndRootFormat: 'as-provided',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(storybookWebpackConfigPath, storybookWebpackConfig);
@ -167,7 +170,7 @@ describe('updateStorybookConfig', () => {
importPath: '@proj/namespace-my-destination',
updateImportPath: true,
newProjectName: 'namespace-my-destination',
relativeToRootDestination: 'libs/namespace/my-destination',
relativeToRootDestination: 'namespace/my-destination',
};
updateStorybookConfig(tree, schema, projectConfig);
@ -177,7 +180,7 @@ describe('updateStorybookConfig', () => {
'utf-8'
);
expect(storybookWebpackConfigAfter).toContain(
`const rootWebpackConfig = require('../../../../.storybook/webpack.config');`
`const rootWebpackConfig = require('../../../.storybook/webpack.config');`
);
const storybookNestedWebpackConfigAfter = tree.read(
@ -185,7 +188,7 @@ describe('updateStorybookConfig', () => {
'utf-8'
);
expect(storybookNestedWebpackConfigAfter).toContain(
`const rootWebpackConfig = require('../../../../../.storybook/webpack.config');`
`const rootWebpackConfig = require('../../../../.storybook/webpack.config');`
);
});
});

View File

@ -19,10 +19,6 @@ export function getDestination(
schema: Schema,
project: ProjectConfiguration
): string {
if (schema.destinationRelativeToRoot) {
return schema.destination;
}
const projectType = project.projectType;
const workspaceLayout = getWorkspaceLayout(host);

View File

@ -12,47 +12,61 @@ describe('move', () => {
});
it('should update jest config when moving down directories', async () => {
await libraryGenerator(tree, { name: 'my-lib' });
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
await moveGenerator(tree, {
projectName: 'my-lib',
importPath: '@proj/shared-mylib',
updateImportPath: true,
destination: 'shared/my-lib-new',
projectNameAndRootFormat: 'as-provided',
});
const jestConfigPath = 'libs/shared/my-lib-new/jest.config.ts';
const jestConfigPath = 'shared/my-lib-new/jest.config.ts';
const afterJestConfig = tree.read(jestConfigPath, 'utf-8');
expect(tree.exists(jestConfigPath)).toBeTruthy();
expect(afterJestConfig).toContain("preset: '../../../jest.preset.js'");
expect(afterJestConfig).toContain("preset: '../../jest.preset.js'");
expect(afterJestConfig).toContain(
"coverageDirectory: '../../../coverage/libs/shared/my-lib-new'"
"coverageDirectory: '../../coverage/shared/my-lib-new'"
);
});
it('should update jest config when moving up directories', async () => {
await libraryGenerator(tree, { name: 'shared/my-lib' });
await libraryGenerator(tree, {
name: 'shared-my-lib',
directory: 'shared/my-lib',
projectNameAndRootFormat: 'as-provided',
});
await moveGenerator(tree, {
projectName: 'shared-my-lib',
importPath: '@proj/mylib',
updateImportPath: true,
destination: 'my-lib-new',
projectNameAndRootFormat: 'as-provided',
});
const jestConfigPath = 'libs/my-lib-new/jest.config.ts';
const jestConfigPath = 'my-lib-new/jest.config.ts';
const afterJestConfig = tree.read(jestConfigPath, 'utf-8');
expect(tree.exists(jestConfigPath)).toBeTruthy();
expect(afterJestConfig).toContain("preset: '../../jest.preset.js'");
expect(afterJestConfig).toContain("preset: '../jest.preset.js'");
expect(afterJestConfig).toContain(
"coverageDirectory: '../../coverage/libs/my-lib-new'"
"coverageDirectory: '../coverage/my-lib-new'"
);
});
it('should update $schema path when move', async () => {
await libraryGenerator(tree, { name: 'my-lib' });
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
let projectJson = readJson(tree, 'libs/my-lib/project.json');
let projectJson = readJson(tree, 'my-lib/project.json');
expect(projectJson['$schema']).toEqual(
'../../node_modules/nx/schemas/project-schema.json'
'../node_modules/nx/schemas/project-schema.json'
);
await moveGenerator(tree, {
@ -60,11 +74,12 @@ describe('move', () => {
importPath: '@proj/shared-mylib',
updateImportPath: true,
destination: 'shared/my-lib-new',
projectNameAndRootFormat: 'as-provided',
});
projectJson = readJson(tree, 'libs/shared/my-lib-new/project.json');
projectJson = readJson(tree, 'shared/my-lib-new/project.json');
expect(projectJson['$schema']).toEqual(
'../../../node_modules/nx/schemas/project-schema.json'
'../../node_modules/nx/schemas/project-schema.json'
);
});
@ -80,6 +95,7 @@ describe('move', () => {
buildable: true,
unitTestRunner: 'jest',
linter: 'eslint',
projectNameAndRootFormat: 'as-provided',
});
updateJson(tree, 'tsconfig.json', (json) => {
@ -100,12 +116,13 @@ describe('move', () => {
importPath: '@proj/my-lib',
updateImportPath: true,
destination: 'my-lib',
projectNameAndRootFormat: 'as-provided',
});
expect(readJson(tree, 'libs/my-lib/project.json')).toMatchObject({
expect(readJson(tree, 'my-lib/project.json')).toMatchObject({
name: 'my-lib',
$schema: '../../node_modules/nx/schemas/project-schema.json',
sourceRoot: 'libs/my-lib/src',
$schema: '../node_modules/nx/schemas/project-schema.json',
sourceRoot: 'my-lib/src',
projectType: 'library',
targets: {
build: {
@ -113,18 +130,15 @@ describe('move', () => {
outputs: ['{options.outputPath}'],
options: {
outputPath: 'dist/my-lib',
main: 'libs/my-lib/src/index.ts',
tsConfig: 'libs/my-lib/tsconfig.lib.json',
main: 'my-lib/src/index.ts',
tsConfig: 'my-lib/tsconfig.lib.json',
},
},
lint: {
executor: '@nx/linter:eslint',
outputs: ['{options.outputFile}'],
options: {
lintFilePatterns: [
'libs/my-lib/**/*.ts',
'libs/my-lib/package.json',
],
lintFilePatterns: ['my-lib/**/*.ts', 'my-lib/package.json'],
},
},
test: {
@ -134,22 +148,22 @@ describe('move', () => {
},
});
expect(readJson(tree, 'libs/my-lib/tsconfig.json')).toMatchObject({
extends: '../../tsconfig.base.json',
files: ['../../node_modules/@foo/bar/index.d.ts'],
expect(readJson(tree, 'my-lib/tsconfig.json')).toMatchObject({
extends: '../tsconfig.base.json',
files: ['../node_modules/@foo/bar/index.d.ts'],
references: [
{ path: './tsconfig.lib.json' },
{ path: './tsconfig.spec.json' },
],
});
const jestConfig = tree.read('libs/my-lib/jest.config.lib.ts', 'utf-8');
expect(jestConfig).toContain(`preset: '../../jest.preset.js'`);
const jestConfig = tree.read('my-lib/jest.config.lib.ts', 'utf-8');
expect(jestConfig).toContain(`preset: '../jest.preset.js'`);
expect(tree.exists('libs/my-lib/tsconfig.lib.json')).toBeTruthy();
expect(tree.exists('libs/my-lib/tsconfig.spec.json')).toBeTruthy();
expect(tree.exists('libs/my-lib/.eslintrc.json')).toBeTruthy();
expect(tree.exists('libs/my-lib/src/index.ts')).toBeTruthy();
expect(tree.exists('my-lib/tsconfig.lib.json')).toBeTruthy();
expect(tree.exists('my-lib/tsconfig.spec.json')).toBeTruthy();
expect(tree.exists('my-lib/.eslintrc.json')).toBeTruthy();
expect(tree.exists('my-lib/src/index.ts')).toBeTruthy();
// Test that other libs and workspace files are not moved.
expect(tree.exists('package.json')).toBeTruthy();
@ -162,4 +176,32 @@ describe('move', () => {
expect(tree.exists('jest.config.ts')).toBeTruthy();
expect(tree.exists('.eslintrc.base.json')).toBeTruthy();
});
it('should move project correctly when --project-name-and-root-format=derived', async () => {
await libraryGenerator(tree, {
name: 'my-lib',
projectNameAndRootFormat: 'derived',
});
await moveGenerator(tree, {
projectName: 'my-lib',
importPath: '@proj/shared-mylib',
updateImportPath: true,
destination: 'shared/my-lib-new',
projectNameAndRootFormat: 'derived',
});
const projectJson = readJson(tree, 'libs/shared/my-lib-new/project.json');
expect(projectJson['$schema']).toEqual(
'../../../node_modules/nx/schemas/project-schema.json'
);
const afterJestConfig = tree.read(
'libs/shared/my-lib-new/jest.config.ts',
'utf-8'
);
expect(afterJestConfig).toContain("preset: '../../../jest.preset.js'");
expect(afterJestConfig).toContain(
"coverageDirectory: '../../../coverage/libs/shared/my-lib-new'"
);
});
});

View File

@ -9,6 +9,7 @@ import { checkDestination } from './lib/check-destination';
import { createProjectConfigurationInNewDestination } from './lib/create-project-configuration-in-new-destination';
import { moveProjectFiles } from './lib/move-project-files';
import { normalizeSchema } from './lib/normalize-schema';
import { runAngularPlugin } from './lib/run-angular-plugin';
import { updateBuildTargets } from './lib/update-build-targets';
import { updateCypressConfig } from './lib/update-cypress-config';
import { updateDefaultProject } from './lib/update-default-project';
@ -28,9 +29,16 @@ import {
import { Schema } from './schema';
export async function moveGenerator(tree: Tree, rawSchema: Schema) {
await moveGeneratorInternal(tree, {
projectNameAndRootFormat: 'derived',
...rawSchema,
});
}
export async function moveGeneratorInternal(tree: Tree, rawSchema: Schema) {
let projectConfig = readProjectConfiguration(tree, rawSchema.projectName);
checkDestination(tree, rawSchema, projectConfig);
const schema = normalizeSchema(tree, rawSchema, projectConfig);
const schema = await normalizeSchema(tree, rawSchema, projectConfig);
checkDestination(tree, schema, rawSchema.destination);
if (projectConfig.root === '.') {
maybeExtractTsConfigBase(tree);
@ -55,6 +63,8 @@ export async function moveGenerator(tree: Tree, rawSchema: Schema) {
updateDefaultProject(tree, schema);
updateImplicitDependencies(tree, schema);
await runAngularPlugin(tree, schema);
if (!schema.skipFormat) {
await formatFiles(tree);
}

View File

@ -1,14 +1,15 @@
import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
export interface Schema {
projectName: string;
destination: string;
importPath?: string;
updateImportPath: boolean;
skipFormat?: boolean;
destinationRelativeToRoot?: boolean;
newProjectName?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
}
export interface NormalizedSchema extends Schema {
importPath: string;
relativeToRootDestination: string;
}

View File

@ -18,6 +18,13 @@
"description": "The name of the project to move.",
"x-dropdown": "projects"
},
"newProjectName": {
"type": "string",
"alias": "project",
"description": "The new name of the project after the move.",
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
"x-priority": "important"
},
"destination": {
"type": "string",
"description": "The folder to move the project into.",
@ -26,6 +33,11 @@
"index": 0
}
},
"projectNameAndRootFormat": {
"description": "Whether to generate the new project name and destination as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"importPath": {
"type": "string",
"description": "The new import path to use in the `tsconfig.base.json`."

View File

@ -60,7 +60,7 @@ async function moveWorkspaceGeneratorsToLocalPlugin(tree: Tree) {
);
project = readProjectConfiguration(tree, PROJECT_NAME);
}
await updateExistingPlugin(tree, project);
updateExistingPlugin(tree, project);
return tasks;
}
@ -187,7 +187,7 @@ async function createNewPlugin(tree: Tree) {
e2eTestRunner: 'none',
publishable: false,
});
getCreateGeneratorsJson()(
await getCreateGeneratorsJson()(
tree,
readProjectConfiguration(tree, PROJECT_NAME).root,
PROJECT_NAME
@ -207,8 +207,8 @@ function moveGeneratedPlugin(
projectName: PROJECT_NAME,
newProjectName: PROJECT_NAME,
updateImportPath: true,
destinationRelativeToRoot: true,
importPath: importPath,
projectNameAndRootFormat: 'as-provided',
});
}
}

View File

@ -9,7 +9,7 @@ export function getImportPath(tree: Tree, projectDirectory: string): string {
: projectDirectory;
}
function getNpmScope(tree: Tree) {
export function getNpmScope(tree: Tree) {
const nxJson = readNxJson(tree);
// TODO(v17): Remove reading this from nx.json