diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index ac1210df32eaa..4b5447bddd455 100644 --- a/docs/generated/cli/create-nx-workspace.md +++ b/docs/generated/cli/create-nx-workspace.md @@ -135,6 +135,12 @@ Default: `npm` Package manager to use +### prefix + +Type: `string` + +Prefix to use for Angular component and directive selectors. + ### preset Type: `string` diff --git a/docs/generated/packages/angular/generators/application.json b/docs/generated/packages/angular/generators/application.json index 2cfad4c3b36c2..6123da361f897 100644 --- a/docs/generated/packages/angular/generators/application.json +++ b/docs/generated/packages/angular/generators/application.json @@ -78,6 +78,7 @@ "type": "string", "format": "html-selector", "description": "The prefix to apply to generated selectors.", + "default": "app", "alias": "p" }, "skipTests": { diff --git a/docs/generated/packages/nx/documents/create-nx-workspace.md b/docs/generated/packages/nx/documents/create-nx-workspace.md index ac1210df32eaa..4b5447bddd455 100644 --- a/docs/generated/packages/nx/documents/create-nx-workspace.md +++ b/docs/generated/packages/nx/documents/create-nx-workspace.md @@ -135,6 +135,12 @@ Default: `npm` Package manager to use +### prefix + +Type: `string` + +Prefix to use for Angular component and directive selectors. + ### preset Type: `string` diff --git a/docs/generated/packages/workspace/generators/new.json b/docs/generated/packages/workspace/generators/new.json index f1d31c68b83bf..c0e68a442b725 100644 --- a/docs/generated/packages/workspace/generators/new.json +++ b/docs/generated/packages/workspace/generators/new.json @@ -79,6 +79,10 @@ "description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.", "type": "boolean", "default": false + }, + "prefix": { + "description": "The prefix to use for Angular component and directive selectors.", + "type": "string" } }, "additionalProperties": true, diff --git a/docs/generated/packages/workspace/generators/preset.json b/docs/generated/packages/workspace/generators/preset.json index a04f2482976eb..9b050e4602033 100644 --- a/docs/generated/packages/workspace/generators/preset.json +++ b/docs/generated/packages/workspace/generators/preset.json @@ -96,6 +96,10 @@ "description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.", "type": "boolean", "default": false + }, + "prefix": { + "description": "The prefix to use for Angular component and directive selectors.", + "type": "string" } }, "required": ["preset", "name"], diff --git a/e2e/angular-module-federation/src/module-federation.test.ts b/e2e/angular-module-federation/src/module-federation.test.ts index b9301f1f7326d..2eb6f5dbc650a 100644 --- a/e2e/angular-module-federation/src/module-federation.test.ts +++ b/e2e/angular-module-federation/src/module-federation.test.ts @@ -346,7 +346,7 @@ describe('Angular Module Federation', () => { import { isEven } from '${remote}/${module}'; @Component({ - selector: 'proj-root', + selector: 'app-root', template: \`
{{title}}
\`, standalone: true }) @@ -433,7 +433,7 @@ describe('Angular Module Federation', () => { import { isEven } from '${childRemote}/${module}'; @Component({ - selector: 'proj-${remote}-entry', + selector: 'app-${remote}-entry', template: \`
{{title}}
\`, standalone: true }) diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index a2d70c2ca4bc0..06b13e47167a9 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -233,6 +233,7 @@ export function runCreateWorkspace( e2eTestRunner, ssr, framework, + prefix, }: { preset: string; appName?: string; @@ -251,6 +252,7 @@ export function runCreateWorkspace( e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none'; ssr?: boolean; framework?: string; + prefix?: string; } ) { projName = name; @@ -317,6 +319,10 @@ export function runCreateWorkspace( command += ` --ssr=${ssr}`; } + if (prefix !== undefined) { + command += ` --prefix=${prefix}`; + } + try { const create = execSync(`${command}${isVerbose() ? ' --verbose' : ''}`, { cwd, diff --git a/e2e/workspace-create/src/create-nx-workspace.test.ts b/e2e/workspace-create/src/create-nx-workspace.test.ts index de6e7aa57602b..cc1e75811b663 100644 --- a/e2e/workspace-create/src/create-nx-workspace.test.ts +++ b/e2e/workspace-create/src/create-nx-workspace.test.ts @@ -156,8 +156,7 @@ describe('create-nx-workspace', () => { it('should fail correctly when preset errors', () => { // Using Angular Preset as the example here to test - // It will error when npmScope is of form `--` - // Due to a validation error Angular will throw. + // It will error when prefix is not valid const wsName = uniq('angular-1-test'); const appName = uniq('app'); expect(() => @@ -171,6 +170,7 @@ describe('create-nx-workspace', () => { e2eTestRunner: 'none', bundler: 'webpack', ssr: false, + prefix: '1-one', }) ).toThrow(); }); diff --git a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap index ec3fc174c7d68..bee93be1070ed 100644 --- a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap @@ -24,7 +24,7 @@ exports[`app --minimal should skip "nx-welcome.component.ts" file and references "import { Component } from '@angular/core'; @Component({ - selector: 'proj-root', + selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.css', }) @@ -83,7 +83,7 @@ exports[`app --minimal should skip "nx-welcome.component.ts" file and references "import { Component } from '@angular/core'; @Component({ - selector: 'proj-root', + selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.css', }) @@ -127,7 +127,7 @@ import { RouterModule } from '@angular/router'; @Component({ standalone: true, imports: [RouterModule], - selector: 'proj-root', + selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.css', }) @@ -170,7 +170,7 @@ exports[`app --minimal should skip "nx-welcome.component.ts" file and references @Component({ standalone: true, imports: [], - selector: 'proj-root', + selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.css', }) @@ -210,7 +210,7 @@ exports[`app --project-name-and-root-format=derived should generate correctly wh { "$schema": "../../../node_modules/nx/schemas/project-schema.json", "name": "my-dir-my-app", - "prefix": "proj", + "prefix": "app", "projectType": "application", "root": "apps/my-dir/my-app", "sourceRoot": "apps/my-dir/my-app/src", @@ -409,7 +409,7 @@ exports[`app --project-name-and-root-format=derived should generate correctly wh { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "my-app", - "prefix": "proj", + "prefix": "app", "projectType": "application", "root": "apps/my-app", "sourceRoot": "apps/my-app/src", @@ -641,7 +641,7 @@ import { NxWelcomeComponent } from './nx-welcome.component'; @Component({ standalone: true, imports: [NxWelcomeComponent, RouterModule], - selector: 'proj-root', + selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.css', }) @@ -709,7 +709,7 @@ import { NxWelcomeComponent } from './nx-welcome.component'; @Component({ standalone: true, imports: [NxWelcomeComponent, ], - selector: 'proj-root', + selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.css', }) @@ -850,7 +850,7 @@ exports[`app format files should format files 2`] = ` "import { Component } from '@angular/core'; @Component({ - selector: 'proj-root', + selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.css', }) @@ -903,7 +903,7 @@ exports[`app nested should create project configs 1`] = ` { "$schema": "../../node_modules/nx/schemas/project-schema.json", "name": "my-app", - "prefix": "proj", + "prefix": "app", "projectType": "application", "root": "my-dir/my-app", "sourceRoot": "my-dir/my-app/src", @@ -1015,7 +1015,7 @@ exports[`app not nested should create project configs 1`] = ` { "$schema": "../node_modules/nx/schemas/project-schema.json", "name": "my-app", - "prefix": "proj", + "prefix": "app", "projectType": "application", "root": "my-app", "sourceRoot": "my-app/src", diff --git a/packages/angular/src/generators/application/application.spec.ts b/packages/angular/src/generators/application/application.spec.ts index b5ea41c6de7e9..580420f8cd915 100644 --- a/packages/angular/src/generators/application/application.spec.ts +++ b/packages/angular/src/generators/application/application.spec.ts @@ -449,7 +449,7 @@ describe('app', () => { await generateApp(appTree, 'my-app', { directory: 'my-dir/my-app' }); expect( appTree.read('my-dir/my-app/src/app/app.component.html', 'utf-8') - ).toContain(''); + ).toContain(''); }); it("should update `template`'s property of AppComponent with Nx content", async () => { @@ -459,7 +459,7 @@ describe('app', () => { }); expect( appTree.read('my-dir/my-app/src/app/app.component.ts', 'utf-8') - ).toContain(''); + ).toContain(''); }); it('should create Nx specific `nx-welcome.component.ts` file', async () => { @@ -598,7 +598,7 @@ describe('app', () => { "@angular-eslint/component-selector": [ "error", { - "prefix": "proj", + "prefix": "app", "style": "kebab-case", "type": "element", }, @@ -606,7 +606,7 @@ describe('app', () => { "@angular-eslint/directive-selector": [ "error", { - "prefix": "proj", + "prefix": "app", "style": "camelCase", "type": "attribute", }, diff --git a/packages/angular/src/generators/application/lib/create-files.ts b/packages/angular/src/generators/application/lib/create-files.ts index 8bc45d738179a..2777ba0259d0f 100644 --- a/packages/angular/src/generators/application/lib/create-files.ts +++ b/packages/angular/src/generators/application/lib/create-files.ts @@ -5,6 +5,7 @@ import { getRelativePathToRootTsConfig, getRootTsConfigFileName } from '@nx/js'; import { createTsConfig } from '../../utils/create-ts-config'; import { UnitTestRunner } from '../../../utils/test-runners'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; +import { validateHtmlSelector } from '../../utils/selector'; export async function createFiles( tree: Tree, @@ -15,8 +16,13 @@ export async function createFiles( const isUsingApplicationBuilder = angularMajorVersion >= 17 && options.bundler === 'esbuild'; + const rootSelector = `${options.prefix}-root`; + validateHtmlSelector(rootSelector); + const nxWelcomeSelector = `${options.prefix}-nx-welcome`; + validateHtmlSelector(nxWelcomeSelector); + const substitutions = { - rootSelector: `${options.prefix}-root`, + rootSelector, appName: options.name, inlineStyle: options.inlineStyle, inlineTemplate: options.inlineTemplate, @@ -25,7 +31,7 @@ export async function createFiles( unitTesting: options.unitTestRunner !== UnitTestRunner.None, routing: options.routing, minimal: options.minimal, - nxWelcomeSelector: `${options.prefix}-nx-welcome`, + nxWelcomeSelector, rootTsConfig: joinPathFragments(rootOffset, getRootTsConfigFileName(tree)), angularMajorVersion, rootOffset, diff --git a/packages/angular/src/generators/application/lib/normalize-options.ts b/packages/angular/src/generators/application/lib/normalize-options.ts index e8d989d216d9a..229d676918921 100644 --- a/packages/angular/src/generators/application/lib/normalize-options.ts +++ b/packages/angular/src/generators/application/lib/normalize-options.ts @@ -1,9 +1,7 @@ import { joinPathFragments, type Tree } from '@nx/devkit'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Linter } from '@nx/eslint'; -import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; import { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners'; -import { normalizeNewProjectPrefix } from '../../utils/project'; import type { Schema } from '../schema'; import type { NormalizedSchema } from './normalized-schema'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; @@ -34,12 +32,6 @@ export async function normalizeOptions( ? options.tags.split(',').map((s) => s.trim()) : []; - const prefix = normalizeNewProjectPrefix( - options.prefix, - getNpmScope(host), - 'app' - ); - let bundler = options.bundler; if (!bundler) { const { major: angularMajorVersion } = getInstalledAngularVersionInfo(host); @@ -60,7 +52,7 @@ export async function normalizeOptions( strict: true, standalone: true, ...options, - prefix, + prefix: options.prefix || 'app', name: appProjectName, appProjectRoot, appProjectSourceRoot: `${appProjectRoot}/src`, diff --git a/packages/angular/src/generators/application/schema.json b/packages/angular/src/generators/application/schema.json index daf56df4bd41c..3b915e47857fc 100644 --- a/packages/angular/src/generators/application/schema.json +++ b/packages/angular/src/generators/application/schema.json @@ -81,6 +81,7 @@ "type": "string", "format": "html-selector", "description": "The prefix to apply to generated selectors.", + "default": "app", "alias": "p" }, "skipTests": { diff --git a/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap b/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap index 11bf85802ec83..d082b8eae4418 100644 --- a/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap +++ b/packages/angular/src/generators/component/__snapshots__/component.spec.ts.snap @@ -4,7 +4,7 @@ exports[`component Generator --flat should create the component correctly and ex "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -16,7 +16,7 @@ exports[`component Generator --flat should create the component correctly and no "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -41,7 +41,7 @@ exports[`component Generator --path should create the component correctly and ex "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -53,7 +53,7 @@ exports[`component Generator compat should inline styles when --inline-style=tru "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styles: \`\` }) @@ -65,7 +65,7 @@ exports[`component Generator secondary entry points should create the component "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -82,7 +82,7 @@ exports[`component Generator should create component files correctly: component "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css', }) @@ -131,7 +131,7 @@ exports[`component Generator should create the component correctly and export it "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -149,7 +149,7 @@ exports[`component Generator should create the component correctly and export it import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', standalone: true, imports: [CommonModule], templateUrl: './example.component.html', @@ -163,7 +163,7 @@ exports[`component Generator should create the component correctly and not expor "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -176,7 +176,7 @@ exports[`component Generator should create the component correctly and not expor import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', standalone: true, imports: [CommonModule], templateUrl: './example.component.html', @@ -190,7 +190,7 @@ exports[`component Generator should create the component correctly and not expor "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -202,7 +202,7 @@ exports[`component Generator should create the component correctly but not expor "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -214,7 +214,7 @@ exports[`component Generator should inline styles when --inline-style=true 1`] = "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styles: \`\` }) @@ -226,7 +226,7 @@ exports[`component Generator should inline template when --inline-template=true "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', template: \`

example works!

\`, styleUrl: './example.component.css' }) diff --git a/packages/angular/src/generators/component/component.spec.ts b/packages/angular/src/generators/component/component.spec.ts index 3a5f8aa2e54be..ba264f0d2d489 100644 --- a/packages/angular/src/generators/component/component.spec.ts +++ b/packages/angular/src/generators/component/component.spec.ts @@ -1,5 +1,12 @@ -import { addProjectConfiguration, writeJson } from '@nx/devkit'; +import { + Tree, + addProjectConfiguration, + readProjectConfiguration, + updateProjectConfiguration, + writeJson, +} from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { AngularProjectConfiguration } from '../../utils/types'; import { componentGenerator } from './component'; describe('component Generator', () => { @@ -202,7 +209,7 @@ describe('component Generator', () => { "import { Component } from '@angular/core'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html' }) export class ExampleComponent {} @@ -885,6 +892,108 @@ export class LibModule {} }); }); + describe('prefix & selector', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'lib1', { + projectType: 'library', + root: 'lib1', + }); + }); + + it('should use the prefix', async () => { + await componentGenerator(tree, { + name: 'lib1/src/lib/example/example', + prefix: 'foo', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read( + 'lib1/src/lib/example/example.component.ts', + 'utf-8' + ); + expect(content).toMatch(/selector: 'foo-example'/); + }); + + it('should error when name starts with a digit', async () => { + await expect( + componentGenerator(tree, { + name: 'lib1/src/lib/1-one/1-one', + prefix: 'foo', + nameAndDirectoryFormat: 'as-provided', + }) + ).rejects.toThrow('The selector "foo-1-one" is invalid.'); + }); + + it('should allow dash in selector before a number', async () => { + await componentGenerator(tree, { + name: 'lib1/src/lib/one-1/one-1', + prefix: 'foo', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read( + 'lib1/src/lib/one-1/one-1.component.ts', + 'utf-8' + ); + expect(content).toMatch(/selector: 'foo-one-1'/); + }); + + it('should allow dash in selector before a number and without a prefix', async () => { + await componentGenerator(tree, { + name: 'lib1/src/lib/example/example', + selector: 'one-1', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read( + 'lib1/src/lib/example/example.component.ts', + 'utf-8' + ); + expect(content).toMatch(/selector: 'one-1'/); + }); + + it('should use the default project prefix if none is passed', async () => { + const projectConfig = readProjectConfiguration(tree, 'lib1'); + updateProjectConfiguration(tree, 'lib1', { + ...projectConfig, + prefix: 'bar', + } as AngularProjectConfiguration); + + await componentGenerator(tree, { + name: 'lib1/src/lib/example/example', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read( + 'lib1/src/lib/example/example.component.ts', + 'utf-8' + ); + expect(content).toMatch(/selector: 'bar-example'/); + }); + + it('should not use the default project prefix when supplied prefix is ""', async () => { + const projectConfig = readProjectConfiguration(tree, 'lib1'); + updateProjectConfiguration(tree, 'lib1', { + ...projectConfig, + prefix: '', + } as AngularProjectConfiguration); + + await componentGenerator(tree, { + name: 'lib1/src/lib/example/example', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read( + 'lib1/src/lib/example/example.component.ts', + 'utf-8' + ); + expect(content).toMatch(/selector: 'example'/); + }); + }); + describe('secondary entry points', () => { it('should create the component correctly and export it in the entry point', async () => { // ARRANGE diff --git a/packages/angular/src/generators/component/lib/normalize-options.ts b/packages/angular/src/generators/component/lib/normalize-options.ts index 180044ebe24a8..3f0bc08ee286e 100644 --- a/packages/angular/src/generators/component/lib/normalize-options.ts +++ b/packages/angular/src/generators/component/lib/normalize-options.ts @@ -2,7 +2,7 @@ import type { Tree } from '@nx/devkit'; import { names, readProjectConfiguration } from '@nx/devkit'; import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import type { AngularProjectConfiguration } from '../../../utils/types'; -import { buildSelector } from '../../utils/selector'; +import { buildSelector, validateHtmlSelector } from '../../utils/selector'; import type { NormalizedSchema, Schema } from '../schema'; export async function normalizeOptions( @@ -37,8 +37,8 @@ export async function normalizeOptions( ) as AngularProjectConfiguration; const selector = - options.selector ?? - buildSelector(tree, name, options.prefix, prefix, 'fileName'); + options.selector ?? buildSelector(name, options.prefix, prefix, 'fileName'); + validateHtmlSelector(selector); return { ...options, diff --git a/packages/angular/src/generators/directive/__snapshots__/directive.spec.ts.snap b/packages/angular/src/generators/directive/__snapshots__/directive.spec.ts.snap index 4e3a07738ffa3..c837afba4d214 100644 --- a/packages/angular/src/generators/directive/__snapshots__/directive.spec.ts.snap +++ b/packages/angular/src/generators/directive/__snapshots__/directive.spec.ts.snap @@ -16,7 +16,7 @@ exports[`directive generator --no-standalone should generate a directive with te "import { Directive } from '@angular/core'; @Directive({ - selector: '[projTest]', + selector: '[test]', }) export class TestDirective { constructor() {} @@ -52,7 +52,7 @@ exports[`directive generator --no-standalone should import the directive correct "import { Directive } from '@angular/core'; @Directive({ - selector: '[projTest]' + selector: '[test]' }) export class TestDirective { constructor() {} @@ -88,7 +88,7 @@ exports[`directive generator --no-standalone should import the directive correct "import { Directive } from '@angular/core'; @Directive({ - selector: '[projTest]' + selector: '[test]' }) export class TestDirective { constructor() {} @@ -124,7 +124,7 @@ exports[`directive generator should generate correctly 1`] = ` "import { Directive } from '@angular/core'; @Directive({ - selector: '[projTest]', + selector: '[test]', standalone: true, }) export class TestDirective { diff --git a/packages/angular/src/generators/directive/directive.spec.ts b/packages/angular/src/generators/directive/directive.spec.ts index 22bed620cf069..94e1f344edd20 100644 --- a/packages/angular/src/generators/directive/directive.spec.ts +++ b/packages/angular/src/generators/directive/directive.spec.ts @@ -1,5 +1,11 @@ -import { addProjectConfiguration, Tree } from '@nx/devkit'; +import { + addProjectConfiguration, + readProjectConfiguration, + updateProjectConfiguration, + type Tree, +} from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import type { AngularProjectConfiguration } from '../../utils/types'; import { directiveGenerator } from './directive'; import type { Schema } from './schema'; @@ -164,6 +170,74 @@ describe('directive generator', () => { ); }); }); + + describe('prefix & selector', () => { + it('should use the prefix', async () => { + await directiveGenerator(tree, { + name: 'test/src/app/example/example', + prefix: 'foo', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read( + 'test/src/app/example/example.directive.ts', + 'utf-8' + ); + expect(content).toMatch(/selector: '\[fooExample\]'/); + }); + + it('should use the default project prefix if none is passed', async () => { + const projectConfig = readProjectConfiguration(tree, 'test'); + updateProjectConfiguration(tree, 'test', { + ...projectConfig, + prefix: 'bar', + } as AngularProjectConfiguration); + + await directiveGenerator(tree, { + name: 'test/src/app/example/example', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read( + 'test/src/app/example/example.directive.ts', + 'utf-8' + ); + expect(content).toMatch(/selector: '\[barExample\]'/); + }); + + it('should not use the default project prefix when supplied prefix is ""', async () => { + const projectConfig = readProjectConfiguration(tree, 'test'); + updateProjectConfiguration(tree, 'test', { + ...projectConfig, + prefix: '', + } as AngularProjectConfiguration); + + await directiveGenerator(tree, { + name: 'test/src/app/example/example', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read( + 'test/src/app/example/example.directive.ts', + 'utf-8' + ); + expect(content).toMatch(/selector: '\[example\]'/); + }); + + it('should use provided selector as is', async () => { + await directiveGenerator(tree, { + name: 'test/src/app/example/example', + selector: 'mySelector', + nameAndDirectoryFormat: 'as-provided', + }); + + const content = tree.read( + 'test/src/app/example/example.directive.ts', + 'utf-8' + ); + expect(content).toMatch(/selector: '\[mySelector\]'/); + }); + }); }); function addModule(tree: Tree) { diff --git a/packages/angular/src/generators/directive/lib/normalize-options.ts b/packages/angular/src/generators/directive/lib/normalize-options.ts index ac797d3118dbc..c89a213a10a91 100644 --- a/packages/angular/src/generators/directive/lib/normalize-options.ts +++ b/packages/angular/src/generators/directive/lib/normalize-options.ts @@ -1,7 +1,7 @@ import type { Tree } from '@nx/devkit'; import { names, readProjectConfiguration } from '@nx/devkit'; import type { AngularProjectConfiguration } from '../../../utils/types'; -import { buildSelector } from '../../utils/selector'; +import { buildSelector, validateHtmlSelector } from '../../utils/selector'; import type { NormalizedSchema, Schema } from '../schema'; import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; @@ -37,7 +37,8 @@ export async function normalizeOptions( const selector = options.selector ?? - buildSelector(tree, name, options.prefix, prefix, 'propertyName'); + buildSelector(name, options.prefix, prefix, 'propertyName'); + validateHtmlSelector(selector); return { ...options, diff --git a/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap b/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap index 9f7c499bff352..2e50e3c16472e 100644 --- a/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap +++ b/packages/angular/src/generators/host/__snapshots__/host.spec.ts.snap @@ -954,7 +954,7 @@ import { NxWelcomeComponent } from './nx-welcome.component'; @Component({ standalone: true, imports: [NxWelcomeComponent, RouterModule], - selector: 'proj-root', + selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.css', }) diff --git a/packages/angular/src/generators/library/__snapshots__/library.spec.ts.snap b/packages/angular/src/generators/library/__snapshots__/library.spec.ts.snap index d8a5ae5a36287..01c4bab972e04 100644 --- a/packages/angular/src/generators/library/__snapshots__/library.spec.ts.snap +++ b/packages/angular/src/generators/library/__snapshots__/library.spec.ts.snap @@ -7,7 +7,7 @@ exports[`lib --standalone should generate a library with a standalone component import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-my-lib', + selector: 'lib-my-lib', standalone: true, imports: [CommonModule], templateUrl: './my-lib.component.html', @@ -53,7 +53,7 @@ exports[`lib --standalone should generate a library with a standalone component import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-my-lib', + selector: 'lib-my-lib', standalone: true, imports: [CommonModule], templateUrl: './my-lib.component.html', @@ -105,7 +105,7 @@ exports[`lib --standalone should generate a library with a standalone component import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-my-lib', + selector: 'lib-my-lib', standalone: true, imports: [CommonModule], templateUrl: './my-lib.component.html', @@ -147,7 +147,7 @@ exports[`lib --standalone should generate a library with a standalone component import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-my-lib', + selector: 'lib-my-lib', standalone: true, imports: [CommonModule], template: \`

my-lib works!

\`, @@ -166,7 +166,7 @@ exports[`lib --standalone should generate a library with a standalone component import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-my-lib', + selector: 'lib-my-lib', standalone: true, imports: [CommonModule], template: \`

my-lib works!

\`, @@ -183,7 +183,7 @@ exports[`lib --standalone should generate a library with a standalone component import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-my-lib', + selector: 'lib-my-lib', standalone: true, imports: [CommonModule], template: \`

my-lib works!

\`, @@ -239,7 +239,7 @@ exports[`lib --standalone should generate a library with a standalone component import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-my-lib', + selector: 'lib-my-lib', standalone: true, imports: [CommonModule], templateUrl: './my-lib.component.html', @@ -353,7 +353,7 @@ exports[`lib --standalone should generate a library with a standalone component import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-my-lib', + selector: 'lib-my-lib', standalone: true, imports: [CommonModule], templateUrl: './my-lib.component.html', @@ -395,7 +395,7 @@ exports[`lib --standalone should generate a library with a standalone component import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-my-lib', + selector: 'lib-my-lib', standalone: true, imports: [CommonModule], templateUrl: './my-lib.component.html', diff --git a/packages/angular/src/generators/library/lib/normalize-options.ts b/packages/angular/src/generators/library/lib/normalize-options.ts index e52c98622c386..1a412ddaf5e9a 100644 --- a/packages/angular/src/generators/library/lib/normalize-options.ts +++ b/packages/angular/src/generators/library/lib/normalize-options.ts @@ -1,9 +1,7 @@ import { names, Tree } from '@nx/devkit'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; import { Linter } from '@nx/eslint'; import { UnitTestRunner } from '../../../utils/test-runners'; -import { normalizeNewProjectPrefix } from '../../utils/project'; import { Schema } from '../schema'; import { NormalizedSchema } from './normalized-schema'; @@ -52,15 +50,12 @@ export async function normalizeOptions( : []; const modulePath = `${projectRoot}/src/lib/${fileName}.module.ts`; - const npmScope = getNpmScope(host); - const prefix = normalizeNewProjectPrefix(options.prefix, npmScope, 'lib'); - const ngCliSchematicLibRoot = projectName; const allNormalizedOptions = { ...options, linter: options.linter ?? Linter.EsLint, unitTestRunner: options.unitTestRunner ?? UnitTestRunner.Jest, - prefix, + prefix: options.prefix ?? 'lib', name: projectName, projectRoot, entryFile: 'index', diff --git a/packages/angular/src/generators/library/library.spec.ts b/packages/angular/src/generators/library/library.spec.ts index 775baa85ceb3e..00b57d2af5549 100644 --- a/packages/angular/src/generators/library/library.spec.ts +++ b/packages/angular/src/generators/library/library.spec.ts @@ -652,7 +652,7 @@ describe('lib', () => { "error", { "type": "attribute", - "prefix": "proj", + "prefix": "lib", "style": "camelCase" } ], @@ -660,7 +660,7 @@ describe('lib', () => { "error", { "type": "element", - "prefix": "proj", + "prefix": "lib", "style": "kebab-case" } ] @@ -1201,7 +1201,7 @@ describe('lib', () => { "@angular-eslint/component-selector": [ "error", { - "prefix": "proj", + "prefix": "lib", "style": "kebab-case", "type": "element", }, @@ -1209,7 +1209,7 @@ describe('lib', () => { "@angular-eslint/directive-selector": [ "error", { - "prefix": "proj", + "prefix": "lib", "style": "camelCase", "type": "attribute", }, @@ -1261,7 +1261,7 @@ describe('lib', () => { "@angular-eslint/component-selector": [ "error", { - "prefix": "proj", + "prefix": "lib", "style": "kebab-case", "type": "element", }, @@ -1269,7 +1269,7 @@ describe('lib', () => { "@angular-eslint/directive-selector": [ "error", { - "prefix": "proj", + "prefix": "lib", "style": "camelCase", "type": "attribute", }, diff --git a/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap b/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap index 0d49b11393ac3..a06a027cc6f83 100644 --- a/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap +++ b/packages/angular/src/generators/remote/__snapshots__/remote.spec.ts.snap @@ -224,8 +224,8 @@ exports[`MF Remote App Generator --ssr should generate the correct files 8`] = ` "import { Component } from '@angular/core'; @Component({ - selector: 'proj-test-entry', - template: \`\`, + selector: 'app-test-entry', + template: \`\`, }) export class RemoteEntryComponent {} " @@ -448,8 +448,8 @@ exports[`MF Remote App Generator --ssr should generate the correct files when -- "import { Component } from '@angular/core'; @Component({ - selector: 'proj-test-entry', - template: \`\` + selector: 'app-test-entry', + template: \`\` }) export class RemoteEntryComponent {} " @@ -594,8 +594,8 @@ import { NxWelcomeComponent } from './nx-welcome.component'; @Component({ standalone: true, imports: [CommonModule, NxWelcomeComponent], - selector: 'proj-test-entry', - template: \`\`, + selector: 'app-test-entry', + template: \`\`, }) export class RemoteEntryComponent {} " @@ -657,8 +657,8 @@ import { NxWelcomeComponent } from './nx-welcome.component'; @Component({ standalone: true, imports: [CommonModule, NxWelcomeComponent], - selector: 'proj-test-entry', - template: \`\` + selector: 'app-test-entry', + template: \`\` }) export class RemoteEntryComponent {} " diff --git a/packages/angular/src/generators/remote/remote.spec.ts b/packages/angular/src/generators/remote/remote.spec.ts index aa21b42b5fec6..3a1c01883784e 100644 --- a/packages/angular/src/generators/remote/remote.spec.ts +++ b/packages/angular/src/generators/remote/remote.spec.ts @@ -275,7 +275,7 @@ describe('MF Remote App Generator', () => { "import { Component } from '@angular/core'; @Component({ - selector: 'proj-root', + selector: 'app-root', template: '' }) @@ -294,11 +294,9 @@ describe('MF Remote App Generator', () => { }); // ASSERT - expect(tree.read('test/src/index.html', 'utf-8')).not.toContain( - 'proj-root' - ); + expect(tree.read('test/src/index.html', 'utf-8')).not.toContain('app-root'); expect(tree.read('test/src/index.html', 'utf-8')).toContain( - 'proj-test-entry' + 'app-test-entry' ); }); diff --git a/packages/angular/src/generators/scam-directive/lib/convert-directive-to-scam.spec.ts b/packages/angular/src/generators/scam-directive/lib/convert-directive-to-scam.spec.ts index c6c11d7496751..a8f5efa473e73 100644 --- a/packages/angular/src/generators/scam-directive/lib/convert-directive-to-scam.spec.ts +++ b/packages/angular/src/generators/scam-directive/lib/convert-directive-to-scam.spec.ts @@ -47,7 +47,7 @@ describe('convertDirectiveToScam', () => { import { CommonModule } from '@angular/common'; @Directive({ - selector: '[projExample]' + selector: '[example]' }) export class ExampleDirective { constructor() {} @@ -159,7 +159,7 @@ describe('convertDirectiveToScam', () => { import { CommonModule } from '@angular/common'; @Directive({ - selector: '[projExample]' + selector: '[example]' }) export class ExampleDirective { constructor() {} @@ -272,7 +272,7 @@ describe('convertDirectiveToScam', () => { import { CommonModule } from '@angular/common'; @Directive({ - selector: '[projExample]' + selector: '[example]' }) export class ExampleDirective { constructor() {} @@ -332,7 +332,7 @@ describe('convertDirectiveToScam', () => { import { CommonModule } from '@angular/common'; @Directive({ - selector: '[projExample]' + selector: '[example]' }) export class ExampleDirective { constructor() {} diff --git a/packages/angular/src/generators/scam-directive/scam-directive.spec.ts b/packages/angular/src/generators/scam-directive/scam-directive.spec.ts index 3e7d5c6ed8193..3f3e7b6ec56f9 100644 --- a/packages/angular/src/generators/scam-directive/scam-directive.spec.ts +++ b/packages/angular/src/generators/scam-directive/scam-directive.spec.ts @@ -31,7 +31,7 @@ describe('SCAM Directive Generator', () => { import { CommonModule } from '@angular/common'; @Directive({ - selector: '[projExample]' + selector: '[example]' }) export class ExampleDirective { constructor() {} @@ -166,7 +166,7 @@ describe('SCAM Directive Generator', () => { import { CommonModule } from '@angular/common'; @Directive({ - selector: '[projExample]' + selector: '[example]' }) export class ExampleDirective { constructor() {} @@ -211,7 +211,7 @@ describe('SCAM Directive Generator', () => { import { CommonModule } from '@angular/common'; @Directive({ - selector: '[projExample]' + selector: '[example]' }) export class ExampleDirective { constructor() {} diff --git a/packages/angular/src/generators/scam-to-standalone/scam-to-standalone.spec.ts b/packages/angular/src/generators/scam-to-standalone/scam-to-standalone.spec.ts index b6fb58cdbb776..a49a80b5fda89 100644 --- a/packages/angular/src/generators/scam-to-standalone/scam-to-standalone.spec.ts +++ b/packages/angular/src/generators/scam-to-standalone/scam-to-standalone.spec.ts @@ -36,7 +36,7 @@ describe('scam-to-standalone', () => { @Component({ standalone: true, imports: [CommonModule], - selector: 'proj-bar', + selector: 'app-bar', templateUrl: './bar.component.html', styleUrl: './bar.component.css', }) diff --git a/packages/angular/src/generators/scam/lib/convert-component-to-scam.spec.ts b/packages/angular/src/generators/scam/lib/convert-component-to-scam.spec.ts index b388421820515..b7ca3cd3f55d1 100644 --- a/packages/angular/src/generators/scam/lib/convert-component-to-scam.spec.ts +++ b/packages/angular/src/generators/scam/lib/convert-component-to-scam.spec.ts @@ -45,7 +45,7 @@ describe('convertComponentToScam', () => { import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -155,7 +155,7 @@ describe('convertComponentToScam', () => { import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -269,7 +269,7 @@ describe('convertComponentToScam', () => { import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.random.html', styleUrl: './example.random.css' }) @@ -384,7 +384,7 @@ describe('convertComponentToScam', () => { import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -444,7 +444,7 @@ describe('convertComponentToScam', () => { import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) diff --git a/packages/angular/src/generators/scam/scam.spec.ts b/packages/angular/src/generators/scam/scam.spec.ts index 081fa3b8026f2..359950568f2fc 100644 --- a/packages/angular/src/generators/scam/scam.spec.ts +++ b/packages/angular/src/generators/scam/scam.spec.ts @@ -30,7 +30,7 @@ describe('SCAM Generator', () => { import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -163,7 +163,7 @@ describe('SCAM Generator', () => { import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) @@ -207,7 +207,7 @@ describe('SCAM Generator', () => { import { CommonModule } from '@angular/common'; @Component({ - selector: 'proj-example', + selector: 'example', templateUrl: './example.component.html', styleUrl: './example.component.css' }) diff --git a/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap b/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap index 0fbe81c63e318..ea405b975b001 100644 --- a/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap +++ b/packages/angular/src/generators/setup-mf/__snapshots__/setup-mf.spec.ts.snap @@ -262,8 +262,8 @@ exports[`Init MF should generate the remote entry component correctly when prefi "import { Component } from '@angular/core'; @Component({ - selector: 'proj-remote1-entry', - template: \`\` + selector: 'app-remote1-entry', + template: \`\` }) export class RemoteEntryComponent {} " diff --git a/packages/angular/src/generators/setup-mf/lib/normalize-options.ts b/packages/angular/src/generators/setup-mf/lib/normalize-options.ts index 3338e3b69dc5e..25e7f6bbe139f 100644 --- a/packages/angular/src/generators/setup-mf/lib/normalize-options.ts +++ b/packages/angular/src/generators/setup-mf/lib/normalize-options.ts @@ -10,7 +10,7 @@ export function normalizeOptions( ...options, typescriptConfiguration: options.typescriptConfiguration ?? true, federationType: options.federationType ?? 'static', - prefix: options.prefix ?? getProjectPrefix(tree, options.appName), + prefix: options.prefix ?? getProjectPrefix(tree, options.appName) ?? 'app', standalone: options.standalone ?? true, }; } diff --git a/packages/angular/src/generators/utils/project.ts b/packages/angular/src/generators/utils/project.ts index 86213564b1b95..660ed6e9ac435 100644 --- a/packages/angular/src/generators/utils/project.ts +++ b/packages/angular/src/generators/utils/project.ts @@ -1,50 +1,12 @@ import type { Tree } from '@nx/devkit'; import { readProjectConfiguration } from '@nx/devkit'; import type { AngularProjectConfiguration } from '../../utils/types'; -import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; - -export function normalizeNewProjectPrefix( - prefix: string | undefined, - npmScope: string | undefined, - fallbackPrefix: string -): string { - // Prefix needs to be a valid html selector, if npmScope it's not valid, we don't default - // to it and let it fall through to the Angular schematic to handle it - // https://github.com/angular/angular-cli/blob/aa9f0528f174e856a4923cb24861fdf6e6f96b48/packages/schematics/angular/component/index.ts#L64 - const htmlSelectorRegex = - /^[a-zA-Z][.0-9a-zA-Z]*((:?-[0-9]+)*|(:?-[a-zA-Z][.0-9a-zA-Z]*(:?-[0-9]+)*)*)$/; - - if (prefix) { - if (!htmlSelectorRegex.test(prefix)) { - throw new Error( - 'The provided "prefix" is invalid. The prefix must start with a letter, and must contain only alphanumeric characters or dashes.' - ); - } - - return prefix; - } - - if (npmScope && !htmlSelectorRegex.test(npmScope)) { - throw new Error(`The "--prefix" option was not provided, therefore attempted to use the "npmScope" defined in "nx.json" to set the application's selector prefix, but it is invalid. - -There are two options that can be followed to resolve this issue: - - Pass a valid "--prefix" option. - - Update the "npmScope" in "nx.json" (Note: this can be an involved process, as other libraries and applications may need to be updated to match the new scope). - -If you encountered this error when creating a new Nx Workspace, the workspace name or "npmScope" is invalid to use as the selector prefix for the application being generated. - -Valid selector prefixes must start with a letter, and must contain only alphanumeric characters or dashes.`); - } - - return npmScope || fallbackPrefix; -} export function getProjectPrefix( tree: Tree, project: string ): string | undefined { return ( - (readProjectConfiguration(tree, project) as AngularProjectConfiguration) - .prefix ?? getNpmScope(tree) - ); + readProjectConfiguration(tree, project) as AngularProjectConfiguration + ).prefix; } diff --git a/packages/angular/src/generators/utils/selector.ts b/packages/angular/src/generators/utils/selector.ts index 09c8ba504ff05..bd3ee69c0682f 100644 --- a/packages/angular/src/generators/utils/selector.ts +++ b/packages/angular/src/generators/utils/selector.ts @@ -1,20 +1,26 @@ -import type { Tree } from '@nx/devkit'; import { names } from '@nx/devkit'; -import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'; - export function buildSelector( - tree: Tree, name: string, prefix: string | undefined, projectPrefix: string | undefined, casing: keyof Pick, 'fileName' | 'propertyName'> ): string { let selector = name; - prefix ??= projectPrefix ?? getNpmScope(tree); + prefix ??= projectPrefix; if (prefix) { selector = `${prefix}-${selector}`; } return names(selector)[casing]; } + +// https://github.com/angular/angular-cli/blob/main/packages/schematics/angular/utility/validation.ts#L11-L14 +const htmlSelectorRegex = + /^[a-zA-Z][.0-9a-zA-Z]*((:?-[0-9]+)*|(:?-[a-zA-Z][.0-9a-zA-Z]*(:?-[0-9]+)*)*)$/; + +export function validateHtmlSelector(selector: string): void { + if (selector && !htmlSelectorRegex.test(selector)) { + throw new Error(`The selector "${selector}" is invalid.`); + } +} diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 63ea240fb2a47..2f8b85a256140 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -61,6 +61,7 @@ interface AngularArguments extends BaseArguments { e2eTestRunner: 'none' | 'cypress' | 'playwright'; bundler: 'webpack' | 'esbuild'; ssr: boolean; + prefix: string; } interface VueArguments extends BaseArguments { @@ -175,6 +176,10 @@ export const commandsObject: yargs.Argv = yargs .option('ssr', { describe: chalk.dim`Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application`, type: 'boolean', + }) + .option('prefix', { + describe: chalk.dim`Prefix to use for Angular component and directive selectors.`, + type: 'string', }), withNxCloud, withAllPrompts, @@ -717,6 +722,25 @@ async function determineAngularOptions( const standaloneApi = parsedArgs.standaloneApi; const routing = parsedArgs.routing; + const prefix = parsedArgs.prefix; + + if (prefix) { + // https://github.com/angular/angular-cli/blob/main/packages/schematics/angular/utility/validation.ts#L11-L14 + const htmlSelectorRegex = + /^[a-zA-Z][.0-9a-zA-Z]*((:?-[0-9]+)*|(:?-[a-zA-Z][.0-9a-zA-Z]*(:?-[0-9]+)*)*)$/; + + // validate whether component/directive selectors will be valid with the provided prefix + if (!htmlSelectorRegex.test(`${prefix}-placeholder`)) { + output.error({ + title: `Failed to create a workspace.`, + bodyLines: [ + `The provided "${prefix}" prefix is invalid. It must be a valid HTML selector.`, + ], + }); + + process.exit(1); + } + } if (parsedArgs.preset && parsedArgs.preset !== Preset.Angular) { preset = parsedArgs.preset; @@ -817,6 +841,7 @@ async function determineAngularOptions( e2eTestRunner, bundler, ssr, + prefix, }; } diff --git a/packages/workspace/src/generators/new/generate-preset.ts b/packages/workspace/src/generators/new/generate-preset.ts index 5095515c1868a..81d865a40c88c 100644 --- a/packages/workspace/src/generators/new/generate-preset.ts +++ b/packages/workspace/src/generators/new/generate-preset.ts @@ -81,6 +81,7 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) { ? `--e2eTestRunner=${opts.e2eTestRunner}` : null, opts.ssr ? `--ssr` : null, + opts.prefix !== undefined ? `--prefix=${opts.prefix}` : null, ].filter((e) => !!e); } } diff --git a/packages/workspace/src/generators/new/new.ts b/packages/workspace/src/generators/new/new.ts index ae64f1671ee56..5213dd162ba1e 100644 --- a/packages/workspace/src/generators/new/new.ts +++ b/packages/workspace/src/generators/new/new.ts @@ -34,6 +34,7 @@ interface Schema { packageManager?: PackageManager; e2eTestRunner?: 'cypress' | 'playwright' | 'detox' | 'jest' | 'none'; ssr?: boolean; + prefix?: string; } export interface NormalizedSchema extends Schema { diff --git a/packages/workspace/src/generators/new/schema.json b/packages/workspace/src/generators/new/schema.json index 000d0fa460d44..1720dab56e7a9 100644 --- a/packages/workspace/src/generators/new/schema.json +++ b/packages/workspace/src/generators/new/schema.json @@ -82,6 +82,10 @@ "description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.", "type": "boolean", "default": false + }, + "prefix": { + "description": "The prefix to use for Angular component and directive selectors.", + "type": "string" } }, "additionalProperties": true diff --git a/packages/workspace/src/generators/preset/preset.ts b/packages/workspace/src/generators/preset/preset.ts index 27dd003874c28..e2d48c77d7c40 100644 --- a/packages/workspace/src/generators/preset/preset.ts +++ b/packages/workspace/src/generators/preset/preset.ts @@ -34,6 +34,7 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: options.e2eTestRunner ?? 'cypress', bundler: options.bundler, ssr: options.ssr, + prefix: options.prefix, }); } else if (options.preset === Preset.AngularStandalone) { const { @@ -52,6 +53,7 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: options.e2eTestRunner ?? 'cypress', bundler: options.bundler, ssr: options.ssr, + prefix: options.prefix, }); } else if (options.preset === Preset.ReactMonorepo) { const { applicationGenerator: reactApplicationGenerator } = require('@nx' + diff --git a/packages/workspace/src/generators/preset/schema.d.ts b/packages/workspace/src/generators/preset/schema.d.ts index 13fbbfbc61ded..dff3c94c9e543 100644 --- a/packages/workspace/src/generators/preset/schema.d.ts +++ b/packages/workspace/src/generators/preset/schema.d.ts @@ -18,4 +18,5 @@ export interface Schema { e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none'; js?: boolean; ssr?: boolean; + prefix?: string; } diff --git a/packages/workspace/src/generators/preset/schema.json b/packages/workspace/src/generators/preset/schema.json index 1426b4947e489..9c0b8718e0562 100644 --- a/packages/workspace/src/generators/preset/schema.json +++ b/packages/workspace/src/generators/preset/schema.json @@ -99,6 +99,10 @@ "description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.", "type": "boolean", "default": false + }, + "prefix": { + "description": "The prefix to use for Angular component and directive selectors.", + "type": "string" } }, "required": ["preset", "name"]