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

fix(@schematics/angular): Allow for scoped library names #646

Merged
merged 2 commits into from
Apr 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 2 additions & 38 deletions packages/schematics/angular/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { JsonObject, normalize, relative, strings, tags } from '@angular-devkit/core';
import { JsonObject, normalize, relative, strings } from '@angular-devkit/core';
import {
MergeStrategy,
Rule,
Expand All @@ -30,6 +30,7 @@ import {
getWorkspace,
} from '../utility/config';
import { latestVersions } from '../utility/latest-versions';
import { validateProjectName } from '../utility/validation';
import { Schema as ApplicationOptions } from './schema';


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

return addProjectToWorkspace(workspace, options.name, project);
}
const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/;
const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app'];

function getRegExpFailPosition(str: string): number | null {
const parts = str.indexOf('-') >= 0 ? str.split('-') : [str];
const matched: string[] = [];

parts.forEach(part => {
if (part.match(projectNameRegexp)) {
matched.push(part);
}
});

const compare = matched.join('-');

return (str !== compare) ? compare.length : null;
}

function validateProjectName(projectName: string) {
const errorIndex = getRegExpFailPosition(projectName);
if (errorIndex !== null) {
const firstMessage = tags.oneLine`
Project name "${projectName}" is not valid. New project names must
start with a letter, and must contain only alphanumeric characters or dashes.
When adding a dash the segment after the dash must also start with a letter.
`;
const msg = tags.stripIndent`
${firstMessage}
${projectName}
${Array(errorIndex + 1).join(' ') + '^'}
`;
throw new SchematicsException(msg);
} else if (unsupportedProjectNames.indexOf(projectName) !== -1) {
throw new SchematicsException(`Project name "${projectName}" is not a supported name.`);
}

}

export default function (options: ApplicationOptions): Rule {
return (host: Tree, context: SchematicContext) => {
Expand Down
1 change: 0 additions & 1 deletion packages/schematics/angular/application/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"name": {
"description": "The name of the application.",
"type": "string",
"format": "html-selector",
"$default": {
"$source": "argv",
"index": 0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "<%= projectRoot.split('/').map(x => '..').join('/') %>/node_modules/ng-packagr/ng-package.schema.json",
"dest": "<%= projectRoot.split('/').map(x => '..').join('/') %>/dist/<%= dasherize(name) %>",
"dest": "<%= projectRoot.split('/').map(x => '..').join('/') %>/dist/<%= dasherize(packageName) %>",
"deleteDestPath": false,
"lib": {
"entryFile": "src/<%= entryFile %>.ts"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "<%= dasherize(name) %>",
"name": "<%= dasherize(packageName) %>",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^6.0.0-rc.0 || ^6.0.0",
Expand Down
37 changes: 27 additions & 10 deletions packages/schematics/angular/library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
getWorkspace,
} from '../utility/config';
import { latestVersions } from '../utility/latest-versions';
import { validateProjectName } from '../utility/validation';
import { Schema as LibraryOptions } from './schema';


Expand Down Expand Up @@ -125,7 +126,7 @@ function addDependenciesToPackageJson() {
}

function addAppToWorkspaceFile(options: LibraryOptions, workspace: WorkspaceSchema,
projectRoot: string): Rule {
projectRoot: string, packageName: string): Rule {

const project: WorkspaceProject = {
root: `${projectRoot}`,
Expand Down Expand Up @@ -166,27 +167,43 @@ function addAppToWorkspaceFile(options: LibraryOptions, workspace: WorkspaceSche
},
};

return addProjectToWorkspace(workspace, options.name, project);
return addProjectToWorkspace(workspace, packageName, project);
}

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

validateProjectName(options.name);

// If scoped project (i.e. "@foo/bar"), convert projectDir to "foo/bar".
const packageName = options.name;
let scopeName = '';
if (/^@.*\/.*/.test(options.name)) {
const [scope, name] = options.name.split('/');
scopeName = scope.replace(/^@/, '');
options.name = name;
}

const workspace = getWorkspace(host);
const newProjectRoot = workspace.newProjectRoot;
const projectRoot = `${newProjectRoot}/${strings.dasherize(options.name)}`;
let projectRoot = `${newProjectRoot}/${strings.dasherize(options.name)}`;
if (scopeName) {
projectRoot =
`${newProjectRoot}/${strings.dasherize(scopeName)}/${strings.dasherize(options.name)}`;
}

const sourceDir = `${projectRoot}/src/lib`;
const relativeTsLintPath = projectRoot.split('/').map(x => '..').join('/');

const templateSource = apply(url('./files'), [
template({
...strings,
...options,
packageName,
projectRoot,
relativeTsLintPath,
prefix,
Expand All @@ -198,27 +215,27 @@ export default function (options: LibraryOptions): Rule {

return chain([
branchAndMerge(mergeWith(templateSource)),
addAppToWorkspaceFile(options, workspace, projectRoot),
addAppToWorkspaceFile(options, workspace, projectRoot, packageName),
options.skipPackageJson ? noop() : addDependenciesToPackageJson(),
options.skipTsConfig ? noop() : updateTsConfig(name),
options.skipTsConfig ? noop() : updateTsConfig(options.name),
schematic('module', {
name: name,
name: options.name,
commonModule: false,
flat: true,
path: sourceDir,
spec: false,
}),
schematic('component', {
name: name,
selector: `${prefix}-${name}`,
name: options.name,
selector: `${prefix}-${options.name}`,
inlineStyle: true,
inlineTemplate: true,
flat: true,
path: sourceDir,
export: true,
}),
schematic('service', {
name: name,
name: options.name,
flat: true,
path: sourceDir,
}),
Expand Down
39 changes: 39 additions & 0 deletions packages/schematics/angular/library/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,43 @@ describe('Library Schematic', () => {
tree = schematicRunner.runSchematic('component', componentOptions, tree);
expect(tree.exists('/projects/foo/src/lib/comp/comp.component.ts')).toBe(true);
});

it(`should support creating scoped libraries`, () => {
const scopedName = '@myscope/mylib';
const options = { ...defaultOptions, name: scopedName };
const tree = schematicRunner.runSchematic('library', options, workspaceTree);

const pkgJsonPath = '/projects/myscope/mylib/package.json';
expect(tree.files).toContain(pkgJsonPath);
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.module.ts');
expect(tree.files).toContain('/projects/myscope/mylib/src/lib/mylib.component.ts');

const pkgJson = JSON.parse(tree.readContent(pkgJsonPath));
expect(pkgJson.name).toEqual(scopedName);

const tsConfigJson = JSON.parse(tree.readContent('/projects/myscope/mylib/tsconfig.spec.json'));
expect(tsConfigJson.extends).toEqual('../../../tsconfig.json');

const cfg = JSON.parse(tree.readContent('/angular.json'));
expect(cfg.projects['@myscope/mylib']).toBeDefined();
});

it(`should dasherize scoped libraries`, () => {
const scopedName = '@myScope/myLib';
const expectedScopeName = '@my-scope/my-lib';
const options = { ...defaultOptions, name: scopedName };
const tree = schematicRunner.runSchematic('library', options, workspaceTree);

const pkgJsonPath = '/projects/my-scope/my-lib/package.json';
expect(tree.readContent(pkgJsonPath)).toContain(expectedScopeName);

const ngPkgJsonPath = '/projects/my-scope/my-lib/ng-package.json';
expect(tree.readContent(ngPkgJsonPath)).toContain(expectedScopeName);

const pkgJson = JSON.parse(tree.readContent(pkgJsonPath));
expect(pkgJson.name).toEqual(expectedScopeName);

const cfg = JSON.parse(tree.readContent('/angular.json'));
expect(cfg.projects['@myScope/myLib']).toBeDefined();
});
});
50 changes: 50 additions & 0 deletions packages/schematics/angular/utility/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,53 @@ export function validateHtmlSelector(selector: string): void {
is invalid.`);
}
}


export function validateProjectName(projectName: string) {
const errorIndex = getRegExpFailPosition(projectName);
const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app'];
const packageNameRegex = /^(?:@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9_-]+$/;
if (errorIndex !== null) {
const firstMessage = tags.oneLine`
Project name "${projectName}" is not valid. New project names must
start with a letter, and must contain only alphanumeric characters or dashes.
When adding a dash the segment after the dash must also start with a letter.
`;
const msg = tags.stripIndent`
${firstMessage}
${projectName}
${Array(errorIndex + 1).join(' ') + '^'}
`;
throw new SchematicsException(msg);
} else if (unsupportedProjectNames.indexOf(projectName) !== -1) {
throw new SchematicsException(
`Project name ${JSON.stringify(projectName)} is not a supported name.`);
} else if (!packageNameRegex.test(projectName)) {
throw new SchematicsException(`Project name ${JSON.stringify(projectName)} is invalid.`);
}
}

function getRegExpFailPosition(str: string): number | null {
const isScope = /^@.*\/.*/.test(str);
if (isScope) {
// Remove starting @
str = str.replace(/^@/, '');
// Change / to - for validation
str = str.replace(/\//g, '-');
}

const parts = str.indexOf('-') >= 0 ? str.split('-') : [str];
const matched: string[] = [];

const projectNameRegexp = /^[a-zA-Z][.0-9a-zA-Z]*(-[.0-9a-zA-Z]*)*$/;

parts.forEach(part => {
if (part.match(projectNameRegexp)) {
matched.push(part);
}
});

const compare = matched.join('-');

return (str !== compare) ? compare.length : null;
}