fix(misc): update project directory validation to only account for the derived name (#30532)
## Current Behavior The helper to normalize the project name and directory for project generators validates the provided full directory with a regex pattern intended to only validate the name. ## Expected Behavior The helper to normalize the project name and directory for project generators should only validate the provided name or the name portion of the provided directory. ## Related Issue(s) Fixes #28801
This commit is contained in:
parent
b911ddbdac
commit
8dceb6c17d
@ -188,6 +188,54 @@ describe('determineProjectNameAndRootOptions', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should support a directory outside of the cwd', async () => {
|
||||
// simulate running in a subdirectory
|
||||
const originalInitCwd = process.env.INIT_CWD;
|
||||
process.env.INIT_CWD = join(workspaceRoot, 'some/path');
|
||||
|
||||
const result = await determineProjectNameAndRootOptions(tree, {
|
||||
directory: '../../libs/lib-name',
|
||||
projectType: 'library',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'lib-name',
|
||||
names: {
|
||||
projectSimpleName: 'lib-name',
|
||||
projectFileName: 'lib-name',
|
||||
},
|
||||
importPath: '@proj/lib-name',
|
||||
projectRoot: 'libs/lib-name',
|
||||
});
|
||||
|
||||
// restore original cwd
|
||||
if (originalInitCwd === undefined) {
|
||||
delete process.env.INIT_CWD;
|
||||
} else {
|
||||
process.env.INIT_CWD = originalInitCwd;
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw when the resolved directory is outside of the workspace root', async () => {
|
||||
// simulate running in a subdirectory
|
||||
const originalInitCwd = process.env.INIT_CWD;
|
||||
process.env.INIT_CWD = join(workspaceRoot, 'some/path');
|
||||
|
||||
await expect(
|
||||
determineProjectNameAndRootOptions(tree, {
|
||||
directory: '../../../libs/lib-name',
|
||||
projectType: 'library',
|
||||
})
|
||||
).rejects.toThrow(/is outside of the workspace root/);
|
||||
|
||||
// restore original cwd
|
||||
if (originalInitCwd === undefined) {
|
||||
delete process.env.INIT_CWD;
|
||||
} else {
|
||||
process.env.INIT_CWD = originalInitCwd;
|
||||
}
|
||||
});
|
||||
|
||||
it('should return the project name and directory as provided for root projects', async () => {
|
||||
updateJson(tree, 'package.json', (json) => {
|
||||
json.name = 'lib-name';
|
||||
@ -212,10 +260,19 @@ describe('determineProjectNameAndRootOptions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when an invalid directory is provided', async () => {
|
||||
it('should throw when a name is not provided for a root project', async () => {
|
||||
await expect(
|
||||
determineProjectNameAndRootOptions(tree, {
|
||||
directory: '!scope/lib-name',
|
||||
directory: '.',
|
||||
projectType: 'library',
|
||||
})
|
||||
).rejects.toThrow(/you must also specify the name option/);
|
||||
});
|
||||
|
||||
it('should throw when a directory is provided where the derived name is invalid', async () => {
|
||||
await expect(
|
||||
determineProjectNameAndRootOptions(tree, {
|
||||
directory: '@scope/lib-name/invalid-extra-segment',
|
||||
projectType: 'library',
|
||||
})
|
||||
).rejects.toThrow(/directory should match/);
|
||||
@ -231,23 +288,6 @@ describe('determineProjectNameAndRootOptions', () => {
|
||||
).rejects.toThrow(/name should match/);
|
||||
});
|
||||
|
||||
it('should handle providing a path including "@" with multiple segments as the project name', async () => {
|
||||
const result = await determineProjectNameAndRootOptions(tree, {
|
||||
directory: 'shared/@scope/lib-name/testing',
|
||||
projectType: 'library',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: '@scope/lib-name/testing',
|
||||
names: {
|
||||
projectSimpleName: 'testing',
|
||||
projectFileName: 'lib-name-testing',
|
||||
},
|
||||
importPath: '@scope/lib-name/testing',
|
||||
projectRoot: 'shared/@scope/lib-name/testing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle providing a path including multiple "@" as the project name', async () => {
|
||||
const result = await determineProjectNameAndRootOptions(tree, {
|
||||
directory: 'shared/@foo/@scope/libName',
|
||||
|
||||
@ -49,7 +49,12 @@ export async function determineProjectNameAndRootOptions(
|
||||
tree: Tree,
|
||||
options: ProjectGenerationOptions
|
||||
): Promise<ProjectNameAndRootOptions> {
|
||||
validateOptions(options);
|
||||
// root projects must provide name option
|
||||
if (options.directory === '.' && !options.name) {
|
||||
throw new Error(
|
||||
`When generating a root project, you must also specify the name option.`
|
||||
);
|
||||
}
|
||||
|
||||
const directory = normalizePath(options.directory);
|
||||
const name =
|
||||
@ -57,6 +62,8 @@ export async function determineProjectNameAndRootOptions(
|
||||
directory.match(/(@[^@/]+(\/[^@/]+)+)/)?.[1] ??
|
||||
directory.substring(directory.lastIndexOf('/') + 1);
|
||||
|
||||
validateOptions(options.name, name, options.directory);
|
||||
|
||||
let projectSimpleName: string;
|
||||
let projectFileName: string;
|
||||
if (name.startsWith('@')) {
|
||||
@ -88,6 +95,12 @@ export async function determineProjectNameAndRootOptions(
|
||||
}
|
||||
}
|
||||
|
||||
if (projectRoot.startsWith('..')) {
|
||||
throw new Error(
|
||||
`The resolved project root "${projectRoot}" is outside of the workspace root "${workspaceRoot}".`
|
||||
);
|
||||
}
|
||||
|
||||
const importPath =
|
||||
options.importPath ?? resolveImportPath(tree, name, projectRoot);
|
||||
|
||||
@ -136,38 +149,34 @@ export async function ensureRootProjectName(
|
||||
}
|
||||
}
|
||||
|
||||
function validateOptions(options: ProjectGenerationOptions): void {
|
||||
if (options.directory === '.') {
|
||||
/**
|
||||
* Root projects must provide name option
|
||||
*/
|
||||
if (!options.name) {
|
||||
throw new Error(`Root projects must also specify name option.`);
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Both directory and name (if present) must match one of two cases:
|
||||
*
|
||||
* 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 pattern =
|
||||
'(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$';
|
||||
const validationRegex = new RegExp(pattern);
|
||||
if (options.name && !validationRegex.test(options.name)) {
|
||||
function validateOptions(
|
||||
providedName: string,
|
||||
derivedName: string,
|
||||
directory: string
|
||||
): void {
|
||||
/**
|
||||
* The provided name and the derived name from the provided directory must match one of two cases:
|
||||
*
|
||||
* 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 pattern =
|
||||
'(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$';
|
||||
const validationRegex = new RegExp(pattern);
|
||||
if (providedName) {
|
||||
if (!validationRegex.test(providedName)) {
|
||||
throw new Error(
|
||||
`The name should match the pattern "${pattern}". The provided value "${options.name}" does not match.`
|
||||
);
|
||||
}
|
||||
if (!validationRegex.test(options.directory)) {
|
||||
throw new Error(
|
||||
`The directory should match the pattern "${pattern}". The provided value "${options.directory}" does not match.`
|
||||
`The name should match the pattern "${pattern}". The provided value "${providedName}" does not match.`
|
||||
);
|
||||
}
|
||||
} else if (!validationRegex.test(derivedName)) {
|
||||
throw new Error(
|
||||
`The derived name from the provided directory should match the pattern "${pattern}". The derived name "${derivedName}" from the provided value "${directory}" does not match.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user