diff --git a/docs/generated/packages/angular/generators/move.json b/docs/generated/packages/angular/generators/move.json index d89b0d89e8..b9d9dc4321 100644 --- a/docs/generated/packages/angular/generators/move.json +++ b/docs/generated/packages/angular/generators/move.json @@ -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" diff --git a/docs/generated/packages/workspace/generators/move.json b/docs/generated/packages/workspace/generators/move.json index 5942f98da6..1de5b88dbd 100644 --- a/docs/generated/packages/workspace/generators/move.json +++ b/docs/generated/packages/workspace/generators/move.json @@ -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" diff --git a/e2e/angular-extensions/src/misc.test.ts b/e2e/angular-extensions/src/misc.test.ts index 4232d0f611..b4dd87c004 100644 --- a/e2e/angular-extensions/src/misc.test.ts +++ b/e2e/angular-extensions/src/misc.test.ts @@ -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}`; diff --git a/e2e/nx-misc/src/workspace.test.ts b/e2e/nx-misc/src/workspace.test.ts index c8205bd000..7c28ee110f 100644 --- a/e2e/nx-misc/src/workspace.test.ts +++ b/e2e/nx-misc/src/workspace.test.ts @@ -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';` ); }); }); diff --git a/packages/angular/generators.json b/packages/angular/generators.json index d808a55291..9102c2499f 100644 --- a/packages/angular/generators.json +++ b/packages/angular/generators.json @@ -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", diff --git a/packages/angular/package.json b/packages/angular/package.json index a03d93482d..747bc78336 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -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", diff --git a/packages/angular/src/generators/move/lib/index.ts b/packages/angular/src/generators/move/lib/index.ts index 0fcaada668..0cc184b69f 100644 --- a/packages/angular/src/generators/move/lib/index.ts +++ b/packages/angular/src/generators/move/lib/index.ts @@ -1,4 +1,3 @@ -export * from './normalize-schema'; export * from './update-module-name'; export * from './update-ng-package'; export * from './update-secondary-entry-points'; diff --git a/packages/angular/src/generators/move/lib/normalize-schema.ts b/packages/angular/src/generators/move/lib/normalize-schema.ts deleted file mode 100644 index ce2afdd6d4..0000000000 --- a/packages/angular/src/generators/move/lib/normalize-schema.ts +++ /dev/null @@ -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, - }; -} diff --git a/packages/angular/src/generators/move/lib/types.ts b/packages/angular/src/generators/move/lib/types.ts new file mode 100644 index 0000000000..25389961ad --- /dev/null +++ b/packages/angular/src/generators/move/lib/types.ts @@ -0,0 +1,4 @@ +export type MoveImplOptions = { + oldProjectName: string; + newProjectName: string; +}; diff --git a/packages/angular/src/generators/move/lib/update-module-name.spec.ts b/packages/angular/src/generators/move/lib/update-module-name.spec.ts deleted file mode 100644 index a7c840f5e3..0000000000 --- a/packages/angular/src/generators/move/lib/update-module-name.spec.ts +++ /dev/null @@ -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 {}`); - }); - }); -}); diff --git a/packages/angular/src/generators/move/lib/update-module-name.ts b/packages/angular/src/generators/move/lib/update-module-name.ts index e52c89108b..89157ab1a7 100644 --- a/packages/angular/src/generators/move/lib/update-module-name.ts +++ b/packages/angular/src/generators/move/lib/update-module-name.ts @@ -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'); diff --git a/packages/angular/src/generators/move/lib/update-ng-package.ts b/packages/angular/src/generators/move/lib/update-ng-package.ts index c7c87b55b2..01c158f410 100644 --- a/packages/angular/src/generators/move/lib/update-ng-package.ts +++ b/packages/angular/src/generators/move/lib/update-ng-package.ts @@ -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') { diff --git a/packages/angular/src/generators/move/lib/update-secondary-entry-points.ts b/packages/angular/src/generators/move/lib/update-secondary-entry-points.ts index e20474049e..833b92217b 100644 --- a/packages/angular/src/generators/move/lib/update-secondary-entry-points.ts +++ b/packages/angular/src/generators/move/lib/update-secondary-entry-points.ts @@ -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 ); }); diff --git a/packages/angular/src/generators/move/move-impl.ts b/packages/angular/src/generators/move/move-impl.ts new file mode 100644 index 0000000000..331f749bc4 --- /dev/null +++ b/packages/angular/src/generators/move/move-impl.ts @@ -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 { + // 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 { + const projectGraph = await createProjectGraphAsync(); + + return projectGraph.dependencies[project]?.some( + (dependency) => dependency.target === 'npm:@angular/core' + ); +} diff --git a/packages/angular/src/generators/move/move.spec.ts b/packages/angular/src/generators/move/move.spec.ts index de9857a5dd..1212117148 100644 --- a/packages/angular/src/generators/move/move.spec.ts +++ b/packages/angular/src/generators/move/move.spec.ts @@ -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'); }); }); diff --git a/packages/angular/src/generators/move/move.ts b/packages/angular/src/generators/move/move.ts index b5f77840c8..2a806dfd4c 100644 --- a/packages/angular/src/generators/move/move.ts +++ b/packages/angular/src/generators/move/move.ts @@ -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 { - 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 { + process.env.NX_ANGULAR_MOVE_INVOKED = 'true'; + await moveGeneratorInternal(tree, schema); } diff --git a/packages/angular/src/generators/move/schema.d.ts b/packages/angular/src/generators/move/schema.d.ts index 8aa957be35..dff7858362 100644 --- a/packages/angular/src/generators/move/schema.d.ts +++ b/packages/angular/src/generators/move/schema.d.ts @@ -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; } diff --git a/packages/angular/src/generators/move/schema.json b/packages/angular/src/generators/move/schema.json index eac1fd01d5..30063cbf45 100644 --- a/packages/angular/src/generators/move/schema.json +++ b/packages/angular/src/generators/move/schema.json @@ -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`." diff --git a/packages/workspace/generators.json b/packages/workspace/generators.json index f2448a5a51..ff28dc9d80 100644 --- a/packages/workspace/generators.json +++ b/packages/workspace/generators.json @@ -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." diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 2b9d6a43ad..5ccbe64da8 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -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" diff --git a/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.ts b/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.ts index 2fa76f4323..f3dead4470 100644 --- a/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.ts +++ b/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.ts @@ -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', }); } } diff --git a/packages/workspace/src/generators/move/lib/check-destination.spec.ts b/packages/workspace/src/generators/move/lib/check-destination.spec.ts index 17267ac8c6..83fc4c4635 100644 --- a/packages/workspace/src/generators/move/lib/check-destination.spec.ts +++ b/packages/workspace/src/generators/move/lib/check-destination.spec.ts @@ -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(); }); }); diff --git a/packages/workspace/src/generators/move/lib/check-destination.ts b/packages/workspace/src/generators/move/lib/check-destination.ts index 930eaed071..0cc4ebabb4 100644 --- a/packages/workspace/src/generators/move/lib/check-destination.ts +++ b/packages/workspace/src/generators/move/lib/check-destination.ts @@ -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.`); } } diff --git a/packages/workspace/src/generators/move/lib/move-project-files.spec.ts b/packages/workspace/src/generators/move/lib/move-project-files.spec.ts index d39e3caa39..cd5eaeafe4 100644 --- a/packages/workspace/src/generators/move/lib/move-project-files.spec.ts +++ b/packages/workspace/src/generators/move/lib/move-project-files.spec.ts @@ -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(); }); }); diff --git a/packages/workspace/src/generators/move/lib/normalize-schema.spec.ts b/packages/workspace/src/generators/move/lib/normalize-schema.spec.ts index fc5a3ba0df..1e237bd298 100644 --- a/packages/workspace/src/generators/move/lib/normalize-schema.spec.ts +++ b/packages/workspace/src/generators/move/lib/normalize-schema.spec.ts @@ -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'); }); diff --git a/packages/workspace/src/generators/move/lib/normalize-schema.ts b/packages/workspace/src/generators/move/lib/normalize-schema.ts index dbf892d21d..a0abee4736 100644 --- a/packages/workspace/src/generators/move/lib/normalize-schema.ts +++ b/packages/workspace/src/generators/move/lib/normalize-schema.ts @@ -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 { + 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 { + 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 { + 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'; +} diff --git a/packages/workspace/src/generators/move/lib/run-angular-plugin.ts b/packages/workspace/src/generators/move/lib/run-angular-plugin.ts new file mode 100644 index 0000000000..e04e1c3550 --- /dev/null +++ b/packages/workspace/src/generators/move/lib/run-angular-plugin.ts @@ -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; + 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, + }); +} diff --git a/packages/workspace/src/generators/move/lib/update-cypress-config.spec.ts b/packages/workspace/src/generators/move/lib/update-cypress-config.spec.ts index 5ac417fc5f..b00fec6fac 100644 --- a/packages/workspace/src/generators/move/lib/update-cypress-config.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-cypress-config.spec.ts @@ -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'` ); }); }); diff --git a/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts b/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts index 425864a542..62cb232e94 100644 --- a/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-eslint-config.spec.ts @@ -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"` ) ); }); diff --git a/packages/workspace/src/generators/move/lib/update-imports.spec.ts b/packages/workspace/src/generators/move/lib/update-imports.spec.ts index c199a257db..9b02a8a51b 100644 --- a/packages/workspace/src/generators/move/lib/update-imports.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-imports.spec.ts @@ -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'], }); }); }); diff --git a/packages/workspace/src/generators/move/lib/update-imports.ts b/packages/workspace/src/generators/move/lib/update-imports.ts index 10d28006d1..6653b5c759 100644 --- a/packages/workspace/src/generators/move/lib/update-imports.ts +++ b/packages/workspace/src/generators/move/lib/update-imports.ts @@ -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, })), ]; diff --git a/packages/workspace/src/generators/move/lib/update-jest-config.spec.ts b/packages/workspace/src/generators/move/lib/update-jest-config.spec.ts index ed7c4ba781..c376cad926 100644 --- a/packages/workspace/src/generators/move/lib/update-jest-config.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-jest-config.spec.ts @@ -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: ['/libs/some/test/dir/my-source'] + projects: ['/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( - '/libs/some/test/dir/my-source' + '/some/test/dir/my-source' ); expect(rootJestConfigAfter).toContain( - '/libs/other/test/dir/my-destination' + '/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(), '/libs/some/test/dir/my-source'] + projects: [...getJestProjects(), '/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( - '/libs/some/test/dir/my-source' + '/some/test/dir/my-source' ); expect(rootJestConfigAfter).not.toContain( - '/libs/other/test/dir/my-destination' + '/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(), '/libs/some/test/dir/my-source', '/libs/foo'] + projects: [...getJestProjects(), '/some/test/dir/my-source', '/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( - '/libs/some/test/dir/my-source' + '/some/test/dir/my-source' ); expect(rootJestConfigAfter).not.toContain( - '/libs/other/test/dir/my-destination' + '/other/test/dir/my-destination' ); - expect(rootJestConfigAfter).toContain('/libs/foo'); + expect(rootJestConfigAfter).toContain('/foo'); expect(rootJestConfigAfter).toContain('getJestProjects()'); }); }); diff --git a/packages/workspace/src/generators/move/lib/update-jest-config.ts b/packages/workspace/src/generators/move/lib/update-jest-config.ts index e0b8447662..d087d35a89 100644 --- a/packages/workspace/src/generators/move/lib/update-jest-config.ts +++ b/packages/workspace/src/generators/move/lib/update-jest-config.ts @@ -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); } diff --git a/packages/workspace/src/generators/move/lib/update-package-json.spec.ts b/packages/workspace/src/generators/move/lib/update-package-json.spec.ts index 2307319609..633a4ad656 100644 --- a/packages/workspace/src/generators/move/lib/update-package-json.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-package-json.spec.ts @@ -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', }); diff --git a/packages/workspace/src/generators/move/lib/update-package-json.ts b/packages/workspace/src/generators/move/lib/update-package-json.ts index ef8f3f9673..c9dcff11e7 100644 --- a/packages/workspace/src/generators/move/lib/update-package-json.ts +++ b/packages/workspace/src/generators/move/lib/update-package-json.ts @@ -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' diff --git a/packages/workspace/src/generators/move/lib/update-project-root-files.spec.ts b/packages/workspace/src/generators/move/lib/update-project-root-files.spec.ts index 3cebac62b2..912105faba 100644 --- a/packages/workspace/src/generators/move/lib/update-project-root-files.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-project-root-files.spec.ts @@ -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'` ); }); }); diff --git a/packages/workspace/src/generators/move/lib/update-readme.spec.ts b/packages/workspace/src/generators/move/lib/update-readme.spec.ts index f1f895ed08..7ed7e9051f 100644 --- a/packages/workspace/src/generators/move/lib/update-readme.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-readme.spec.ts @@ -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'); diff --git a/packages/workspace/src/generators/move/lib/update-storybook-config.spec.ts b/packages/workspace/src/generators/move/lib/update-storybook-config.spec.ts index 817df8a31b..1f528670bd 100644 --- a/packages/workspace/src/generators/move/lib/update-storybook-config.spec.ts +++ b/packages/workspace/src/generators/move/lib/update-storybook-config.spec.ts @@ -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');` ); }); }); diff --git a/packages/workspace/src/generators/move/lib/utils.ts b/packages/workspace/src/generators/move/lib/utils.ts index 294e509c72..2a54d2cfd1 100644 --- a/packages/workspace/src/generators/move/lib/utils.ts +++ b/packages/workspace/src/generators/move/lib/utils.ts @@ -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); diff --git a/packages/workspace/src/generators/move/move.spec.ts b/packages/workspace/src/generators/move/move.spec.ts index 543c2ed002..d6bd4f0bda 100644 --- a/packages/workspace/src/generators/move/move.spec.ts +++ b/packages/workspace/src/generators/move/move.spec.ts @@ -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'" + ); + }); }); diff --git a/packages/workspace/src/generators/move/move.ts b/packages/workspace/src/generators/move/move.ts index 6c868db0a2..07ff3bd763 100644 --- a/packages/workspace/src/generators/move/move.ts +++ b/packages/workspace/src/generators/move/move.ts @@ -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); } diff --git a/packages/workspace/src/generators/move/schema.d.ts b/packages/workspace/src/generators/move/schema.d.ts index 24c54abdf4..5af363e473 100644 --- a/packages/workspace/src/generators/move/schema.d.ts +++ b/packages/workspace/src/generators/move/schema.d.ts @@ -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; } diff --git a/packages/workspace/src/generators/move/schema.json b/packages/workspace/src/generators/move/schema.json index 22422bbb8f..e148886bcf 100644 --- a/packages/workspace/src/generators/move/schema.json +++ b/packages/workspace/src/generators/move/schema.json @@ -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`." diff --git a/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts index b27ce66ba1..e2fa6a3b7c 100644 --- a/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts +++ b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts @@ -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', }); } } diff --git a/packages/workspace/src/utilities/get-import-path.ts b/packages/workspace/src/utilities/get-import-path.ts index b68447f22f..6f2bc2e8f1 100644 --- a/packages/workspace/src/utilities/get-import-path.ts +++ b/packages/workspace/src/utilities/get-import-path.ts @@ -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