Skip to content

Commit

Permalink
fix(misc): disallow path segments and allow scoped package name in --…
Browse files Browse the repository at this point in the history
…newProjectName option of move generator (#20768)
  • Loading branch information
leosvelperez authored Dec 14, 2023
1 parent d047ad5 commit 6129d31
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('normalizeSchema', () => {
importPath: '@proj/my/library',
newProjectName: 'my-library',
projectName: 'my-library',
projectNameAndRootFormat: 'derived',
relativeToRootDestination: 'libs/my/library',
updateImportPath: true,
};
Expand All @@ -52,6 +53,7 @@ describe('normalizeSchema', () => {
importPath: '@proj/my/library',
newProjectName: 'my-library',
projectName: 'my-library',
projectNameAndRootFormat: 'derived',
relativeToRootDestination: 'libs/my/library',
updateImportPath: true,
};
Expand All @@ -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,
};
Expand Down
220 changes: 130 additions & 90 deletions packages/workspace/src/generators/move/lib/normalize-schema.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -54,6 +53,13 @@ async function determineProjectNameAndRootOptions(
options: Schema,
projectConfiguration: ProjectConfiguration
): Promise<ProjectNameAndRootOptions> {
if (
!options.projectNameAndRootFormat &&
(process.env.NX_INTERACTIVE !== 'true' || !isTTY())
) {
options.projectNameAndRootFormat = 'derived';
}

validateName(
options.newProjectName,
options.projectNameAndRootFormat,
Expand All @@ -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];
}
Expand Down Expand Up @@ -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<ProjectNameAndRootFormat> {
if (!formats.derived) {
return 'as-provided';
}

if (process.env.NX_INTERACTIVE !== 'true' || !isTTY()) {
return 'derived';
}

const asProvidedDescription = `As provided:
Name: ${formats['as-provided'].newProjectName}
Destination: ${formats['as-provided'].destination}`;
Expand Down Expand Up @@ -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 {
Expand Down
61 changes: 61 additions & 0 deletions packages/workspace/src/generators/move/move.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 6129d31

Please sign in to comment.