Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

Commit 2f16d7a

Browse files
committed
fix(@schematics/angular): Allow for scoped library names
fixes angular/angular-cli#10172
1 parent 8d48293 commit 2f16d7a

File tree

6 files changed

+87
-47
lines changed

6 files changed

+87
-47
lines changed

packages/schematics/angular/application/index.ts

+2-38
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 { normalize, relative, strings, tags } from '@angular-devkit/core';
8+
import { 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;
@@ -226,43 +227,6 @@ function addAppToWorkspaceFile(options: ApplicationOptions, workspace: Workspace
226227
host.overwrite(getWorkspacePath(host), JSON.stringify(workspace, null, 2));
227228
};
228229
}
229-
const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/;
230-
const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app'];
231-
232-
function getRegExpFailPosition(str: string): number | null {
233-
const parts = str.indexOf('-') >= 0 ? str.split('-') : [str];
234-
const matched: string[] = [];
235-
236-
parts.forEach(part => {
237-
if (part.match(projectNameRegexp)) {
238-
matched.push(part);
239-
}
240-
});
241-
242-
const compare = matched.join('-');
243-
244-
return (str !== compare) ? compare.length : null;
245-
}
246-
247-
function validateProjectName(projectName: string) {
248-
const errorIndex = getRegExpFailPosition(projectName);
249-
if (errorIndex !== null) {
250-
const firstMessage = tags.oneLine`
251-
Project name "${projectName}" is not valid. New project names must
252-
start with a letter, and must contain only alphanumeric characters or dashes.
253-
When adding a dash the segment after the dash must also start with a letter.
254-
`;
255-
const msg = tags.stripIndent`
256-
${firstMessage}
257-
${projectName}
258-
${Array(errorIndex + 1).join(' ') + '^'}
259-
`;
260-
throw new SchematicsException(msg);
261-
} else if (unsupportedProjectNames.indexOf(projectName) !== -1) {
262-
throw new SchematicsException(`Project name "${projectName}" is not a supported name.`);
263-
}
264-
265-
}
266230

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

packages/schematics/angular/application/schema.json

-1
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

+1-1
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

+21-7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from '@angular-devkit/schematics';
2323
import { WorkspaceSchema, getWorkspace, getWorkspacePath } from '../utility/config';
2424
import { latestVersions } from '../utility/latest-versions';
25+
import { validateProjectName } from '../utility/validation';
2526
import { Schema as LibraryOptions } from './schema';
2627

2728

@@ -171,17 +172,30 @@ export default function (options: LibraryOptions): Rule {
171172
if (!options.name) {
172173
throw new SchematicsException(`Invalid options, "name" is required.`);
173174
}
174-
const name = options.name;
175+
validateProjectName(options.name);
176+
177+
// If scoped project (i.e. "@foo/bar"), convert projectDir to "foo/bar".
178+
const packageName = options.name;
179+
let scopeName = '';
180+
if (/^@.*\/.*/.test(options.name)) {
181+
const [scope, name] = options.name.split('/');
182+
scopeName = scope.replace(/^@/, '');
183+
options.name = name;
184+
}
175185

176186
const workspace = getWorkspace(host);
177187
const newProjectRoot = workspace.newProjectRoot;
178-
const projectRoot = `${newProjectRoot}/${options.name}`;
188+
let projectRoot = `${newProjectRoot}/${options.name}`;
189+
if (scopeName) {
190+
projectRoot = `${newProjectRoot}/${scopeName}/${options.name}`;
191+
}
179192
const sourceDir = `${projectRoot}/src/lib`;
180193

181194
const templateSource = apply(url('./files'), [
182195
template({
183196
...strings,
184197
...options,
198+
packageName,
185199
projectRoot,
186200
}),
187201
// TODO: Moving inside `branchAndMerge` should work but is bugged right now.
@@ -193,27 +207,27 @@ export default function (options: LibraryOptions): Rule {
193207
branchAndMerge(mergeWith(templateSource)),
194208
addAppToWorkspaceFile(options, workspace),
195209
options.skipPackageJson ? noop() : addDependenciesToPackageJson(),
196-
options.skipTsConfig ? noop() : updateTsConfig(name),
210+
options.skipTsConfig ? noop() : updateTsConfig(options.name),
197211
schematic('module', {
198-
name: name,
212+
name: options.name,
199213
commonModule: false,
200214
flat: true,
201215
path: sourceDir,
202216
spec: false,
203217
}),
204218
schematic('component', {
205-
name: name,
219+
name: options.name,
206220
inlineStyle: true,
207221
inlineTemplate: true,
208222
flat: true,
209223
path: sourceDir,
210224
export: true,
211225
}),
212226
schematic('service', {
213-
name: name,
227+
name: options.name,
214228
flat: true,
215229
path: sourceDir,
216-
module: `${name}.module.ts`,
230+
module: `${options.name}.module.ts`,
217231
}),
218232
])(host, context);
219233
};

packages/schematics/angular/library/index_spec.ts

+17
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,21 @@ describe('Library Schematic', () => {
166166
expect(tsConfigJson.compilerOptions.paths).toBeUndefined();
167167
});
168168
});
169+
170+
it(`should support creating scoped libraries`, () => {
171+
const scopedName = '@myscope/mylib';
172+
const options = { ...defaultOptions, name: scopedName };
173+
const tree = schematicRunner.runSchematic('library', options, workspaceTree);
174+
175+
const pkgJsonPath = '/projects/myscope/mylib/package.json';
176+
expect(tree.files).toContain(pkgJsonPath);
177+
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.module.ts');
178+
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.component.ts');
179+
180+
const pkgJson = JSON.parse(tree.readContent(pkgJsonPath));
181+
expect(pkgJson.name).toEqual(scopedName);
182+
183+
const tsConfigJson = JSON.parse(tree.readContent('/projects/myscope/mylib/tsconfig.spec.json'));
184+
expect(tsConfigJson.extends).toEqual('../../../tsconfig.json');
185+
});
169186
});

packages/schematics/angular/utility/validation.ts

+46
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)