Skip to content

Commit e4878a8

Browse files
committed
fix(@schematics/angular): Allow for scoped library names
fixes angular/angular-cli#10172
1 parent 766059b commit e4878a8

File tree

6 files changed

+89
-47
lines changed

6 files changed

+89
-47
lines changed

packages/schematics/angular/application/index.ts

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import { JsonObject, normalize, relative, strings, tags } from '@angular-devkit/core';
8+
import { JsonObject, normalize, relative, strings } from '@angular-devkit/core';
99
import { experimental } from '@angular-devkit/core';
1010
import {
1111
MergeStrategy,
@@ -26,6 +26,7 @@ import {
2626
import { Schema as E2eOptions } from '../e2e/schema';
2727
import { getWorkspace, getWorkspacePath } from '../utility/config';
2828
import { latestVersions } from '../utility/latest-versions';
29+
import { validateProjectName } from '../utility/validation';
2930
import { Schema as ApplicationOptions } from './schema';
3031

3132
type WorkspaceSchema = experimental.workspace.WorkspaceSchema;
@@ -252,43 +253,6 @@ function addAppToWorkspaceFile(options: ApplicationOptions, workspace: Workspace
252253
host.overwrite(getWorkspacePath(host), JSON.stringify(workspace, null, 2));
253254
};
254255
}
255-
const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/;
256-
const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app'];
257-
258-
function getRegExpFailPosition(str: string): number | null {
259-
const parts = str.indexOf('-') >= 0 ? str.split('-') : [str];
260-
const matched: string[] = [];
261-
262-
parts.forEach(part => {
263-
if (part.match(projectNameRegexp)) {
264-
matched.push(part);
265-
}
266-
});
267-
268-
const compare = matched.join('-');
269-
270-
return (str !== compare) ? compare.length : null;
271-
}
272-
273-
function validateProjectName(projectName: string) {
274-
const errorIndex = getRegExpFailPosition(projectName);
275-
if (errorIndex !== null) {
276-
const firstMessage = tags.oneLine`
277-
Project name "${projectName}" is not valid. New project names must
278-
start with a letter, and must contain only alphanumeric characters or dashes.
279-
When adding a dash the segment after the dash must also start with a letter.
280-
`;
281-
const msg = tags.stripIndent`
282-
${firstMessage}
283-
${projectName}
284-
${Array(errorIndex + 1).join(' ') + '^'}
285-
`;
286-
throw new SchematicsException(msg);
287-
} else if (unsupportedProjectNames.indexOf(projectName) !== -1) {
288-
throw new SchematicsException(`Project name "${projectName}" is not a supported name.`);
289-
}
290-
291-
}
292256

293257
export default function (options: ApplicationOptions): Rule {
294258
return (host: Tree, context: SchematicContext) => {

packages/schematics/angular/application/schema.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"name": {
1313
"description": "The name of the application.",
1414
"type": "string",
15-
"format": "html-selector",
1615
"$default": {
1716
"$source": "argv",
1817
"index": 0

packages/schematics/angular/library/files/__projectRoot__/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "<%= dasherize(name) %>",
2+
"name": "<%= packageName %>",
33
"version": "0.0.1",
44
"peerDependencies": {
55
"@angular/common": "^6.0.0-rc.0 || ^6.0.0",

packages/schematics/angular/library/index.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
2424
import { WorkspaceSchema, getWorkspace, getWorkspacePath } from '../utility/config';
2525
import { latestVersions } from '../utility/latest-versions';
26+
import { validateProjectName } from '../utility/validation';
2627
import { Schema as LibraryOptions } from './schema';
2728

2829

@@ -173,19 +174,34 @@ export default function (options: LibraryOptions): Rule {
173174
if (!options.name) {
174175
throw new SchematicsException(`Invalid options, "name" is required.`);
175176
}
176-
const name = options.name;
177177
const prefix = options.prefix || 'lib';
178178

179+
validateProjectName(options.name);
180+
181+
// If scoped project (i.e. "@foo/bar"), convert projectDir to "foo/bar".
182+
const packageName = options.name;
183+
let scopeName = '';
184+
if (/^@.*\/.*/.test(options.name)) {
185+
const [scope, name] = options.name.split('/');
186+
scopeName = scope.replace(/^@/, '');
187+
options.name = name;
188+
}
189+
179190
const workspace = getWorkspace(host);
180191
const newProjectRoot = workspace.newProjectRoot;
181-
const projectRoot = `${newProjectRoot}/${strings.dasherize(options.name)}`;
192+
let projectRoot = `${newProjectRoot}/${strings.dasherize(options.name)}`;
193+
if (scopeName) {
194+
projectRoot = `${newProjectRoot}/${scopeName}/${strings.dasherize(options.name)}`;
195+
}
196+
182197
const sourceDir = `${projectRoot}/src/lib`;
183198
const relativeTsLintPath = projectRoot.split('/').map(x => '..').join('/');
184199

185200
const templateSource = apply(url('./files'), [
186201
template({
187202
...strings,
188203
...options,
204+
packageName,
189205
projectRoot,
190206
relativeTsLintPath,
191207
prefix,
@@ -199,25 +215,25 @@ export default function (options: LibraryOptions): Rule {
199215
branchAndMerge(mergeWith(templateSource)),
200216
addAppToWorkspaceFile(options, workspace, projectRoot),
201217
options.skipPackageJson ? noop() : addDependenciesToPackageJson(),
202-
options.skipTsConfig ? noop() : updateTsConfig(name),
218+
options.skipTsConfig ? noop() : updateTsConfig(options.name),
203219
schematic('module', {
204-
name: name,
220+
name: options.name,
205221
commonModule: false,
206222
flat: true,
207223
path: sourceDir,
208224
spec: false,
209225
}),
210226
schematic('component', {
211-
name: name,
212-
selector: `${prefix}-${name}`,
227+
name: options.name,
228+
selector: `${prefix}-${options.name}`,
213229
inlineStyle: true,
214230
inlineTemplate: true,
215231
flat: true,
216232
path: sourceDir,
217233
export: true,
218234
}),
219235
schematic('service', {
220-
name: name,
236+
name: options.name,
221237
flat: true,
222238
path: sourceDir,
223239
}),

packages/schematics/angular/library/index_spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,21 @@ describe('Library Schematic', () => {
203203
expect(tsConfigJson.compilerOptions.paths).toBeUndefined();
204204
});
205205
});
206+
207+
it(`should support creating scoped libraries`, () => {
208+
const scopedName = '@myscope/mylib';
209+
const options = { ...defaultOptions, name: scopedName };
210+
const tree = schematicRunner.runSchematic('library', options, workspaceTree);
211+
212+
const pkgJsonPath = '/projects/myscope/mylib/package.json';
213+
expect(tree.files).toContain(pkgJsonPath);
214+
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.module.ts');
215+
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.component.ts');
216+
217+
const pkgJson = JSON.parse(tree.readContent(pkgJsonPath));
218+
expect(pkgJson.name).toEqual(scopedName);
219+
220+
const tsConfigJson = JSON.parse(tree.readContent('/projects/myscope/mylib/tsconfig.spec.json'));
221+
expect(tsConfigJson.extends).toEqual('../../../tsconfig.json');
222+
});
206223
});

packages/schematics/angular/utility/validation.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,49 @@ export function validateHtmlSelector(selector: string): void {
2525
is invalid.`);
2626
}
2727
}
28+
29+
30+
export function validateProjectName(projectName: string) {
31+
const errorIndex = getRegExpFailPosition(projectName);
32+
const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app'];
33+
if (errorIndex !== null) {
34+
const firstMessage = tags.oneLine`
35+
Project name "${projectName}" is not valid. New project names must
36+
start with a letter, and must contain only alphanumeric characters or dashes.
37+
When adding a dash the segment after the dash must also start with a letter.
38+
`;
39+
const msg = tags.stripIndent`
40+
${firstMessage}
41+
${projectName}
42+
${Array(errorIndex + 1).join(' ') + '^'}
43+
`;
44+
throw new SchematicsException(msg);
45+
} else if (unsupportedProjectNames.indexOf(projectName) !== -1) {
46+
throw new SchematicsException(`Project name "${projectName}" is not a supported name.`);
47+
}
48+
}
49+
50+
function getRegExpFailPosition(str: string): number | null {
51+
const isScope = /^@.*\/.*/.test(str);
52+
if (isScope) {
53+
// Remove starting @
54+
str = str.replace(/^@/, '');
55+
// Change / to - for validation
56+
str = str.replace(/\//g, '-');
57+
}
58+
59+
const parts = str.indexOf('-') >= 0 ? str.split('-') : [str];
60+
const matched: string[] = [];
61+
62+
const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/;
63+
64+
parts.forEach(part => {
65+
if (part.match(projectNameRegexp)) {
66+
matched.push(part);
67+
}
68+
});
69+
70+
const compare = matched.join('-');
71+
72+
return (str !== compare) ? compare.length : null;
73+
}

0 commit comments

Comments
 (0)