From bbd62fe2fc43b43bb876d1612ab1c10e3f9194ab Mon Sep 17 00:00:00 2001 From: Mike Brocchi Date: Fri, 6 Apr 2018 17:32:24 -0400 Subject: [PATCH] fix(@schematics/angular): Allow for scoped library names fixes angular/angular-cli#10172 --- .../schematics/angular/application/index.ts | 40 +--------------- .../angular/application/schema.json | 1 - .../files/__projectRoot__/ng-package.json | 2 +- .../files/__projectRoot__/package.json | 2 +- packages/schematics/angular/library/index.ts | 37 +++++++++++---- .../schematics/angular/library/index_spec.ts | 39 ++++++++++++++++ .../schematics/angular/utility/validation.ts | 46 +++++++++++++++++++ 7 files changed, 116 insertions(+), 51 deletions(-) diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index 2c39be3b8d..0847450db1 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -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, @@ -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'; @@ -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) => { diff --git a/packages/schematics/angular/application/schema.json b/packages/schematics/angular/application/schema.json index 0bf4bd5469..9389d7ef95 100644 --- a/packages/schematics/angular/application/schema.json +++ b/packages/schematics/angular/application/schema.json @@ -12,7 +12,6 @@ "name": { "description": "The name of the application.", "type": "string", - "format": "html-selector", "$default": { "$source": "argv", "index": 0 diff --git a/packages/schematics/angular/library/files/__projectRoot__/ng-package.json b/packages/schematics/angular/library/files/__projectRoot__/ng-package.json index 123cc3811b..97c51503bb 100644 --- a/packages/schematics/angular/library/files/__projectRoot__/ng-package.json +++ b/packages/schematics/angular/library/files/__projectRoot__/ng-package.json @@ -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" diff --git a/packages/schematics/angular/library/files/__projectRoot__/package.json b/packages/schematics/angular/library/files/__projectRoot__/package.json index 66125d35f0..f72ab5bfbc 100644 --- a/packages/schematics/angular/library/files/__projectRoot__/package.json +++ b/packages/schematics/angular/library/files/__projectRoot__/package.json @@ -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", diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts index afa84cb591..c6a6ea3094 100644 --- a/packages/schematics/angular/library/index.ts +++ b/packages/schematics/angular/library/index.ts @@ -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'; @@ -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}`, @@ -166,7 +167,7 @@ function addAppToWorkspaceFile(options: LibraryOptions, workspace: WorkspaceSche }, }; - return addProjectToWorkspace(workspace, options.name, project); + return addProjectToWorkspace(workspace, packageName, project); } export default function (options: LibraryOptions): Rule { @@ -174,12 +175,27 @@ export default function (options: LibraryOptions): Rule { 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('/'); @@ -187,6 +203,7 @@ export default function (options: LibraryOptions): Rule { template({ ...strings, ...options, + packageName, projectRoot, relativeTsLintPath, prefix, @@ -198,19 +215,19 @@ 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, @@ -218,7 +235,7 @@ export default function (options: LibraryOptions): Rule { export: true, }), schematic('service', { - name: name, + name: options.name, flat: true, path: sourceDir, }), diff --git a/packages/schematics/angular/library/index_spec.ts b/packages/schematics/angular/library/index_spec.ts index 20be0b45b6..fcb17a69ed 100644 --- a/packages/schematics/angular/library/index_spec.ts +++ b/packages/schematics/angular/library/index_spec.ts @@ -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(); + }); }); diff --git a/packages/schematics/angular/utility/validation.ts b/packages/schematics/angular/utility/validation.ts index 0578d18948..e5d35cc965 100644 --- a/packages/schematics/angular/utility/validation.ts +++ b/packages/schematics/angular/utility/validation.ts @@ -25,3 +25,49 @@ 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']; + 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.`); + } +} + +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; +}