diff --git a/packages/schematics/angular/class/index_spec.ts b/packages/schematics/angular/class/index_spec.ts index c5238636927c..76428ae1dfb9 100644 --- a/packages/schematics/angular/class/index_spec.ts +++ b/packages/schematics/angular/class/index_spec.ts @@ -109,4 +109,12 @@ describe('Class Schematic', () => { expect(tree.files).toContain('/projects/bar/src/app/foo.ts'); expect(tree.files).not.toContain('/projects/bar/src/app/foo.spec.ts'); }); + + it('should error when class name contains invalid characters', async () => { + const options = { ...defaultOptions, name: '1Clazz' }; + + await expectAsync( + schematicRunner.runSchematicAsync('class', options, appTree).toPromise(), + ).toBeRejectedWithError('Class name "1Clazz" is invalid.'); + }); }); diff --git a/packages/schematics/angular/component/index_spec.ts b/packages/schematics/angular/component/index_spec.ts index c496c0931545..d887a524b0d7 100644 --- a/packages/schematics/angular/component/index_spec.ts +++ b/packages/schematics/angular/component/index_spec.ts @@ -209,7 +209,7 @@ describe('Component Schematic', () => { await expectAsync( schematicRunner.runSchematicAsync('component', options, appTree).toPromise(), - ).toBeRejectedWithError('Selector (app-1-one) is invalid.'); + ).toBeRejectedWithError('Selector "app-1-one" is invalid.'); }); it('should use the default project prefix if none is passed', async () => { diff --git a/packages/schematics/angular/enum/index_spec.ts b/packages/schematics/angular/enum/index_spec.ts index 4279bf53a7fe..82327dea1c86 100644 --- a/packages/schematics/angular/enum/index_spec.ts +++ b/packages/schematics/angular/enum/index_spec.ts @@ -73,4 +73,12 @@ describe('Enum Schematic', () => { const tree = await schematicRunner.runSchematicAsync('enum', options, appTree).toPromise(); expect(tree.files).toContain('/projects/bar/src/app/foo.enum.ts'); }); + + it('should error when class name contains invalid characters', async () => { + const options = { ...defaultOptions, name: '1Clazz' }; + + await expectAsync( + schematicRunner.runSchematicAsync('enum', options, appTree).toPromise(), + ).toBeRejectedWithError('Class name "1Clazz" is invalid.'); + }); }); diff --git a/packages/schematics/angular/module/index.ts b/packages/schematics/angular/module/index.ts index 6c599c418bce..dfd52bdf35ef 100644 --- a/packages/schematics/angular/module/index.ts +++ b/packages/schematics/angular/module/index.ts @@ -33,6 +33,7 @@ import { findModuleFromOptions, } from '../utility/find-module'; import { parseName } from '../utility/parse-name'; +import { validateClassName } from '../utility/validation'; import { createDefaultPath } from '../utility/workspace'; import { Schema as ModuleOptions, RoutingScope } from './schema'; @@ -149,6 +150,7 @@ export default function (options: ModuleOptions): Rule { const parsedPath = parseName(options.path, options.name); options.name = parsedPath.name; options.path = parsedPath.path; + validateClassName(strings.classify(options.name)); const templateSource = apply(url('./files'), [ options.routing || (isLazyLoadedModuleGen && routingModulePath) diff --git a/packages/schematics/angular/module/index_spec.ts b/packages/schematics/angular/module/index_spec.ts index 3d759ab628de..0bbb9f22c64b 100644 --- a/packages/schematics/angular/module/index_spec.ts +++ b/packages/schematics/angular/module/index_spec.ts @@ -71,6 +71,15 @@ describe('Module Schematic', () => { expect(content).toMatch(/imports: \[[^\]]*FooModule[^\]]*\]/m); }); + it('should import into another module when using flat', async () => { + const options = { ...defaultOptions, flat: true, module: 'app.module.ts' }; + + const tree = await schematicRunner.runSchematicAsync('module', options, appTree).toPromise(); + const content = tree.readContent('/projects/bar/src/app/app.module.ts'); + expect(content).toMatch(/import { FooModule } from '.\/foo.module'/); + expect(content).toMatch(/imports: \[[^\]]*FooModule[^\]]*\]/m); + }); + it('should import into another module (deep)', async () => { let tree = appTree; diff --git a/packages/schematics/angular/pipe/index.ts b/packages/schematics/angular/pipe/index.ts index 5d6af0b6b1ab..d7ca4a153c03 100644 --- a/packages/schematics/angular/pipe/index.ts +++ b/packages/schematics/angular/pipe/index.ts @@ -8,7 +8,6 @@ import { Rule, - SchematicsException, Tree, apply, applyTemplates, @@ -25,6 +24,7 @@ import { addDeclarationToModule, addExportToModule } from '../utility/ast-utils' import { InsertChange } from '../utility/change'; import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; import { parseName } from '../utility/parse-name'; +import { validateClassName } from '../utility/validation'; import { createDefaultPath } from '../utility/workspace'; import { Schema as PipeOptions } from './schema'; @@ -84,15 +84,13 @@ function addDeclarationToNgModule(options: PipeOptions): Rule { export default function (options: PipeOptions): Rule { return async (host: Tree) => { - if (options.path === undefined) { - options.path = await createDefaultPath(host, options.project as string); - } - + options.path ??= await createDefaultPath(host, options.project as string); options.module = findModuleFromOptions(host, options); const parsedPath = parseName(options.path, options.name); options.name = parsedPath.name; options.path = parsedPath.path; + validateClassName(strings.classify(options.name)); const templateSource = apply(url('./files'), [ options.skipTests ? filter((path) => !path.endsWith('.spec.ts.template')) : noop(), diff --git a/packages/schematics/angular/pipe/index_spec.ts b/packages/schematics/angular/pipe/index_spec.ts index a4533ebbdca0..083b00a901c3 100644 --- a/packages/schematics/angular/pipe/index_spec.ts +++ b/packages/schematics/angular/pipe/index_spec.ts @@ -154,4 +154,12 @@ describe('Pipe Schematic', () => { expect(pipeContent).toContain('class FooPipe'); expect(moduleContent).not.toContain('FooPipe'); }); + + it('should error when class name contains invalid characters', async () => { + const options = { ...defaultOptions, name: '1Clazz' }; + + await expectAsync( + schematicRunner.runSchematicAsync('pipe', options, appTree).toPromise(), + ).toBeRejectedWithError('Class name "1Clazz" is invalid.'); + }); }); diff --git a/packages/schematics/angular/utility/generate-from-files.ts b/packages/schematics/angular/utility/generate-from-files.ts index b4be7c66ea12..d62b02bc92ad 100644 --- a/packages/schematics/angular/utility/generate-from-files.ts +++ b/packages/schematics/angular/utility/generate-from-files.ts @@ -20,6 +20,7 @@ import { url, } from '@angular-devkit/schematics'; import { parseName } from './parse-name'; +import { validateClassName } from './validation'; import { createDefaultPath } from './workspace'; export interface GenerateFromFilesOptions { @@ -44,6 +45,8 @@ export function generateFromFiles( options.name = parsedPath.name; options.path = parsedPath.path; + validateClassName(strings.classify(options.name)); + const templateSource = apply(url('./files'), [ options.skipTests ? filter((path) => !path.endsWith('.spec.ts.template')) : noop(), applyTemplates({ diff --git a/packages/schematics/angular/utility/validation.ts b/packages/schematics/angular/utility/validation.ts index 80ba0cd784da..619fe8e924b3 100644 --- a/packages/schematics/angular/utility/validation.ts +++ b/packages/schematics/angular/utility/validation.ts @@ -12,8 +12,17 @@ import { SchematicsException } from '@angular-devkit/schematics'; // When adding a dash the segment after the dash must also start with a letter. export const htmlSelectorRe = /^[a-zA-Z][.0-9a-zA-Z]*(:?-[a-zA-Z][.0-9a-zA-Z]*)*$/; +// See: https://github.com/tc39/proposal-regexp-unicode-property-escapes/blob/fe6d07fad74cd0192d154966baa1e95e7cda78a1/README.md#other-examples +const ecmaIdentifierNameRegExp = /^(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u; + export function validateHtmlSelector(selector: string): void { if (selector && !htmlSelectorRe.test(selector)) { - throw new SchematicsException(`Selector (${selector}) is invalid.`); + throw new SchematicsException(`Selector "${selector}" is invalid.`); + } +} + +export function validateClassName(className: string): void { + if (!ecmaIdentifierNameRegExp.test(className)) { + throw new SchematicsException(`Class name "${className}" is invalid.`); } }