Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(misc): disallow path segments and allow scoped package name in --newProjectName option of move generator #20768

Merged
merged 1 commit into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
leosvelperez marked this conversation as resolved.
Show resolved Hide resolved
};
}

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