fix(angular): stop using npmScope as a prefix for component and directive selectors (#21828)

This commit is contained in:
Leosvel Pérez Espinosa 2024-02-21 16:20:12 +01:00 committed by GitHub
parent fe72ab4c78
commit cfa0815385
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 364 additions and 155 deletions

View File

@ -135,6 +135,12 @@ Default: `npm`
Package manager to use Package manager to use
### prefix
Type: `string`
Prefix to use for Angular component and directive selectors.
### preset ### preset
Type: `string` Type: `string`

View File

@ -78,6 +78,7 @@
"type": "string", "type": "string",
"format": "html-selector", "format": "html-selector",
"description": "The prefix to apply to generated selectors.", "description": "The prefix to apply to generated selectors.",
"default": "app",
"alias": "p" "alias": "p"
}, },
"skipTests": { "skipTests": {

View File

@ -135,6 +135,12 @@ Default: `npm`
Package manager to use Package manager to use
### prefix
Type: `string`
Prefix to use for Angular component and directive selectors.
### preset ### preset
Type: `string` Type: `string`

View File

@ -79,6 +79,10 @@
"description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.", "description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"prefix": {
"description": "The prefix to use for Angular component and directive selectors.",
"type": "string"
} }
}, },
"additionalProperties": true, "additionalProperties": true,

View File

@ -96,6 +96,10 @@
"description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.", "description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"prefix": {
"description": "The prefix to use for Angular component and directive selectors.",
"type": "string"
} }
}, },
"required": ["preset", "name"], "required": ["preset", "name"],

View File

@ -346,7 +346,7 @@ describe('Angular Module Federation', () => {
import { isEven } from '${remote}/${module}'; import { isEven } from '${remote}/${module}';
@Component({ @Component({
selector: 'proj-root', selector: 'app-root',
template: \`<div class="host">{{title}}</div>\`, template: \`<div class="host">{{title}}</div>\`,
standalone: true standalone: true
}) })
@ -433,7 +433,7 @@ describe('Angular Module Federation', () => {
import { isEven } from '${childRemote}/${module}'; import { isEven } from '${childRemote}/${module}';
@Component({ @Component({
selector: 'proj-${remote}-entry', selector: 'app-${remote}-entry',
template: \`<div class="childremote">{{title}}</div>\`, template: \`<div class="childremote">{{title}}</div>\`,
standalone: true standalone: true
}) })

View File

@ -233,6 +233,7 @@ export function runCreateWorkspace(
e2eTestRunner, e2eTestRunner,
ssr, ssr,
framework, framework,
prefix,
}: { }: {
preset: string; preset: string;
appName?: string; appName?: string;
@ -251,6 +252,7 @@ export function runCreateWorkspace(
e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none';
ssr?: boolean; ssr?: boolean;
framework?: string; framework?: string;
prefix?: string;
} }
) { ) {
projName = name; projName = name;
@ -317,6 +319,10 @@ export function runCreateWorkspace(
command += ` --ssr=${ssr}`; command += ` --ssr=${ssr}`;
} }
if (prefix !== undefined) {
command += ` --prefix=${prefix}`;
}
try { try {
const create = execSync(`${command}${isVerbose() ? ' --verbose' : ''}`, { const create = execSync(`${command}${isVerbose() ? ' --verbose' : ''}`, {
cwd, cwd,

View File

@ -156,8 +156,7 @@ describe('create-nx-workspace', () => {
it('should fail correctly when preset errors', () => { it('should fail correctly when preset errors', () => {
// Using Angular Preset as the example here to test // Using Angular Preset as the example here to test
// It will error when npmScope is of form `<char>-<num>-<char>` // It will error when prefix is not valid
// Due to a validation error Angular will throw.
const wsName = uniq('angular-1-test'); const wsName = uniq('angular-1-test');
const appName = uniq('app'); const appName = uniq('app');
expect(() => expect(() =>
@ -171,6 +170,7 @@ describe('create-nx-workspace', () => {
e2eTestRunner: 'none', e2eTestRunner: 'none',
bundler: 'webpack', bundler: 'webpack',
ssr: false, ssr: false,
prefix: '1-one',
}) })
).toThrow(); ).toThrow();
}); });

View File

@ -24,7 +24,7 @@ exports[`app --minimal should skip "nx-welcome.component.ts" file and references
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css', 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'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css', styleUrl: './app.component.css',
}) })
@ -127,7 +127,7 @@ import { RouterModule } from '@angular/router';
@Component({ @Component({
standalone: true, standalone: true,
imports: [RouterModule], imports: [RouterModule],
selector: 'proj-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css', styleUrl: './app.component.css',
}) })
@ -170,7 +170,7 @@ exports[`app --minimal should skip "nx-welcome.component.ts" file and references
@Component({ @Component({
standalone: true, standalone: true,
imports: [], imports: [],
selector: 'proj-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css', 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", "$schema": "../../../node_modules/nx/schemas/project-schema.json",
"name": "my-dir-my-app", "name": "my-dir-my-app",
"prefix": "proj", "prefix": "app",
"projectType": "application", "projectType": "application",
"root": "apps/my-dir/my-app", "root": "apps/my-dir/my-app",
"sourceRoot": "apps/my-dir/my-app/src", "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", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "my-app", "name": "my-app",
"prefix": "proj", "prefix": "app",
"projectType": "application", "projectType": "application",
"root": "apps/my-app", "root": "apps/my-app",
"sourceRoot": "apps/my-app/src", "sourceRoot": "apps/my-app/src",
@ -641,7 +641,7 @@ import { NxWelcomeComponent } from './nx-welcome.component';
@Component({ @Component({
standalone: true, standalone: true,
imports: [NxWelcomeComponent, RouterModule], imports: [NxWelcomeComponent, RouterModule],
selector: 'proj-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css', styleUrl: './app.component.css',
}) })
@ -709,7 +709,7 @@ import { NxWelcomeComponent } from './nx-welcome.component';
@Component({ @Component({
standalone: true, standalone: true,
imports: [NxWelcomeComponent, ], imports: [NxWelcomeComponent, ],
selector: 'proj-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css', styleUrl: './app.component.css',
}) })
@ -850,7 +850,7 @@ exports[`app format files should format files 2`] = `
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css', styleUrl: './app.component.css',
}) })
@ -903,7 +903,7 @@ exports[`app nested should create project configs 1`] = `
{ {
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "my-app", "name": "my-app",
"prefix": "proj", "prefix": "app",
"projectType": "application", "projectType": "application",
"root": "my-dir/my-app", "root": "my-dir/my-app",
"sourceRoot": "my-dir/my-app/src", "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", "$schema": "../node_modules/nx/schemas/project-schema.json",
"name": "my-app", "name": "my-app",
"prefix": "proj", "prefix": "app",
"projectType": "application", "projectType": "application",
"root": "my-app", "root": "my-app",
"sourceRoot": "my-app/src", "sourceRoot": "my-app/src",

View File

@ -449,7 +449,7 @@ describe('app', () => {
await generateApp(appTree, 'my-app', { directory: 'my-dir/my-app' }); await generateApp(appTree, 'my-app', { directory: 'my-dir/my-app' });
expect( expect(
appTree.read('my-dir/my-app/src/app/app.component.html', 'utf-8') appTree.read('my-dir/my-app/src/app/app.component.html', 'utf-8')
).toContain('<proj-nx-welcome></proj-nx-welcome>'); ).toContain('<app-nx-welcome></app-nx-welcome>');
}); });
it("should update `template`'s property of AppComponent with Nx content", async () => { it("should update `template`'s property of AppComponent with Nx content", async () => {
@ -459,7 +459,7 @@ describe('app', () => {
}); });
expect( expect(
appTree.read('my-dir/my-app/src/app/app.component.ts', 'utf-8') appTree.read('my-dir/my-app/src/app/app.component.ts', 'utf-8')
).toContain('<proj-nx-welcome></proj-nx-welcome>'); ).toContain('<app-nx-welcome></app-nx-welcome>');
}); });
it('should create Nx specific `nx-welcome.component.ts` file', async () => { it('should create Nx specific `nx-welcome.component.ts` file', async () => {
@ -598,7 +598,7 @@ describe('app', () => {
"@angular-eslint/component-selector": [ "@angular-eslint/component-selector": [
"error", "error",
{ {
"prefix": "proj", "prefix": "app",
"style": "kebab-case", "style": "kebab-case",
"type": "element", "type": "element",
}, },
@ -606,7 +606,7 @@ describe('app', () => {
"@angular-eslint/directive-selector": [ "@angular-eslint/directive-selector": [
"error", "error",
{ {
"prefix": "proj", "prefix": "app",
"style": "camelCase", "style": "camelCase",
"type": "attribute", "type": "attribute",
}, },

View File

@ -5,6 +5,7 @@ import { getRelativePathToRootTsConfig, getRootTsConfigFileName } from '@nx/js';
import { createTsConfig } from '../../utils/create-ts-config'; import { createTsConfig } from '../../utils/create-ts-config';
import { UnitTestRunner } from '../../../utils/test-runners'; import { UnitTestRunner } from '../../../utils/test-runners';
import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils';
import { validateHtmlSelector } from '../../utils/selector';
export async function createFiles( export async function createFiles(
tree: Tree, tree: Tree,
@ -15,8 +16,13 @@ export async function createFiles(
const isUsingApplicationBuilder = const isUsingApplicationBuilder =
angularMajorVersion >= 17 && options.bundler === 'esbuild'; angularMajorVersion >= 17 && options.bundler === 'esbuild';
const rootSelector = `${options.prefix}-root`;
validateHtmlSelector(rootSelector);
const nxWelcomeSelector = `${options.prefix}-nx-welcome`;
validateHtmlSelector(nxWelcomeSelector);
const substitutions = { const substitutions = {
rootSelector: `${options.prefix}-root`, rootSelector,
appName: options.name, appName: options.name,
inlineStyle: options.inlineStyle, inlineStyle: options.inlineStyle,
inlineTemplate: options.inlineTemplate, inlineTemplate: options.inlineTemplate,
@ -25,7 +31,7 @@ export async function createFiles(
unitTesting: options.unitTestRunner !== UnitTestRunner.None, unitTesting: options.unitTestRunner !== UnitTestRunner.None,
routing: options.routing, routing: options.routing,
minimal: options.minimal, minimal: options.minimal,
nxWelcomeSelector: `${options.prefix}-nx-welcome`, nxWelcomeSelector,
rootTsConfig: joinPathFragments(rootOffset, getRootTsConfigFileName(tree)), rootTsConfig: joinPathFragments(rootOffset, getRootTsConfigFileName(tree)),
angularMajorVersion, angularMajorVersion,
rootOffset, rootOffset,

View File

@ -1,9 +1,7 @@
import { joinPathFragments, type Tree } from '@nx/devkit'; import { joinPathFragments, type Tree } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Linter } from '@nx/eslint'; 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 { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners';
import { normalizeNewProjectPrefix } from '../../utils/project';
import type { Schema } from '../schema'; import type { Schema } from '../schema';
import type { NormalizedSchema } from './normalized-schema'; import type { NormalizedSchema } from './normalized-schema';
import { getInstalledAngularVersionInfo } from '../../utils/version-utils'; import { getInstalledAngularVersionInfo } from '../../utils/version-utils';
@ -34,12 +32,6 @@ export async function normalizeOptions(
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
const prefix = normalizeNewProjectPrefix(
options.prefix,
getNpmScope(host),
'app'
);
let bundler = options.bundler; let bundler = options.bundler;
if (!bundler) { if (!bundler) {
const { major: angularMajorVersion } = getInstalledAngularVersionInfo(host); const { major: angularMajorVersion } = getInstalledAngularVersionInfo(host);
@ -60,7 +52,7 @@ export async function normalizeOptions(
strict: true, strict: true,
standalone: true, standalone: true,
...options, ...options,
prefix, prefix: options.prefix || 'app',
name: appProjectName, name: appProjectName,
appProjectRoot, appProjectRoot,
appProjectSourceRoot: `${appProjectRoot}/src`, appProjectSourceRoot: `${appProjectRoot}/src`,

View File

@ -81,6 +81,7 @@
"type": "string", "type": "string",
"format": "html-selector", "format": "html-selector",
"description": "The prefix to apply to generated selectors.", "description": "The prefix to apply to generated selectors.",
"default": "app",
"alias": "p" "alias": "p"
}, },
"skipTests": { "skipTests": {

View File

@ -4,7 +4,7 @@ exports[`component Generator --flat should create the component correctly and ex
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -16,7 +16,7 @@ exports[`component Generator --flat should create the component correctly and no
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -41,7 +41,7 @@ exports[`component Generator --path should create the component correctly and ex
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -53,7 +53,7 @@ exports[`component Generator compat should inline styles when --inline-style=tru
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styles: \`\` styles: \`\`
}) })
@ -65,7 +65,7 @@ exports[`component Generator secondary entry points should create the component
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -82,7 +82,7 @@ exports[`component Generator should create component files correctly: component
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css', styleUrl: './example.component.css',
}) })
@ -131,7 +131,7 @@ exports[`component Generator should create the component correctly and export it
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -149,7 +149,7 @@ exports[`component Generator should create the component correctly and export it
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
templateUrl: './example.component.html', templateUrl: './example.component.html',
@ -163,7 +163,7 @@ exports[`component Generator should create the component correctly and not expor
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -176,7 +176,7 @@ exports[`component Generator should create the component correctly and not expor
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
templateUrl: './example.component.html', templateUrl: './example.component.html',
@ -190,7 +190,7 @@ exports[`component Generator should create the component correctly and not expor
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -202,7 +202,7 @@ exports[`component Generator should create the component correctly but not expor
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -214,7 +214,7 @@ exports[`component Generator should inline styles when --inline-style=true 1`] =
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styles: \`\` styles: \`\`
}) })
@ -226,7 +226,7 @@ exports[`component Generator should inline template when --inline-template=true
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
template: \`<p>example works!</p>\`, template: \`<p>example works!</p>\`,
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })

View File

@ -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 { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { AngularProjectConfiguration } from '../../utils/types';
import { componentGenerator } from './component'; import { componentGenerator } from './component';
describe('component Generator', () => { describe('component Generator', () => {
@ -202,7 +209,7 @@ describe('component Generator', () => {
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html' templateUrl: './example.component.html'
}) })
export class ExampleComponent {} 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', () => { describe('secondary entry points', () => {
it('should create the component correctly and export it in the entry point', async () => { it('should create the component correctly and export it in the entry point', async () => {
// ARRANGE // ARRANGE

View File

@ -2,7 +2,7 @@ import type { Tree } from '@nx/devkit';
import { names, readProjectConfiguration } from '@nx/devkit'; import { names, readProjectConfiguration } from '@nx/devkit';
import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils';
import type { AngularProjectConfiguration } from '../../../utils/types'; import type { AngularProjectConfiguration } from '../../../utils/types';
import { buildSelector } from '../../utils/selector'; import { buildSelector, validateHtmlSelector } from '../../utils/selector';
import type { NormalizedSchema, Schema } from '../schema'; import type { NormalizedSchema, Schema } from '../schema';
export async function normalizeOptions( export async function normalizeOptions(
@ -37,8 +37,8 @@ export async function normalizeOptions(
) as AngularProjectConfiguration; ) as AngularProjectConfiguration;
const selector = const selector =
options.selector ?? options.selector ?? buildSelector(name, options.prefix, prefix, 'fileName');
buildSelector(tree, name, options.prefix, prefix, 'fileName'); validateHtmlSelector(selector);
return { return {
...options, ...options,

View File

@ -16,7 +16,7 @@ exports[`directive generator --no-standalone should generate a directive with te
"import { Directive } from '@angular/core'; "import { Directive } from '@angular/core';
@Directive({ @Directive({
selector: '[projTest]', selector: '[test]',
}) })
export class TestDirective { export class TestDirective {
constructor() {} constructor() {}
@ -52,7 +52,7 @@ exports[`directive generator --no-standalone should import the directive correct
"import { Directive } from '@angular/core'; "import { Directive } from '@angular/core';
@Directive({ @Directive({
selector: '[projTest]' selector: '[test]'
}) })
export class TestDirective { export class TestDirective {
constructor() {} constructor() {}
@ -88,7 +88,7 @@ exports[`directive generator --no-standalone should import the directive correct
"import { Directive } from '@angular/core'; "import { Directive } from '@angular/core';
@Directive({ @Directive({
selector: '[projTest]' selector: '[test]'
}) })
export class TestDirective { export class TestDirective {
constructor() {} constructor() {}
@ -124,7 +124,7 @@ exports[`directive generator should generate correctly 1`] = `
"import { Directive } from '@angular/core'; "import { Directive } from '@angular/core';
@Directive({ @Directive({
selector: '[projTest]', selector: '[test]',
standalone: true, standalone: true,
}) })
export class TestDirective { export class TestDirective {

View File

@ -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 { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import type { AngularProjectConfiguration } from '../../utils/types';
import { directiveGenerator } from './directive'; import { directiveGenerator } from './directive';
import type { Schema } from './schema'; 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) { function addModule(tree: Tree) {

View File

@ -1,7 +1,7 @@
import type { Tree } from '@nx/devkit'; import type { Tree } from '@nx/devkit';
import { names, readProjectConfiguration } from '@nx/devkit'; import { names, readProjectConfiguration } from '@nx/devkit';
import type { AngularProjectConfiguration } from '../../../utils/types'; import type { AngularProjectConfiguration } from '../../../utils/types';
import { buildSelector } from '../../utils/selector'; import { buildSelector, validateHtmlSelector } from '../../utils/selector';
import type { NormalizedSchema, Schema } from '../schema'; import type { NormalizedSchema, Schema } from '../schema';
import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils';
@ -37,7 +37,8 @@ export async function normalizeOptions(
const selector = const selector =
options.selector ?? options.selector ??
buildSelector(tree, name, options.prefix, prefix, 'propertyName'); buildSelector(name, options.prefix, prefix, 'propertyName');
validateHtmlSelector(selector);
return { return {
...options, ...options,

View File

@ -954,7 +954,7 @@ import { NxWelcomeComponent } from './nx-welcome.component';
@Component({ @Component({
standalone: true, standalone: true,
imports: [NxWelcomeComponent, RouterModule], imports: [NxWelcomeComponent, RouterModule],
selector: 'proj-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css', styleUrl: './app.component.css',
}) })

View File

@ -7,7 +7,7 @@ exports[`lib --standalone should generate a library with a standalone component
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-my-lib', selector: 'lib-my-lib',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
templateUrl: './my-lib.component.html', 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'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-my-lib', selector: 'lib-my-lib',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
templateUrl: './my-lib.component.html', 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'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-my-lib', selector: 'lib-my-lib',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
templateUrl: './my-lib.component.html', 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'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-my-lib', selector: 'lib-my-lib',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
template: \`<p>my-lib works!</p>\`, template: \`<p>my-lib works!</p>\`,
@ -166,7 +166,7 @@ exports[`lib --standalone should generate a library with a standalone component
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-my-lib', selector: 'lib-my-lib',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
template: \`<p>my-lib works!</p>\`, template: \`<p>my-lib works!</p>\`,
@ -183,7 +183,7 @@ exports[`lib --standalone should generate a library with a standalone component
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-my-lib', selector: 'lib-my-lib',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
template: \`<p>my-lib works!</p>\`, template: \`<p>my-lib works!</p>\`,
@ -239,7 +239,7 @@ exports[`lib --standalone should generate a library with a standalone component
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-my-lib', selector: 'lib-my-lib',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
templateUrl: './my-lib.component.html', 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'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-my-lib', selector: 'lib-my-lib',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
templateUrl: './my-lib.component.html', 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'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-my-lib', selector: 'lib-my-lib',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
templateUrl: './my-lib.component.html', templateUrl: './my-lib.component.html',

View File

@ -1,9 +1,7 @@
import { names, Tree } from '@nx/devkit'; import { names, Tree } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; 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 { Linter } from '@nx/eslint';
import { UnitTestRunner } from '../../../utils/test-runners'; import { UnitTestRunner } from '../../../utils/test-runners';
import { normalizeNewProjectPrefix } from '../../utils/project';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { NormalizedSchema } from './normalized-schema'; import { NormalizedSchema } from './normalized-schema';
@ -52,15 +50,12 @@ export async function normalizeOptions(
: []; : [];
const modulePath = `${projectRoot}/src/lib/${fileName}.module.ts`; const modulePath = `${projectRoot}/src/lib/${fileName}.module.ts`;
const npmScope = getNpmScope(host);
const prefix = normalizeNewProjectPrefix(options.prefix, npmScope, 'lib');
const ngCliSchematicLibRoot = projectName; const ngCliSchematicLibRoot = projectName;
const allNormalizedOptions = { const allNormalizedOptions = {
...options, ...options,
linter: options.linter ?? Linter.EsLint, linter: options.linter ?? Linter.EsLint,
unitTestRunner: options.unitTestRunner ?? UnitTestRunner.Jest, unitTestRunner: options.unitTestRunner ?? UnitTestRunner.Jest,
prefix, prefix: options.prefix ?? 'lib',
name: projectName, name: projectName,
projectRoot, projectRoot,
entryFile: 'index', entryFile: 'index',

View File

@ -652,7 +652,7 @@ describe('lib', () => {
"error", "error",
{ {
"type": "attribute", "type": "attribute",
"prefix": "proj", "prefix": "lib",
"style": "camelCase" "style": "camelCase"
} }
], ],
@ -660,7 +660,7 @@ describe('lib', () => {
"error", "error",
{ {
"type": "element", "type": "element",
"prefix": "proj", "prefix": "lib",
"style": "kebab-case" "style": "kebab-case"
} }
] ]
@ -1201,7 +1201,7 @@ describe('lib', () => {
"@angular-eslint/component-selector": [ "@angular-eslint/component-selector": [
"error", "error",
{ {
"prefix": "proj", "prefix": "lib",
"style": "kebab-case", "style": "kebab-case",
"type": "element", "type": "element",
}, },
@ -1209,7 +1209,7 @@ describe('lib', () => {
"@angular-eslint/directive-selector": [ "@angular-eslint/directive-selector": [
"error", "error",
{ {
"prefix": "proj", "prefix": "lib",
"style": "camelCase", "style": "camelCase",
"type": "attribute", "type": "attribute",
}, },
@ -1261,7 +1261,7 @@ describe('lib', () => {
"@angular-eslint/component-selector": [ "@angular-eslint/component-selector": [
"error", "error",
{ {
"prefix": "proj", "prefix": "lib",
"style": "kebab-case", "style": "kebab-case",
"type": "element", "type": "element",
}, },
@ -1269,7 +1269,7 @@ describe('lib', () => {
"@angular-eslint/directive-selector": [ "@angular-eslint/directive-selector": [
"error", "error",
{ {
"prefix": "proj", "prefix": "lib",
"style": "camelCase", "style": "camelCase",
"type": "attribute", "type": "attribute",
}, },

View File

@ -224,8 +224,8 @@ exports[`MF Remote App Generator --ssr should generate the correct files 8`] = `
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-test-entry', selector: 'app-test-entry',
template: \`<proj-nx-welcome></proj-nx-welcome>\`, template: \`<app-nx-welcome></app-nx-welcome>\`,
}) })
export class RemoteEntryComponent {} export class RemoteEntryComponent {}
" "
@ -448,8 +448,8 @@ exports[`MF Remote App Generator --ssr should generate the correct files when --
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-test-entry', selector: 'app-test-entry',
template: \`<proj-nx-welcome></proj-nx-welcome>\` template: \`<app-nx-welcome></app-nx-welcome>\`
}) })
export class RemoteEntryComponent {} export class RemoteEntryComponent {}
" "
@ -594,8 +594,8 @@ import { NxWelcomeComponent } from './nx-welcome.component';
@Component({ @Component({
standalone: true, standalone: true,
imports: [CommonModule, NxWelcomeComponent], imports: [CommonModule, NxWelcomeComponent],
selector: 'proj-test-entry', selector: 'app-test-entry',
template: \`<proj-nx-welcome></proj-nx-welcome>\`, template: \`<app-nx-welcome></app-nx-welcome>\`,
}) })
export class RemoteEntryComponent {} export class RemoteEntryComponent {}
" "
@ -657,8 +657,8 @@ import { NxWelcomeComponent } from './nx-welcome.component';
@Component({ @Component({
standalone: true, standalone: true,
imports: [CommonModule, NxWelcomeComponent], imports: [CommonModule, NxWelcomeComponent],
selector: 'proj-test-entry', selector: 'app-test-entry',
template: \`<proj-nx-welcome></proj-nx-welcome>\` template: \`<app-nx-welcome></app-nx-welcome>\`
}) })
export class RemoteEntryComponent {} export class RemoteEntryComponent {}
" "

View File

@ -275,7 +275,7 @@ describe('MF Remote App Generator', () => {
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-root', selector: 'app-root',
template: '<router-outlet></router-outlet>' template: '<router-outlet></router-outlet>'
}) })
@ -294,11 +294,9 @@ describe('MF Remote App Generator', () => {
}); });
// ASSERT // ASSERT
expect(tree.read('test/src/index.html', 'utf-8')).not.toContain( expect(tree.read('test/src/index.html', 'utf-8')).not.toContain('app-root');
'proj-root'
);
expect(tree.read('test/src/index.html', 'utf-8')).toContain( expect(tree.read('test/src/index.html', 'utf-8')).toContain(
'proj-test-entry' 'app-test-entry'
); );
}); });

View File

@ -47,7 +47,7 @@ describe('convertDirectiveToScam', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Directive({ @Directive({
selector: '[projExample]' selector: '[example]'
}) })
export class ExampleDirective { export class ExampleDirective {
constructor() {} constructor() {}
@ -159,7 +159,7 @@ describe('convertDirectiveToScam', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Directive({ @Directive({
selector: '[projExample]' selector: '[example]'
}) })
export class ExampleDirective { export class ExampleDirective {
constructor() {} constructor() {}
@ -272,7 +272,7 @@ describe('convertDirectiveToScam', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Directive({ @Directive({
selector: '[projExample]' selector: '[example]'
}) })
export class ExampleDirective { export class ExampleDirective {
constructor() {} constructor() {}
@ -332,7 +332,7 @@ describe('convertDirectiveToScam', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Directive({ @Directive({
selector: '[projExample]' selector: '[example]'
}) })
export class ExampleDirective { export class ExampleDirective {
constructor() {} constructor() {}

View File

@ -31,7 +31,7 @@ describe('SCAM Directive Generator', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Directive({ @Directive({
selector: '[projExample]' selector: '[example]'
}) })
export class ExampleDirective { export class ExampleDirective {
constructor() {} constructor() {}
@ -166,7 +166,7 @@ describe('SCAM Directive Generator', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Directive({ @Directive({
selector: '[projExample]' selector: '[example]'
}) })
export class ExampleDirective { export class ExampleDirective {
constructor() {} constructor() {}
@ -211,7 +211,7 @@ describe('SCAM Directive Generator', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Directive({ @Directive({
selector: '[projExample]' selector: '[example]'
}) })
export class ExampleDirective { export class ExampleDirective {
constructor() {} constructor() {}

View File

@ -36,7 +36,7 @@ describe('scam-to-standalone', () => {
@Component({ @Component({
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
selector: 'proj-bar', selector: 'app-bar',
templateUrl: './bar.component.html', templateUrl: './bar.component.html',
styleUrl: './bar.component.css', styleUrl: './bar.component.css',
}) })

View File

@ -45,7 +45,7 @@ describe('convertComponentToScam', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -155,7 +155,7 @@ describe('convertComponentToScam', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -269,7 +269,7 @@ describe('convertComponentToScam', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.random.html', templateUrl: './example.random.html',
styleUrl: './example.random.css' styleUrl: './example.random.css'
}) })
@ -384,7 +384,7 @@ describe('convertComponentToScam', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -444,7 +444,7 @@ describe('convertComponentToScam', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })

View File

@ -30,7 +30,7 @@ describe('SCAM Generator', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -163,7 +163,7 @@ describe('SCAM Generator', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })
@ -207,7 +207,7 @@ describe('SCAM Generator', () => {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@Component({ @Component({
selector: 'proj-example', selector: 'example',
templateUrl: './example.component.html', templateUrl: './example.component.html',
styleUrl: './example.component.css' styleUrl: './example.component.css'
}) })

View File

@ -262,8 +262,8 @@ exports[`Init MF should generate the remote entry component correctly when prefi
"import { Component } from '@angular/core'; "import { Component } from '@angular/core';
@Component({ @Component({
selector: 'proj-remote1-entry', selector: 'app-remote1-entry',
template: \`<proj-nx-welcome></proj-nx-welcome>\` template: \`<app-nx-welcome></app-nx-welcome>\`
}) })
export class RemoteEntryComponent {} export class RemoteEntryComponent {}
" "

View File

@ -10,7 +10,7 @@ export function normalizeOptions(
...options, ...options,
typescriptConfiguration: options.typescriptConfiguration ?? true, typescriptConfiguration: options.typescriptConfiguration ?? true,
federationType: options.federationType ?? 'static', federationType: options.federationType ?? 'static',
prefix: options.prefix ?? getProjectPrefix(tree, options.appName), prefix: options.prefix ?? getProjectPrefix(tree, options.appName) ?? 'app',
standalone: options.standalone ?? true, standalone: options.standalone ?? true,
}; };
} }

View File

@ -1,50 +1,12 @@
import type { Tree } from '@nx/devkit'; import type { Tree } from '@nx/devkit';
import { readProjectConfiguration } from '@nx/devkit'; import { readProjectConfiguration } from '@nx/devkit';
import type { AngularProjectConfiguration } from '../../utils/types'; 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( export function getProjectPrefix(
tree: Tree, tree: Tree,
project: string project: string
): string | undefined { ): string | undefined {
return ( return (
(readProjectConfiguration(tree, project) as AngularProjectConfiguration) readProjectConfiguration(tree, project) as AngularProjectConfiguration
.prefix ?? getNpmScope(tree) ).prefix;
);
} }

View File

@ -1,20 +1,26 @@
import type { Tree } from '@nx/devkit';
import { names } from '@nx/devkit'; import { names } from '@nx/devkit';
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope';
export function buildSelector( export function buildSelector(
tree: Tree,
name: string, name: string,
prefix: string | undefined, prefix: string | undefined,
projectPrefix: string | undefined, projectPrefix: string | undefined,
casing: keyof Pick<ReturnType<typeof names>, 'fileName' | 'propertyName'> casing: keyof Pick<ReturnType<typeof names>, 'fileName' | 'propertyName'>
): string { ): string {
let selector = name; let selector = name;
prefix ??= projectPrefix ?? getNpmScope(tree); prefix ??= projectPrefix;
if (prefix) { if (prefix) {
selector = `${prefix}-${selector}`; selector = `${prefix}-${selector}`;
} }
return names(selector)[casing]; 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.`);
}
}

View File

@ -61,6 +61,7 @@ interface AngularArguments extends BaseArguments {
e2eTestRunner: 'none' | 'cypress' | 'playwright'; e2eTestRunner: 'none' | 'cypress' | 'playwright';
bundler: 'webpack' | 'esbuild'; bundler: 'webpack' | 'esbuild';
ssr: boolean; ssr: boolean;
prefix: string;
} }
interface VueArguments extends BaseArguments { interface VueArguments extends BaseArguments {
@ -175,6 +176,10 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
.option('ssr', { .option('ssr', {
describe: chalk.dim`Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application`, describe: chalk.dim`Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application`,
type: 'boolean', type: 'boolean',
})
.option('prefix', {
describe: chalk.dim`Prefix to use for Angular component and directive selectors.`,
type: 'string',
}), }),
withNxCloud, withNxCloud,
withAllPrompts, withAllPrompts,
@ -717,6 +722,25 @@ async function determineAngularOptions(
const standaloneApi = parsedArgs.standaloneApi; const standaloneApi = parsedArgs.standaloneApi;
const routing = parsedArgs.routing; 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) { if (parsedArgs.preset && parsedArgs.preset !== Preset.Angular) {
preset = parsedArgs.preset; preset = parsedArgs.preset;
@ -817,6 +841,7 @@ async function determineAngularOptions(
e2eTestRunner, e2eTestRunner,
bundler, bundler,
ssr, ssr,
prefix,
}; };
} }

View File

@ -81,6 +81,7 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) {
? `--e2eTestRunner=${opts.e2eTestRunner}` ? `--e2eTestRunner=${opts.e2eTestRunner}`
: null, : null,
opts.ssr ? `--ssr` : null, opts.ssr ? `--ssr` : null,
opts.prefix !== undefined ? `--prefix=${opts.prefix}` : null,
].filter((e) => !!e); ].filter((e) => !!e);
} }
} }

View File

@ -34,6 +34,7 @@ interface Schema {
packageManager?: PackageManager; packageManager?: PackageManager;
e2eTestRunner?: 'cypress' | 'playwright' | 'detox' | 'jest' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'detox' | 'jest' | 'none';
ssr?: boolean; ssr?: boolean;
prefix?: string;
} }
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {

View File

@ -82,6 +82,10 @@
"description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.", "description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"prefix": {
"description": "The prefix to use for Angular component and directive selectors.",
"type": "string"
} }
}, },
"additionalProperties": true "additionalProperties": true

View File

@ -34,6 +34,7 @@ async function createPreset(tree: Tree, options: Schema) {
e2eTestRunner: options.e2eTestRunner ?? 'cypress', e2eTestRunner: options.e2eTestRunner ?? 'cypress',
bundler: options.bundler, bundler: options.bundler,
ssr: options.ssr, ssr: options.ssr,
prefix: options.prefix,
}); });
} else if (options.preset === Preset.AngularStandalone) { } else if (options.preset === Preset.AngularStandalone) {
const { const {
@ -52,6 +53,7 @@ async function createPreset(tree: Tree, options: Schema) {
e2eTestRunner: options.e2eTestRunner ?? 'cypress', e2eTestRunner: options.e2eTestRunner ?? 'cypress',
bundler: options.bundler, bundler: options.bundler,
ssr: options.ssr, ssr: options.ssr,
prefix: options.prefix,
}); });
} else if (options.preset === Preset.ReactMonorepo) { } else if (options.preset === Preset.ReactMonorepo) {
const { applicationGenerator: reactApplicationGenerator } = require('@nx' + const { applicationGenerator: reactApplicationGenerator } = require('@nx' +

View File

@ -18,4 +18,5 @@ export interface Schema {
e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none';
js?: boolean; js?: boolean;
ssr?: boolean; ssr?: boolean;
prefix?: string;
} }

View File

@ -99,6 +99,10 @@
"description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.", "description": "Enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering) for the Angular application.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"prefix": {
"description": "The prefix to use for Angular component and directive selectors.",
"type": "string"
} }
}, },
"required": ["preset", "name"] "required": ["preset", "name"]