Skip to content

Commit 21809e1

Browse files
alan-agius4filipesilva
authored andcommitted
feat(@schematics/angular): loosen project name validation
With this change we update the validation of the libraries and application projects names to fully allow characters that make a valid NPM package name. http://json.schemastore.org/package has been used as reference. We also remove validators that are no longer needed. Closes #11051
1 parent 6d0f99a commit 21809e1

File tree

12 files changed

+86
-102
lines changed

12 files changed

+86
-102
lines changed

packages/schematics/angular/application/files/karma.conf.js.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module.exports = function (config) {
2525
suppressAll: true // removes the duplicated traces
2626
},
2727
coverageReporter: {
28-
dir: require('path').join(__dirname, '<%= relativePathToWorkspaceRoot %>/coverage/<%= appName%>'),
28+
dir: require('path').join(__dirname, '<%= relativePathToWorkspaceRoot %>/coverage/<%= folderName%>'),
2929
subdir: '.',
3030
reporters: [
3131
{ type: 'html' },

packages/schematics/angular/application/index.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
MergeStrategy,
1212
Rule,
1313
SchematicContext,
14-
SchematicsException,
1514
Tree,
1615
apply,
1716
applyTemplates,
@@ -28,7 +27,6 @@ import { Schema as ComponentOptions } from '../component/schema';
2827
import { NodeDependencyType, addPackageJsonDependency } from '../utility/dependencies';
2928
import { latestVersions } from '../utility/latest-versions';
3029
import { relativePathToWorkspaceRoot } from '../utility/paths';
31-
import { validateProjectName } from '../utility/validation';
3230
import { getWorkspace, updateWorkspace } from '../utility/workspace';
3331
import { Builders, ProjectType } from '../utility/workspace-models';
3432
import { Schema as ApplicationOptions, Style } from './schema';
@@ -61,7 +59,11 @@ function addDependenciesToPackageJson(options: ApplicationOptions) {
6159
};
6260
}
6361

64-
function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rule {
62+
function addAppToWorkspaceFile(
63+
options: ApplicationOptions,
64+
appDir: string,
65+
folderName: string,
66+
): Rule {
6567
let projectRoot = appDir;
6668
if (projectRoot) {
6769
projectRoot += '/';
@@ -151,7 +153,7 @@ function addAppToWorkspaceFile(options: ApplicationOptions, appDir: string): Rul
151153
builder: Builders.Browser,
152154
defaultConfiguration: 'production',
153155
options: {
154-
outputPath: `dist/${options.name}`,
156+
outputPath: `dist/${folderName}`,
155157
index: `${sourceRoot}/index.html`,
156158
main: `${sourceRoot}/main.ts`,
157159
polyfills: `${sourceRoot}/polyfills.ts`,
@@ -238,12 +240,6 @@ function minimalPathFilter(path: string): boolean {
238240

239241
export default function (options: ApplicationOptions): Rule {
240242
return async (host: Tree) => {
241-
if (!options.name) {
242-
throw new SchematicsException(`Invalid options, "name" is required.`);
243-
}
244-
245-
validateProjectName(options.name);
246-
247243
const appRootSelector = `${options.prefix}-root`;
248244
const componentOptions: Partial<ComponentOptions> = !options.minimal
249245
? {
@@ -264,13 +260,20 @@ export default function (options: ApplicationOptions): Rule {
264260
const workspace = await getWorkspace(host);
265261
const newProjectRoot = (workspace.extensions.newProjectRoot as string | undefined) || '';
266262
const isRootApp = options.projectRoot !== undefined;
263+
264+
// If scoped project (i.e. "@foo/bar"), convert dir to "foo/bar".
265+
let folderName = options.name.startsWith('@') ? options.name.substr(1) : options.name;
266+
if (/[A-Z]/.test(folderName)) {
267+
folderName = strings.dasherize(folderName);
268+
}
269+
267270
const appDir = isRootApp
268271
? normalize(options.projectRoot || '')
269-
: join(normalize(newProjectRoot), strings.dasherize(options.name));
272+
: join(normalize(newProjectRoot), folderName);
270273
const sourceDir = `${appDir}/src/app`;
271274

272275
return chain([
273-
addAppToWorkspaceFile(options, appDir),
276+
addAppToWorkspaceFile(options, appDir, folderName),
274277
mergeWith(
275278
apply(url('./files'), [
276279
options.minimal ? filter(minimalPathFilter) : noop(),
@@ -280,6 +283,7 @@ export default function (options: ApplicationOptions): Rule {
280283
relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(appDir),
281284
appName: options.name,
282285
isRootApp,
286+
folderName,
283287
}),
284288
move(appDir),
285289
]),

packages/schematics/angular/application/index_spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,15 @@ describe('Application Schematic', () => {
534534
expect(exists).toBeTrue();
535535
});
536536

537+
it(`should create scoped kebab-case project folder names with camelCase project name`, async () => {
538+
const options: ApplicationOptions = { ...defaultOptions, name: '@foo/myCool' };
539+
const tree = await schematicRunner
540+
.runSchematicAsync('application', options, workspaceTree)
541+
.toPromise();
542+
const exists = tree.exists('/projects/foo/my-cool/.browserslistrc');
543+
expect(exists).toBeTrue();
544+
});
545+
537546
it(`should create kebab-case project folder names with PascalCase project name`, async () => {
538547
const options: ApplicationOptions = { ...defaultOptions, name: 'MyCool' };
539548
const tree = await schematicRunner
@@ -542,4 +551,39 @@ describe('Application Schematic', () => {
542551
const exists = tree.exists('/projects/my-cool/.browserslistrc');
543552
expect(exists).toBeTrue();
544553
});
554+
555+
it(`should create scoped kebab-case project folder names with PascalCase project name`, async () => {
556+
const options: ApplicationOptions = { ...defaultOptions, name: '@foo/MyCool' };
557+
const tree = await schematicRunner
558+
.runSchematicAsync('application', options, workspaceTree)
559+
.toPromise();
560+
const exists = tree.exists('/projects/foo/my-cool/.browserslistrc');
561+
expect(exists).toBeTrue();
562+
});
563+
564+
it('should support creating applications with `_` and `.` in name', async () => {
565+
const options = { ...defaultOptions, name: 'foo.bar_buz' };
566+
const tree = await schematicRunner
567+
.runSchematicAsync('application', options, workspaceTree)
568+
.toPromise();
569+
570+
const exists = tree.exists('/projects/foo.bar_buz/.browserslistrc');
571+
expect(exists).toBeTrue();
572+
});
573+
574+
it('should support creating scoped application', async () => {
575+
const scopedName = '@myscope/myapp';
576+
const options = { ...defaultOptions, name: scopedName };
577+
const tree = await schematicRunner
578+
.runSchematicAsync('application', options, workspaceTree)
579+
.toPromise();
580+
581+
const cfg = JSON.parse(tree.readContent('/angular.json'));
582+
expect(cfg.projects['@myscope/myapp']).toBeDefined();
583+
584+
const karmaConf = getFileContent(tree, '/projects/myscope/myapp/karma.conf.js');
585+
expect(karmaConf).toContain(
586+
`dir: require('path').join(__dirname, '../../../coverage/myscope/myapp')`,
587+
);
588+
});
545589
});

packages/schematics/angular/application/schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"name": {
1515
"description": "The name of the new app.",
1616
"type": "string",
17+
"pattern": "^(?:@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*/)?[a-zA-Z0-9-~][a-zA-Z0-9-._~]*$",
1718
"$default": {
1819
"$source": "argv",
1920
"index": 0

packages/schematics/angular/component/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { addDeclarationToModule, addExportToModule } from '../utility/ast-utils'
2727
import { InsertChange } from '../utility/change';
2828
import { buildRelativePath, findModuleFromOptions } from '../utility/find-module';
2929
import { parseName } from '../utility/parse-name';
30-
import { validateHtmlSelector, validateName } from '../utility/validation';
30+
import { validateHtmlSelector } from '../utility/validation';
3131
import { buildDefaultPath, getWorkspace } from '../utility/workspace';
3232
import { Schema as ComponentOptions, Style } from './schema';
3333

@@ -131,7 +131,6 @@ export default function (options: ComponentOptions): Rule {
131131
options.selector =
132132
options.selector || buildSelector(options, (project && project.prefix) || '');
133133

134-
validateName(options.name);
135134
validateHtmlSelector(options.selector);
136135

137136
const skipStyleFile = options.inlineStyle || options.style === Style.None;

packages/schematics/angular/component/index_spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,14 @@ describe('Component Schematic', () => {
207207
expect(content).toMatch(/selector: 'pre-foo'/);
208208
});
209209

210+
it('should error when name starts with a digit', async () => {
211+
const options = { ...defaultOptions, name: '1-one' };
212+
213+
await expectAsync(
214+
schematicRunner.runSchematicAsync('component', options, appTree).toPromise(),
215+
).toBeRejectedWithError('Selector (app-1-one) is invalid.');
216+
});
217+
210218
it('should use the default project prefix if none is passed', async () => {
211219
const options = { ...defaultOptions, prefix: undefined };
212220

packages/schematics/angular/library/index.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { join, normalize, strings } from '@angular-devkit/core';
1010
import {
1111
Rule,
1212
SchematicContext,
13-
SchematicsException,
1413
Tree,
1514
apply,
1615
applyTemplates,
@@ -26,7 +25,6 @@ import { NodeDependencyType, addPackageJsonDependency } from '../utility/depende
2625
import { JSONFile } from '../utility/json-file';
2726
import { latestVersions } from '../utility/latest-versions';
2827
import { relativePathToWorkspaceRoot } from '../utility/paths';
29-
import { validateProjectName } from '../utility/validation';
3028
import { getWorkspace, updateWorkspace } from '../utility/workspace';
3129
import { Builders, ProjectType } from '../utility/workspace-models';
3230
import { Schema as LibraryOptions } from './schema';
@@ -125,28 +123,23 @@ function addLibToWorkspaceFile(
125123

126124
export default function (options: LibraryOptions): Rule {
127125
return async (host: Tree) => {
128-
if (!options.name) {
129-
throw new SchematicsException(`Invalid options, "name" is required.`);
130-
}
131126
const prefix = options.prefix;
132127

133-
validateProjectName(options.name);
134-
135128
// If scoped project (i.e. "@foo/bar"), convert projectDir to "foo/bar".
136-
const projectName = options.name;
137-
const packageName = strings.dasherize(projectName);
138-
let scopeName = null;
129+
const packageName = options.name;
139130
if (/^@.*\/.*/.test(options.name)) {
140-
const [scope, name] = options.name.split('/');
141-
scopeName = scope.replace(/^@/, '');
131+
const [, name] = options.name.split('/');
142132
options.name = name;
143133
}
144134

145135
const workspace = await getWorkspace(host);
146136
const newProjectRoot = (workspace.extensions.newProjectRoot as string | undefined) || '';
147137

148-
const scopeFolder = scopeName ? strings.dasherize(scopeName) + '/' : '';
149-
const folderName = `${scopeFolder}${strings.dasherize(options.name)}`;
138+
let folderName = packageName.startsWith('@') ? packageName.substr(1) : packageName;
139+
if (/[A-Z]/.test(folderName)) {
140+
folderName = strings.dasherize(folderName);
141+
}
142+
150143
const projectRoot = join(normalize(newProjectRoot), folderName);
151144
const distRoot = `dist/${folderName}`;
152145
const pathImportLib = `${distRoot}/${folderName.replace('/', '-')}`;
@@ -170,15 +163,15 @@ export default function (options: LibraryOptions): Rule {
170163

171164
return chain([
172165
mergeWith(templateSource),
173-
addLibToWorkspaceFile(options, projectRoot, projectName),
166+
addLibToWorkspaceFile(options, projectRoot, packageName),
174167
options.skipPackageJson ? noop() : addDependenciesToPackageJson(),
175168
options.skipTsConfig ? noop() : updateTsConfig(packageName, pathImportLib, distRoot),
176169
schematic('module', {
177170
name: options.name,
178171
commonModule: false,
179172
flat: true,
180173
path: sourceDir,
181-
project: projectName,
174+
project: packageName,
182175
}),
183176
schematic('component', {
184177
name: options.name,
@@ -188,13 +181,13 @@ export default function (options: LibraryOptions): Rule {
188181
flat: true,
189182
path: sourceDir,
190183
export: true,
191-
project: projectName,
184+
project: packageName,
192185
}),
193186
schematic('service', {
194187
name: options.name,
195188
flat: true,
196189
path: sourceDir,
197-
project: projectName,
190+
project: packageName,
198191
}),
199192
(_tree: Tree, context: SchematicContext) => {
200193
if (!options.skipPackageJson && !options.skipInstall) {

packages/schematics/angular/library/schema.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"name": {
1111
"type": "string",
1212
"description": "The name of the library.",
13+
"pattern": "^(?:@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*/)?[a-zA-Z0-9-~][a-zA-Z0-9-._~]*$",
1314
"$default": {
1415
"$source": "argv",
1516
"index": 0
@@ -45,5 +46,5 @@
4546
"description": "Do not update \"tsconfig.json\" to add a path mapping for the new library. The path mapping is needed to use the library in an app, but can be disabled here to simplify development."
4647
}
4748
},
48-
"required": []
49+
"required": ["name"]
4950
}

packages/schematics/angular/ng-new/index.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import {
1010
Rule,
1111
SchematicContext,
12-
SchematicsException,
1312
Tree,
1413
apply,
1514
chain,
@@ -25,19 +24,13 @@ import {
2524
RepositoryInitializerTask,
2625
} from '@angular-devkit/schematics/tasks';
2726
import { Schema as ApplicationOptions } from '../application/schema';
28-
import { validateProjectName } from '../utility/validation';
2927
import { Schema as WorkspaceOptions } from '../workspace/schema';
3028
import { Schema as NgNewOptions } from './schema';
3129

3230
export default function (options: NgNewOptions): Rule {
33-
if (!options.name) {
34-
throw new SchematicsException(`Invalid options, "name" is required.`);
35-
}
36-
37-
validateProjectName(options.name);
38-
3931
if (!options.directory) {
40-
options.directory = options.name;
32+
// If scoped project (i.e. "@foo/bar"), convert directory to "foo/bar".
33+
options.directory = options.name.startsWith('@') ? options.name.substr(1) : options.name;
4134
}
4235

4336
const workspaceOptions: WorkspaceOptions = {

packages/schematics/angular/ng-new/schema.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"name": {
1414
"description": "The name of the new workspace and initial project.",
1515
"type": "string",
16-
"format": "html-selector",
1716
"$default": {
1817
"$source": "argv",
1918
"index": 0

0 commit comments

Comments
 (0)