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

Commit 6996de1

Browse files
committed
fix(@schematics/angular): Allow for scoped library names
fixes angular/angular-cli#10172
1 parent 9d82814 commit 6996de1

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

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
@@ -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,19 +172,34 @@ 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;
175175
const prefix = options.prefix || 'lib';
176176

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

183198
const templateSource = apply(url('./files'), [
184199
template({
185200
...strings,
186201
...options,
202+
packageName,
187203
projectRoot,
188204
relativeTsLintPath,
189205
prefix,
@@ -197,25 +213,25 @@ export default function (options: LibraryOptions): Rule {
197213
branchAndMerge(mergeWith(templateSource)),
198214
addAppToWorkspaceFile(options, workspace, projectRoot),
199215
options.skipPackageJson ? noop() : addDependenciesToPackageJson(),
200-
options.skipTsConfig ? noop() : updateTsConfig(name),
216+
options.skipTsConfig ? noop() : updateTsConfig(options.name),
201217
schematic('module', {
202-
name: name,
218+
name: options.name,
203219
commonModule: false,
204220
flat: true,
205221
path: sourceDir,
206222
spec: false,
207223
}),
208224
schematic('component', {
209-
name: name,
210-
selector: `${prefix}-${name}`,
225+
name: options.name,
226+
selector: `${prefix}-${options.name}`,
211227
inlineStyle: true,
212228
inlineTemplate: true,
213229
flat: true,
214230
path: sourceDir,
215231
export: true,
216232
}),
217233
schematic('service', {
218-
name: name,
234+
name: options.name,
219235
flat: true,
220236
path: sourceDir,
221237
}),

packages/schematics/angular/library/index_spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,21 @@ describe('Library Schematic', () => {
188188
expect(tsConfigJson.compilerOptions.paths).toBeUndefined();
189189
});
190190
});
191+
192+
it(`should support creating scoped libraries`, () => {
193+
const scopedName = '@myscope/mylib';
194+
const options = { ...defaultOptions, name: scopedName };
195+
const tree = schematicRunner.runSchematic('library', options, workspaceTree);
196+
197+
const pkgJsonPath = '/projects/myscope/mylib/package.json';
198+
expect(tree.files).toContain(pkgJsonPath);
199+
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.module.ts');
200+
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.component.ts');
201+
202+
const pkgJson = JSON.parse(tree.readContent(pkgJsonPath));
203+
expect(pkgJson.name).toEqual(scopedName);
204+
205+
const tsConfigJson = JSON.parse(tree.readContent('/projects/myscope/mylib/tsconfig.spec.json'));
206+
expect(tsConfigJson.extends).toEqual('../../../tsconfig.json');
207+
});
191208
});

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)