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 () => {
|
it('should return the project name and directory as provided for root projects', async () => {
|
||||||
updateJson(tree, 'package.json', (json) => {
|
updateJson(tree, 'package.json', (json) => {
|
||||||
json.name = 'lib-name';
|
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(
|
await expect(
|
||||||
determineProjectNameAndRootOptions(tree, {
|
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',
|
projectType: 'library',
|
||||||
})
|
})
|
||||||
).rejects.toThrow(/directory should match/);
|
).rejects.toThrow(/directory should match/);
|
||||||
@ -231,23 +288,6 @@ describe('determineProjectNameAndRootOptions', () => {
|
|||||||
).rejects.toThrow(/name should match/);
|
).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 () => {
|
it('should handle providing a path including multiple "@" as the project name', async () => {
|
||||||
const result = await determineProjectNameAndRootOptions(tree, {
|
const result = await determineProjectNameAndRootOptions(tree, {
|
||||||
directory: 'shared/@foo/@scope/libName',
|
directory: 'shared/@foo/@scope/libName',
|
||||||
|
|||||||
@ -49,7 +49,12 @@ export async function determineProjectNameAndRootOptions(
|
|||||||
tree: Tree,
|
tree: Tree,
|
||||||
options: ProjectGenerationOptions
|
options: ProjectGenerationOptions
|
||||||
): Promise<ProjectNameAndRootOptions> {
|
): 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 directory = normalizePath(options.directory);
|
||||||
const name =
|
const name =
|
||||||
@ -57,6 +62,8 @@ export async function determineProjectNameAndRootOptions(
|
|||||||
directory.match(/(@[^@/]+(\/[^@/]+)+)/)?.[1] ??
|
directory.match(/(@[^@/]+(\/[^@/]+)+)/)?.[1] ??
|
||||||
directory.substring(directory.lastIndexOf('/') + 1);
|
directory.substring(directory.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
validateOptions(options.name, name, options.directory);
|
||||||
|
|
||||||
let projectSimpleName: string;
|
let projectSimpleName: string;
|
||||||
let projectFileName: string;
|
let projectFileName: string;
|
||||||
if (name.startsWith('@')) {
|
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 =
|
const importPath =
|
||||||
options.importPath ?? resolveImportPath(tree, name, projectRoot);
|
options.importPath ?? resolveImportPath(tree, name, projectRoot);
|
||||||
|
|
||||||
@ -136,17 +149,13 @@ export async function ensureRootProjectName(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOptions(options: ProjectGenerationOptions): void {
|
function validateOptions(
|
||||||
if (options.directory === '.') {
|
providedName: string,
|
||||||
|
derivedName: string,
|
||||||
|
directory: string
|
||||||
|
): void {
|
||||||
/**
|
/**
|
||||||
* Root projects must provide name option
|
* The provided name and the derived name from the provided directory must match one of two cases:
|
||||||
*/
|
|
||||||
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').
|
* 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 ':'.
|
* 2. Names starting with a letter and can contain any character except whitespace and ':'.
|
||||||
@ -158,17 +167,17 @@ function validateOptions(options: ProjectGenerationOptions): void {
|
|||||||
const pattern =
|
const pattern =
|
||||||
'(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$';
|
'(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$';
|
||||||
const validationRegex = new RegExp(pattern);
|
const validationRegex = new RegExp(pattern);
|
||||||
if (options.name && !validationRegex.test(options.name)) {
|
if (providedName) {
|
||||||
|
if (!validationRegex.test(providedName)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The name should match the pattern "${pattern}". The provided value "${options.name}" does not match.`
|
`The name should match the pattern "${pattern}". The provided value "${providedName}" does not match.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!validationRegex.test(options.directory)) {
|
} else if (!validationRegex.test(derivedName)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The directory should match the pattern "${pattern}". The provided value "${options.directory}" does not match.`
|
`The derived name from the provided directory should match the pattern "${pattern}". The derived name "${derivedName}" from the provided value "${directory}" does not match.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImportPath(npmScope: string | undefined, name: string) {
|
function getImportPath(npmScope: string | undefined, name: string) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user