Skip to content

Commit 82bf0a4

Browse files
Broccohansl
authored andcommitted
fix(@schematics/angular): Allow for scoped library names
fixes #10172
1 parent e249a99 commit 82bf0a4

File tree

7 files changed

+116
-51
lines changed

7 files changed

+116
-51
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 { JsonObject, normalize, relative, strings, tags } from '@angular-devkit/core';
8+
import { JsonObject, normalize, relative, strings } from '@angular-devkit/core';
99
import {
1010
MergeStrategy,
1111
Rule,
@@ -30,6 +30,7 @@ import {
3030
getWorkspace,
3131
} from '../utility/config';
3232
import { latestVersions } from '../utility/latest-versions';
33+
import { validateProjectName } from '../utility/validation';
3334
import { Schema as ApplicationOptions } from './schema';
3435

3536

@@ -249,43 +250,6 @@ function addAppToWorkspaceFile(options: ApplicationOptions, workspace: Workspace
249250

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

290254
export default function (options: ApplicationOptions): Rule {
291255
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__/ng-package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "<%= projectRoot.split('/').map(x => '..').join('/') %>/node_modules/ng-packagr/ng-package.schema.json",
3-
"dest": "<%= projectRoot.split('/').map(x => '..').join('/') %>/dist/<%= dasherize(name) %>",
3+
"dest": "<%= projectRoot.split('/').map(x => '..').join('/') %>/dist/<%= dasherize(packageName) %>",
44
"deleteDestPath": false,
55
"lib": {
66
"entryFile": "src/<%= entryFile %>.ts"

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": "<%= dasherize(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

+27-10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
getWorkspace,
2929
} from '../utility/config';
3030
import { latestVersions } from '../utility/latest-versions';
31+
import { validateProjectName } from '../utility/validation';
3132
import { Schema as LibraryOptions } from './schema';
3233

3334

@@ -125,7 +126,7 @@ function addDependenciesToPackageJson() {
125126
}
126127

127128
function addAppToWorkspaceFile(options: LibraryOptions, workspace: WorkspaceSchema,
128-
projectRoot: string): Rule {
129+
projectRoot: string, packageName: string): Rule {
129130

130131
const project: WorkspaceProject = {
131132
root: `${projectRoot}`,
@@ -166,27 +167,43 @@ function addAppToWorkspaceFile(options: LibraryOptions, workspace: WorkspaceSche
166167
},
167168
};
168169

169-
return addProjectToWorkspace(workspace, options.name, project);
170+
return addProjectToWorkspace(workspace, packageName, project);
170171
}
171172

172173
export default function (options: LibraryOptions): Rule {
173174
return (host: Tree, context: SchematicContext) => {
174175
if (!options.name) {
175176
throw new SchematicsException(`Invalid options, "name" is required.`);
176177
}
177-
const name = options.name;
178178
const prefix = options.prefix || 'lib';
179179

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

186202
const templateSource = apply(url('./files'), [
187203
template({
188204
...strings,
189205
...options,
206+
packageName,
190207
projectRoot,
191208
relativeTsLintPath,
192209
prefix,
@@ -198,27 +215,27 @@ export default function (options: LibraryOptions): Rule {
198215

199216
return chain([
200217
branchAndMerge(mergeWith(templateSource)),
201-
addAppToWorkspaceFile(options, workspace, projectRoot),
218+
addAppToWorkspaceFile(options, workspace, projectRoot, packageName),
202219
options.skipPackageJson ? noop() : addDependenciesToPackageJson(),
203-
options.skipTsConfig ? noop() : updateTsConfig(name),
220+
options.skipTsConfig ? noop() : updateTsConfig(options.name),
204221
schematic('module', {
205-
name: name,
222+
name: options.name,
206223
commonModule: false,
207224
flat: true,
208225
path: sourceDir,
209226
spec: false,
210227
}),
211228
schematic('component', {
212-
name: name,
213-
selector: `${prefix}-${name}`,
229+
name: options.name,
230+
selector: `${prefix}-${options.name}`,
214231
inlineStyle: true,
215232
inlineTemplate: true,
216233
flat: true,
217234
path: sourceDir,
218235
export: true,
219236
}),
220237
schematic('service', {
221-
name: name,
238+
name: options.name,
222239
flat: true,
223240
path: sourceDir,
224241
}),

packages/schematics/angular/library/index_spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,43 @@ describe('Library Schematic', () => {
215215
tree = schematicRunner.runSchematic('component', componentOptions, tree);
216216
expect(tree.exists('/projects/foo/src/lib/comp/comp.component.ts')).toBe(true);
217217
});
218+
219+
it(`should support creating scoped libraries`, () => {
220+
const scopedName = '@myscope/mylib';
221+
const options = { ...defaultOptions, name: scopedName };
222+
const tree = schematicRunner.runSchematic('library', options, workspaceTree);
223+
224+
const pkgJsonPath = '/projects/myscope/mylib/package.json';
225+
expect(tree.files).toContain(pkgJsonPath);
226+
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.module.ts');
227+
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.component.ts');
228+
229+
const pkgJson = JSON.parse(tree.readContent(pkgJsonPath));
230+
expect(pkgJson.name).toEqual(scopedName);
231+
232+
const tsConfigJson = JSON.parse(tree.readContent('/projects/myscope/mylib/tsconfig.spec.json'));
233+
expect(tsConfigJson.extends).toEqual('../../../tsconfig.json');
234+
235+
const cfg = JSON.parse(tree.readContent('/angular.json'));
236+
expect(cfg.projects['@myscope/mylib']).toBeDefined();
237+
});
238+
239+
it(`should dasherize scoped libraries`, () => {
240+
const scopedName = '@myScope/myLib';
241+
const expectedScopeName = '@my-scope/my-lib';
242+
const options = { ...defaultOptions, name: scopedName };
243+
const tree = schematicRunner.runSchematic('library', options, workspaceTree);
244+
245+
const pkgJsonPath = '/projects/my-scope/my-lib/package.json';
246+
expect(tree.readContent(pkgJsonPath)).toContain(expectedScopeName);
247+
248+
const ngPkgJsonPath = '/projects/my-scope/my-lib/ng-package.json';
249+
expect(tree.readContent(ngPkgJsonPath)).toContain(expectedScopeName);
250+
251+
const pkgJson = JSON.parse(tree.readContent(pkgJsonPath));
252+
expect(pkgJson.name).toEqual(expectedScopeName);
253+
254+
const cfg = JSON.parse(tree.readContent('/angular.json'));
255+
expect(cfg.projects['@myScope/myLib']).toBeDefined();
256+
});
218257
});

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)