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 1e237bd298af0..bde0e4a2eed0a 100644 --- a/packages/workspace/src/generators/move/lib/normalize-schema.spec.ts +++ b/packages/workspace/src/generators/move/lib/normalize-schema.spec.ts @@ -37,6 +37,7 @@ describe('normalizeSchema', () => { importPath: '@proj/my/library', newProjectName: 'my-library', projectName: 'my-library', + projectNameAndRootFormat: 'derived', relativeToRootDestination: 'libs/my/library', updateImportPath: true, }; @@ -52,6 +53,7 @@ describe('normalizeSchema', () => { importPath: '@proj/my/library', newProjectName: 'my-library', projectName: 'my-library', + projectNameAndRootFormat: 'derived', relativeToRootDestination: 'libs/my/library', updateImportPath: true, }; @@ -71,6 +73,7 @@ describe('normalizeSchema', () => { importPath: '@proj/my-awesome-library', newProjectName: 'my-library', projectName: 'my-library', + projectNameAndRootFormat: 'derived', relativeToRootDestination: 'libs/my/library', updateImportPath: true, }; diff --git a/packages/workspace/src/generators/move/lib/normalize-schema.ts b/packages/workspace/src/generators/move/lib/normalize-schema.ts index a0abee4736359..9b74461626769 100644 --- a/packages/workspace/src/generators/move/lib/normalize-schema.ts +++ b/packages/workspace/src/generators/move/lib/normalize-schema.ts @@ -1,13 +1,12 @@ import { - ProjectConfiguration, - Tree, logger, names, - readNxJson, + output, stripIndents, - updateNxJson, + type ProjectConfiguration, + type Tree, } from '@nx/devkit'; -import { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { 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'; @@ -54,6 +53,13 @@ async function determineProjectNameAndRootOptions( options: Schema, projectConfiguration: ProjectConfiguration ): Promise { + if ( + !options.projectNameAndRootFormat && + (process.env.NX_INTERACTIVE !== 'true' || !isTTY()) + ) { + options.projectNameAndRootFormat = 'derived'; + } + validateName( options.newProjectName, options.projectNameAndRootFormat, @@ -65,8 +71,11 @@ async function determineProjectNameAndRootOptions( projectConfiguration ); const format = - options.projectNameAndRootFormat ?? - (await determineFormat(tree, formats, options)); + options.projectNameAndRootFormat ?? (await determineFormat(formats)); + + if (format === 'derived') { + logDeprecationMessage(formats, options); + } return formats[format]; } @@ -119,84 +128,75 @@ function validateName( function getProjectNameAndRootFormats( tree: Tree, - schema: Schema, + options: Schema, projectConfiguration: ProjectConfiguration ): ProjectNameAndRootFormats { - let destination = normalizePathSlashes(schema.destination); - const normalizedNewProjectName = schema.newProjectName - ? names(schema.newProjectName).fileName - : undefined; + let destination = normalizePathSlashes(options.destination); - const asProvidedProjectName = normalizedNewProjectName ?? schema.projectName; - const asProvidedDestination = destination; + if ( + options.newProjectName && + options.newProjectName.includes('/') && + !options.newProjectName.startsWith('@') + ) { + throw new Error( + `You can't specify a new project name with a directory path (${options.newProjectName}). ` + + `Please provide a valid name without path segments and the full destination with the "--destination" option.` + ); + } + + const asProvidedOptions = getAsProvidedOptions( + tree, + { ...options, destination }, + projectConfiguration + ); - if (normalizedNewProjectName?.startsWith('@')) { + if (options.projectNameAndRootFormat === 'as-provided') { 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, - }, + 'as-provided': asProvidedOptions, + derived: undefined, }; } - let npmScope: string; - let asProvidedImportPath = schema.importPath; - if ( - !asProvidedImportPath && - schema.newProjectName && - projectConfiguration.projectType === 'library' - ) { - npmScope = getNpmScope(tree); - asProvidedImportPath = npmScope - ? `${npmScope === '@' ? '' : '@'}${npmScope}/${asProvidedProjectName}` - : asProvidedProjectName; - } + if (asProvidedOptions.newProjectName.startsWith('@')) { + if (!options.projectNameAndRootFormat) { + output.warn({ + title: `The provided new project name "${asProvidedOptions.newProjectName}" is a scoped project name and this is not supported by the move generator when using the "derived" format.`, + bodyLines: [ + `The generator will try to move the project using the "as-provided" format with the new name "${asProvidedOptions.newProjectName}" located at "${asProvidedOptions.destination}".`, + ], + }); - const derivedProjectName = - schema.newProjectName ?? getNewProjectName(destination); - const derivedDestination = getDestination(tree, schema, projectConfiguration); + return { + 'as-provided': asProvidedOptions, + derived: undefined, + }; + } - let derivedImportPath: string; - if (projectConfiguration.projectType === 'library') { - derivedImportPath = - schema.importPath ?? - normalizePathSlashes(getImportPath(tree, destination)); + throw new Error( + `The provided new project name "${options.newProjectName}" is a scoped project name and this is not supported by the move generator when using the "derived" format. ` + + `Please provide a name without "@" or use the "as-provided" format.` + ); } + const derivedOptions = getDerivedOptions( + tree, + { ...options, destination }, + projectConfiguration + ); + return { - 'as-provided': { - destination: asProvidedDestination, - newProjectName: asProvidedProjectName, - importPath: asProvidedImportPath, - }, - derived: { - destination: derivedDestination, - newProjectName: derivedProjectName, - importPath: derivedImportPath, - }, + 'as-provided': asProvidedOptions, + derived: derivedOptions, }; } async function determineFormat( - tree: Tree, - formats: ProjectNameAndRootFormats, - schema: Schema + formats: ProjectNameAndRootFormats ): 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}`; @@ -226,40 +226,80 @@ async function determineFormat( format === asProvidedSelectedValue ? 'as-provided' : 'derived' ); + return result; +} + +function logDeprecationMessage( + formats: ProjectNameAndRootFormats, + options: Schema +) { const callingGenerator = process.env.NX_ANGULAR_MOVE_INVOKED === 'true' ? '@nx/angular:move' : '@nx/workspace:move'; - const deprecationWarning = stripIndents` + + logger.warn( + 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); + Please provide the exact new project name and destination in the future. + Example: nx g ${callingGenerator} --projectName ${options.projectName} --destination ${formats['derived'].destination}` + + (options.projectName !== formats['derived'].newProjectName + ? ` --newProjectName ${formats['derived'].newProjectName}` + : '') + ); +} + +function getAsProvidedOptions( + tree: Tree, + options: Schema, + projectConfiguration: ProjectConfiguration +): ProjectNameAndRootOptions { + const newProjectName = options.newProjectName ?? options.projectName; + const destination = options.destination; + + if (projectConfiguration.projectType !== 'library') { + return { destination, newProjectName }; } - return result; + let importPath = options.importPath; + if (importPath) { + return { destination, newProjectName, importPath }; + } + + if (options.newProjectName?.startsWith('@')) { + // keep the existing import path if the name didn't change + importPath = + options.newProjectName && options.projectName !== options.newProjectName + ? newProjectName + : undefined; + } else if (options.newProjectName) { + const npmScope = getNpmScope(tree); + importPath = npmScope + ? `${npmScope === '@' ? '' : '@'}${npmScope}/${newProjectName}` + : newProjectName; + } + + return { destination, newProjectName, importPath }; +} + +function getDerivedOptions( + tree: Tree, + options: Schema, + projectConfiguration: ProjectConfiguration +): ProjectNameAndRootOptions { + const newProjectName = options.newProjectName + ? names(options.newProjectName).fileName + : getNewProjectName(options.destination); + const destination = getDestination(tree, options, projectConfiguration); + + let importPath: string | undefined; + if (projectConfiguration.projectType === 'library') { + importPath = + options.importPath ?? + normalizePathSlashes(getImportPath(tree, options.destination)); + } + + return { destination, newProjectName, importPath }; } function isTTY(): boolean { diff --git a/packages/workspace/src/generators/move/move.spec.ts b/packages/workspace/src/generators/move/move.spec.ts index 804a6a4fbdcc4..e7caca57882bb 100644 --- a/packages/workspace/src/generators/move/move.spec.ts +++ b/packages/workspace/src/generators/move/move.spec.ts @@ -352,6 +352,67 @@ describe('move', () => { ]); }); + it('should support scoped new project name for libraries', async () => { + await libraryGenerator(tree, { + name: 'my-lib', + projectNameAndRootFormat: 'as-provided', + }); + + await moveGenerator(tree, { + projectName: 'my-lib', + newProjectName: '@proj/shared-my-lib', + updateImportPath: true, + destination: 'shared/my-lib', + projectNameAndRootFormat: 'as-provided', + }); + + expect(tree.exists('shared/my-lib/package.json')).toBeTruthy(); + expect(tree.exists('shared/my-lib/tsconfig.lib.json')).toBeTruthy(); + expect(tree.exists('shared/my-lib/src/index.ts')).toBeTruthy(); + expect(readProjectConfiguration(tree, '@proj/shared-my-lib')) + .toMatchInlineSnapshot(` + { + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "@proj/shared-my-lib", + "projectType": "library", + "root": "shared/my-lib", + "sourceRoot": "shared/my-lib/src", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "options": { + "assets": [ + "shared/my-lib/*.md", + ], + "main": "shared/my-lib/src/index.ts", + "outputPath": "dist/shared/my-lib", + "tsConfig": "shared/my-lib/tsconfig.lib.json", + }, + "outputs": [ + "{options.outputPath}", + ], + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}", + ], + }, + "test": { + "executor": "@nx/jest:jest", + "options": { + "jestConfig": "shared/my-lib/jest.config.ts", + }, + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}", + ], + }, + }, + } + `); + }); + it('should move project correctly when --project-name-and-root-format=derived', async () => { await libraryGenerator(tree, { name: 'my-lib',