diff --git a/docs/generated/packages/angular/generators/application.json b/docs/generated/packages/angular/generators/application.json index 54d91e2ae48c6..302ddafd9d4d8 100644 --- a/docs/generated/packages/angular/generators/application.json +++ b/docs/generated/packages/angular/generators/application.json @@ -1,6 +1,6 @@ { "name": "application", - "factory": "./src/generators/application/application", + "factory": "./src/generators/application/application#applicationGeneratorInternal", "schema": { "$schema": "http://json-schema.org/schema", "$id": "GeneratorNxApp", @@ -14,13 +14,18 @@ "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What name would you like to use for the application?", - "pattern": "^[a-zA-Z].*$" + "pattern": "^[a-zA-Z][^:]*$" }, "directory": { "description": "The directory of the new application.", "type": "string", "x-priority": "important" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "style": { "description": "The file extension to be used for style files.", "type": "string", @@ -178,7 +183,7 @@ "aliases": ["app"], "x-type": "application", "description": "Creates an Angular application.", - "implementation": "/packages/angular/src/generators/application/application.ts", + "implementation": "/packages/angular/src/generators/application/application#applicationGeneratorInternal.ts", "hidden": false, "path": "/packages/angular/src/generators/application/schema.json", "type": "generator" diff --git a/docs/generated/packages/angular/generators/host.json b/docs/generated/packages/angular/generators/host.json index ac8cc9d76cf7a..09c4d28854216 100644 --- a/docs/generated/packages/angular/generators/host.json +++ b/docs/generated/packages/angular/generators/host.json @@ -1,6 +1,6 @@ { "name": "host", - "factory": "./src/generators/host/host", + "factory": "./src/generators/host/host#hostInternal", "schema": { "$schema": "http://json-schema.org/schema", "$id": "NxMFHost", @@ -19,7 +19,7 @@ "type": "string", "description": "The name to give to the host Angular application.", "$default": { "$source": "argv", "index": 0 }, - "pattern": "^[a-zA-Z].*$" + "pattern": "^[a-zA-Z][^:]*$" }, "remotes": { "type": "array", @@ -35,6 +35,11 @@ "description": "The directory of the new application.", "type": "string" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "style": { "description": "The file extension to be used for style files.", "type": "string", @@ -173,7 +178,7 @@ }, "x-type": "application", "description": "Generate a Host Angular Module Federation Application.", - "implementation": "/packages/angular/src/generators/host/host.ts", + "implementation": "/packages/angular/src/generators/host/host#hostInternal.ts", "aliases": [], "hidden": false, "path": "/packages/angular/src/generators/host/schema.json", diff --git a/docs/generated/packages/angular/generators/library.json b/docs/generated/packages/angular/generators/library.json index d224a8362da6d..81736803806b2 100644 --- a/docs/generated/packages/angular/generators/library.json +++ b/docs/generated/packages/angular/generators/library.json @@ -1,6 +1,6 @@ { "name": "library", - "factory": "./src/generators/library/library", + "factory": "./src/generators/library/library#libraryGeneratorInternal", "schema": { "$schema": "http://json-schema.org/schema", "$id": "GeneratorAngularLibrary", @@ -14,13 +14,18 @@ "description": "The name of the library.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What name would you like to use for the library?", - "pattern": "^[a-zA-Z].*$" + "pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$" }, "directory": { "type": "string", "description": "A directory where the library is placed.", "x-priority": "important" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "publishable": { "type": "boolean", "default": false, @@ -205,7 +210,7 @@ "aliases": ["lib"], "x-type": "library", "description": "Creates an Angular library.", - "implementation": "/packages/angular/src/generators/library/library.ts", + "implementation": "/packages/angular/src/generators/library/library#libraryGeneratorInternal.ts", "hidden": false, "path": "/packages/angular/src/generators/library/schema.json", "type": "generator" diff --git a/docs/generated/packages/angular/generators/remote.json b/docs/generated/packages/angular/generators/remote.json index 1c56c9cc45af4..b2a772a64c0c7 100644 --- a/docs/generated/packages/angular/generators/remote.json +++ b/docs/generated/packages/angular/generators/remote.json @@ -1,6 +1,6 @@ { "name": "remote", - "factory": "./src/generators/remote/remote", + "factory": "./src/generators/remote/remote#remoteInternal", "schema": { "$schema": "http://json-schema.org/schema", "$id": "NxMFRemote", @@ -19,7 +19,7 @@ "type": "string", "description": "The name to give to the remote Angular app.", "$default": { "$source": "argv", "index": 0 }, - "pattern": "^[a-zA-Z].*$" + "pattern": "^[a-zA-Z][^:]*$" }, "host": { "type": "string", @@ -35,6 +35,11 @@ "description": "The directory of the new application.", "type": "string" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "style": { "description": "The file extension to be used for style files.", "type": "string", @@ -166,7 +171,7 @@ }, "x-type": "application", "description": "Generate a Remote Angular Module Federation Application.", - "implementation": "/packages/angular/src/generators/remote/remote.ts", + "implementation": "/packages/angular/src/generators/remote/remote#remoteInternal.ts", "aliases": [], "hidden": false, "path": "/packages/angular/src/generators/remote/schema.json", diff --git a/e2e/angular-core/src/module-federation.test.ts b/e2e/angular-core/src/module-federation.test.ts index 7bd382862d27c..ed1af147afd2c 100644 --- a/e2e/angular-core/src/module-federation.test.ts +++ b/e2e/angular-core/src/module-federation.test.ts @@ -1,5 +1,6 @@ import { names } from '@nx/devkit'; import { + checkFilesExist, cleanupProject, killProcessAndPorts, newProject, @@ -195,4 +196,39 @@ describe('Angular Module Federation', () => { // port and process cleanup await killProcessAndPorts(process.pid, hostPort, remote1Port, remote2Port); }, 20_000_000); + + it('should should support generating host and remote apps with the new name and root format', async () => { + const hostApp = uniq('host'); + const remoteApp = uniq('remote'); + const hostPort = 4800; + const remotePort = 4801; + + // generate host app + runCLI( + `generate @nx/angular:host ${hostApp} --project-name-and-root-format=as-provided --no-interactive` + ); + // generate remote app + runCLI( + `generate @nx/angular:remote ${remoteApp} --host=${hostApp} --port=${remotePort} --project-name-and-root-format=as-provided --no-interactive` + ); + + // check files are generated without the layout directory ("apps/") and + // using the project name as the directory when no directory is provided + checkFilesExist(`${hostApp}/src/app/app.module.ts`); + checkFilesExist(`${remoteApp}/src/app/app.module.ts`); + + // check default generated host is built successfully + const buildOutput = runCLI(`build ${hostApp}`); + expect(buildOutput).toContain('Successfully ran target build'); + + const process = await runCommandUntil( + `serve ${hostApp} --port=${hostPort} --dev-remotes=${remoteApp}`, + (output) => + output.includes(`listening on localhost:${remotePort}`) && + output.includes(`listening on localhost:${hostPort}`) + ); + + // port and process cleanup + await killProcessAndPorts(process.pid, hostPort, remotePort); + }, 20_000_000); }); diff --git a/e2e/angular-core/src/projects.test.ts b/e2e/angular-core/src/projects.test.ts index 879e005217027..b448bdc32bfad 100644 --- a/e2e/angular-core/src/projects.test.ts +++ b/e2e/angular-core/src/projects.test.ts @@ -350,4 +350,53 @@ describe('Angular Projects', () => { ); expect(buildOutput).toContain('Successfully ran target build'); }); + + it('should support generating projects with the new name and root format', () => { + const appName = uniq('app1'); + const libName = uniq('@my-org/lib1'); + + runCLI( + `generate @nx/angular:app ${appName} --project-name-and-root-format=as-provided --no-interactive` + ); + + // check files are generated without the layout directory ("apps/") and + // using the project name as the directory when no directory is provided + checkFilesExist(`${appName}/src/app/app.module.ts`); + // check build works + expect(runCLI(`build ${appName}`)).toContain( + `Successfully ran target build for project ${appName}` + ); + // check tests pass + const appTestResult = runCLI(`test ${appName}`); + expect(appTestResult).toContain( + `Successfully ran target test for project ${appName}` + ); + + // assert scoped project names are not supported when --project-name-and-root-format=derived + expect(() => + runCLI( + `generate @nx/angular:lib ${libName} --buildable --project-name-and-root-format=derived` + ) + ).toThrow(); + + runCLI( + `generate @nx/angular:lib ${libName} --buildable --project-name-and-root-format=as-provided` + ); + + // check files are generated without the layout directory ("libs/") and + // using the project name as the directory when no directory is provided + checkFilesExist( + `${libName}/src/index.ts`, + `${libName}/src/lib/${libName.split('/')[1]}.module.ts` + ); + // check build works + expect(runCLI(`build ${libName}`)).toContain( + `Successfully ran target build for project ${libName}` + ); + // check tests pass + const libTestResult = runCLI(`test ${libName}`); + expect(libTestResult).toContain( + `Successfully ran target test for project ${libName}` + ); + }, 500_000); }); diff --git a/packages/angular/generators.json b/packages/angular/generators.json index 3635a8ece963c..d808a55291c02 100644 --- a/packages/angular/generators.json +++ b/packages/angular/generators.json @@ -163,7 +163,7 @@ "hidden": true }, "application": { - "factory": "./src/generators/application/application", + "factory": "./src/generators/application/application#applicationGeneratorInternal", "schema": "./src/generators/application/schema.json", "aliases": ["app"], "x-type": "application", @@ -211,7 +211,7 @@ "hidden": true }, "library": { - "factory": "./src/generators/library/library", + "factory": "./src/generators/library/library#libraryGeneratorInternal", "schema": "./src/generators/library/schema.json", "aliases": ["lib"], "x-type": "library", @@ -224,7 +224,7 @@ "description": "Creates a secondary entry point for an Angular publishable library." }, "remote": { - "factory": "./src/generators/remote/remote", + "factory": "./src/generators/remote/remote#remoteInternal", "schema": "./src/generators/remote/schema.json", "x-type": "application", "description": "Generate a Remote Angular Module Federation Application." @@ -241,7 +241,7 @@ "description": "Converts an old micro frontend configuration to use the new withModuleFederation helper. It will run successfully if the following conditions are met: \n - Is either a host or remote application \n - Shared npm package configurations have not been modified \n - Name used to identify the Micro Frontend application matches the project name \n\n{% callout type=\"warning\" title=\"Overrides\" %}This generator will overwrite your webpack config. If you have additional custom configuration in your config file, it will be lost!{% /callout %}" }, "host": { - "factory": "./src/generators/host/host", + "factory": "./src/generators/host/host#hostInternal", "schema": "./src/generators/host/schema.json", "x-type": "application", "description": "Generate a Host Angular Module Federation Application." diff --git a/packages/angular/src/generators/application/application.ts b/packages/angular/src/generators/application/application.ts index bc13869a9bd00..049bbaba11966 100644 --- a/packages/angular/src/generators/application/application.ts +++ b/packages/angular/src/generators/application/application.ts @@ -30,6 +30,16 @@ import { prompt } from 'enquirer'; export async function applicationGenerator( tree: Tree, schema: Partial +): Promise { + return await applicationGeneratorInternal(tree, { + projectNameAndRootFormat: 'derived', + ...schema, + }); +} + +export async function applicationGeneratorInternal( + tree: Tree, + schema: Partial ): Promise { const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree); @@ -50,7 +60,7 @@ export async function applicationGenerator( }).then((a) => a['standalone-components']); } - const options = normalizeOptions(tree, schema); + const options = await normalizeOptions(tree, schema); const rootOffset = offsetFromRoot(options.appProjectRoot); await angularInitGenerator(tree, { diff --git a/packages/angular/src/generators/application/lib/add-e2e.ts b/packages/angular/src/generators/application/lib/add-e2e.ts index 525423384b562..d8ddb27a991ad 100644 --- a/packages/angular/src/generators/application/lib/add-e2e.ts +++ b/packages/angular/src/generators/application/lib/add-e2e.ts @@ -10,19 +10,18 @@ import { } from '@nx/devkit'; import { nxVersion } from '../../../utils/versions'; import type { NormalizedSchema } from './normalized-schema'; -import { removeScaffoldedE2e } from './remove-scaffolded-e2e'; import { cypressProjectGenerator } from '@nx/cypress'; export async function addE2e(tree: Tree, options: NormalizedSchema) { - removeScaffoldedE2e(tree, options, options.ngCliSchematicE2ERoot); - if (options.e2eTestRunner === 'cypress') { // TODO: This can call `@nx/web:static-config` generator when ready addFileServerTarget(tree, options, 'serve-static'); await cypressProjectGenerator(tree, { name: options.e2eProjectName, - directory: options.directory, + directory: options.e2eProjectRoot, + // the name and root are already normalized, instruct the generator to use them as is + projectNameAndRootFormat: 'as-provided', project: options.name, linter: options.linter, standaloneConfig: options.standaloneConfig, diff --git a/packages/angular/src/generators/application/lib/index.ts b/packages/angular/src/generators/application/lib/index.ts index 52058c4459f4f..39e82d94e5092 100644 --- a/packages/angular/src/generators/application/lib/index.ts +++ b/packages/angular/src/generators/application/lib/index.ts @@ -7,7 +7,5 @@ export * from './create-project'; export * from './enable-strict-type-checking'; export * from './normalize-options'; export * from './normalized-schema'; -export * from './remove-scaffolded-e2e'; export * from './set-app-strict-default'; -export * from './update-e2e-project'; export * from './update-editor-tsconfig'; diff --git a/packages/angular/src/generators/application/lib/normalize-options.ts b/packages/angular/src/generators/application/lib/normalize-options.ts index bba333e131d7a..f677f4d57be3c 100644 --- a/packages/angular/src/generators/application/lib/normalize-options.ts +++ b/packages/angular/src/generators/application/lib/normalize-options.ts @@ -1,45 +1,43 @@ -import { - extractLayoutDirectory, - getWorkspaceLayout, - joinPathFragments, - names, - Tree, -} from '@nx/devkit'; - +import { 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/linter'; +import { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners'; +import { normalizeNewProjectPrefix } from '../../utils/project'; import type { Schema } from '../schema'; import type { NormalizedSchema } from './normalized-schema'; -import { E2eTestRunner, UnitTestRunner } from '../../../utils/test-runners'; -import { Linter } from '@nx/linter'; -import { - normalizeDirectory, - normalizeNewProjectPrefix, - normalizeProjectName, -} from '../../utils/project'; -export function normalizeOptions( +export async function normalizeOptions( host: Tree, options: Partial -): NormalizedSchema { - const { layoutDirectory, projectDirectory } = extractLayoutDirectory( - options.directory - ); - const appDirectory = normalizeDirectory(options.name, projectDirectory); - const appProjectName = normalizeProjectName(options.name, projectDirectory); - const e2eProjectName = options.rootProject - ? 'e2e' - : `${names(options.name).fileName}-e2e`; +): Promise { + const { + projectName: appProjectName, + projectRoot: appProjectRoot, + projectNameAndRootFormat, + } = await determineProjectNameAndRootOptions(host, { + name: options.name, + projectType: 'application', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + rootProject: options.rootProject, + }); + options.rootProject = appProjectRoot === '.'; + options.projectNameAndRootFormat = projectNameAndRootFormat; - const { appsDir: defaultAppsDir, standaloneAsDefault } = - getWorkspaceLayout(host); - const appsDir = layoutDirectory ?? defaultAppsDir; - const appProjectRoot = options.rootProject - ? '.' - : joinPathFragments(appsDir, appDirectory); - const e2eProjectRoot = options.rootProject - ? 'e2e' - : joinPathFragments(appsDir, `${appDirectory}-e2e`); + let e2eProjectName = 'e2e'; + let e2eProjectRoot = 'e2e'; + if (!options.rootProject) { + const projectNameAndRoot = await determineProjectNameAndRootOptions(host, { + name: `${options.name}-e2e`, + projectType: 'application', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + rootProject: options.rootProject, + }); + e2eProjectName = projectNameAndRoot.projectName; + e2eProjectRoot = projectNameAndRoot.projectRoot; + } const parsedTags = options.tags ? options.tags.split(',').map((s) => s.trim()) @@ -51,11 +49,6 @@ export function normalizeOptions( 'app' ); - options.standaloneConfig = options.standaloneConfig ?? standaloneAsDefault; - - const ngCliSchematicAppRoot = appProjectName; - const ngCliSchematicE2ERoot = `${appProjectName}/e2e`; - // Set defaults and then overwrite with user options return { style: 'css', @@ -76,7 +69,5 @@ export function normalizeOptions( e2eProjectRoot, e2eProjectName, parsedTags, - ngCliSchematicAppRoot, - ngCliSchematicE2ERoot, }; } diff --git a/packages/angular/src/generators/application/lib/normalized-schema.ts b/packages/angular/src/generators/application/lib/normalized-schema.ts index 67aa9fc0c576d..5eb30bdb724e3 100644 --- a/packages/angular/src/generators/application/lib/normalized-schema.ts +++ b/packages/angular/src/generators/application/lib/normalized-schema.ts @@ -11,6 +11,4 @@ export interface NormalizedSchema extends Schema { e2eProjectName: string; e2eProjectRoot: string; parsedTags: string[]; - ngCliSchematicAppRoot: string; - ngCliSchematicE2ERoot: string; } diff --git a/packages/angular/src/generators/application/lib/remove-scaffolded-e2e.ts b/packages/angular/src/generators/application/lib/remove-scaffolded-e2e.ts deleted file mode 100644 index 6f3ca07e258c3..0000000000000 --- a/packages/angular/src/generators/application/lib/remove-scaffolded-e2e.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Tree } from '@nx/devkit'; -import type { NormalizedSchema } from './normalized-schema'; - -import { - updateProjectConfiguration, - readProjectConfiguration, -} from '@nx/devkit'; - -export function removeScaffoldedE2e( - host: Tree, - { name }: NormalizedSchema, - e2eProjectRoot: string -) { - if (host.exists(`${e2eProjectRoot}/src/app.e2e-spec.ts`)) { - host.delete(`${e2eProjectRoot}/src/app.e2e-spec.ts`); - } - if (host.exists(`${e2eProjectRoot}/src/app.po.ts`)) { - host.delete(`${e2eProjectRoot}/src/app.po.ts`); - } - if (host.exists(`${e2eProjectRoot}/protractor.conf.js`)) { - host.delete(`${e2eProjectRoot}/protractor.conf.js`); - } - if (host.exists(`${e2eProjectRoot}/tsconfig.json`)) { - host.delete(`${e2eProjectRoot}/tsconfig.json`); - } - - const project = readProjectConfiguration(host, name); - delete project.targets['e2e']; - - updateProjectConfiguration(host, name, project); -} diff --git a/packages/angular/src/generators/application/lib/update-e2e-project.ts b/packages/angular/src/generators/application/lib/update-e2e-project.ts deleted file mode 100644 index 747f690c28963..0000000000000 --- a/packages/angular/src/generators/application/lib/update-e2e-project.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { ProjectConfiguration, Tree } from '@nx/devkit'; -import { - addProjectConfiguration, - offsetFromRoot, - readProjectConfiguration, - updateJson, - updateProjectConfiguration, -} from '@nx/devkit'; -import { getRelativePathToRootTsConfig } from '@nx/js'; -import type { NormalizedSchema } from './normalized-schema'; - -export function updateE2eProject(tree: Tree, options: NormalizedSchema) { - const spec = `${options.e2eProjectRoot}/src/app.e2e-spec.ts`; - const content = tree.read(spec, 'utf-8'); - tree.write( - spec, - content.replace( - `${options.name} app is running!`, - `Welcome ${options.name}` - ) - ); - - const page = `${options.e2eProjectRoot}/src/app.po.ts`; - const pageContent = tree.read(page, 'utf-8'); - tree.write(page, pageContent.replace(`.content span`, `header h1`)); - - const proj = readProjectConfiguration(tree, options.name); - const project: ProjectConfiguration = { - root: options.e2eProjectRoot, - projectType: 'application', - targets: { - e2e: proj.targets.e2e, - }, - implicitDependencies: [options.name], - tags: [], - }; - project.targets.e2e.options.protractorConfig = `${options.e2eProjectRoot}/protractor.conf.js`; - addProjectConfiguration(tree, options.e2eProjectName, project); - - delete proj.targets.e2e; - updateProjectConfiguration(tree, options.name, proj); - - // update tsconfig e2e - if (!tree.exists(`${options.e2eProjectRoot}/tsconfig.e2e.json`)) { - tree.write(`${options.e2eProjectRoot}/tsconfig.e2e.json`, '{}'); - } - - updateJson(tree, `${options.e2eProjectRoot}/tsconfig.e2e.json`, (json) => { - return { - ...json, - extends: `./tsconfig.json`, - compilerOptions: { - ...json.compilerOptions, - outDir: `${offsetFromRoot(options.e2eProjectRoot)}dist/out-tsc`, - }, - }; - }); - - // update tsconfig - updateJson(tree, `${options.e2eProjectRoot}/tsconfig.json`, (json) => { - return { - ...json, - extends: getRelativePathToRootTsConfig(tree, options.e2eProjectRoot), - }; - }); -} diff --git a/packages/angular/src/generators/application/schema.d.ts b/packages/angular/src/generators/application/schema.d.ts index c6ed74fbf65c5..c13a0ec15825a 100644 --- a/packages/angular/src/generators/application/schema.d.ts +++ b/packages/angular/src/generators/application/schema.d.ts @@ -1,5 +1,6 @@ -import { Linter } from '@nx/linter'; -import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners'; +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { Linter } from '@nx/linter'; +import type { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners'; import type { Styles } from '../utils/types'; export interface Schema { @@ -14,6 +15,7 @@ export interface Schema { style?: Styles; skipTests?: boolean; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; tags?: string; linter?: Linter; unitTestRunner?: UnitTestRunner; diff --git a/packages/angular/src/generators/application/schema.json b/packages/angular/src/generators/application/schema.json index d92bf5021f006..1cc21df7cb186 100644 --- a/packages/angular/src/generators/application/schema.json +++ b/packages/angular/src/generators/application/schema.json @@ -14,13 +14,18 @@ "index": 0 }, "x-prompt": "What name would you like to use for the application?", - "pattern": "^[a-zA-Z].*$" + "pattern": "^[a-zA-Z][^:]*$" }, "directory": { "description": "The directory of the new application.", "type": "string", "x-priority": "important" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "style": { "description": "The file extension to be used for style files.", "type": "string", diff --git a/packages/angular/src/generators/host/host.ts b/packages/angular/src/generators/host/host.ts index 4691476791ed7..13fd02dd27eb7 100644 --- a/packages/angular/src/generators/host/host.ts +++ b/packages/angular/src/generators/host/host.ts @@ -1,23 +1,28 @@ import { - extractLayoutDirectory, formatFiles, getProjects, runTasksInSerial, stripIndents, Tree, } from '@nx/devkit'; -import type { Schema } from './schema'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { lt } from 'semver'; +import { E2eTestRunner } from '../../utils/test-runners'; import applicationGenerator from '../application/application'; import remoteGenerator from '../remote/remote'; -import { normalizeProjectName } from '../utils/project'; import { setupMf } from '../setup-mf/setup-mf'; -import { E2eTestRunner } from '../../utils/test-runners'; -import { addSsr } from './lib'; - import { getInstalledAngularVersionInfo } from '../utils/version-utils'; -import { lt } from 'semver'; +import { addSsr } from './lib'; +import type { Schema } from './schema'; export async function host(tree: Tree, options: Schema) { + return await hostInternal(tree, { + projectNameAndRootFormat: 'derived', + ...options, + }); +} + +export async function hostInternal(tree: Tree, options: Schema) { const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree); if (lt(installedAngularVersionInfo.version, '14.1.0') && options.standalone) { @@ -40,8 +45,14 @@ export async function host(tree: Tree, options: Schema) { }); } - const { projectDirectory } = extractLayoutDirectory(options.directory); - const appName = normalizeProjectName(options.name, projectDirectory); + const { projectName: hostProjectName, projectNameAndRootFormat } = + await determineProjectNameAndRootOptions(tree, { + name: options.name, + projectType: 'application', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + }); + options.projectNameAndRootFormat = projectNameAndRootFormat; const appInstallTask = await applicationGenerator(tree, { ...options, @@ -54,7 +65,7 @@ export async function host(tree: Tree, options: Schema) { const skipE2E = !options.e2eTestRunner || options.e2eTestRunner === E2eTestRunner.None; await setupMf(tree, { - appName, + appName: hostProjectName, mfType: 'host', routing: true, port: 4200, @@ -63,13 +74,13 @@ export async function host(tree: Tree, options: Schema) { skipPackageJson: options.skipPackageJson, skipFormat: true, skipE2E, - e2eProjectName: skipE2E ? undefined : `${appName}-e2e`, + e2eProjectName: skipE2E ? undefined : `${hostProjectName}-e2e`, prefix: options.prefix, }); let installTasks = [appInstallTask]; if (options.ssr) { - let ssrInstallTask = await addSsr(tree, options, appName); + let ssrInstallTask = await addSsr(tree, options, hostProjectName); installTasks.push(ssrInstallTask); } @@ -77,7 +88,7 @@ export async function host(tree: Tree, options: Schema) { await remoteGenerator(tree, { ...options, name: remote, - host: appName, + host: hostProjectName, skipFormat: true, standalone: options.standalone, }); diff --git a/packages/angular/src/generators/host/schema.d.ts b/packages/angular/src/generators/host/schema.d.ts index b5148633847fd..4ad7be7d2578f 100644 --- a/packages/angular/src/generators/host/schema.d.ts +++ b/packages/angular/src/generators/host/schema.d.ts @@ -1,5 +1,6 @@ -import { Linter } from '@nx/linter'; -import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners'; +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { Linter } from '@nx/linter'; +import type { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners'; import type { Styles } from '../utils/types'; export interface Schema { @@ -14,6 +15,7 @@ export interface Schema { style?: Styles; skipTests?: boolean; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; tags?: string; linter?: Linter; unitTestRunner?: UnitTestRunner; diff --git a/packages/angular/src/generators/host/schema.json b/packages/angular/src/generators/host/schema.json index b37009f314a2d..5c751de9db98b 100644 --- a/packages/angular/src/generators/host/schema.json +++ b/packages/angular/src/generators/host/schema.json @@ -19,7 +19,7 @@ "$source": "argv", "index": 0 }, - "pattern": "^[a-zA-Z].*$" + "pattern": "^[a-zA-Z][^:]*$" }, "remotes": { "type": "array", @@ -35,6 +35,11 @@ "description": "The directory of the new application.", "type": "string" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "style": { "description": "The file extension to be used for style files.", "type": "string", diff --git a/packages/angular/src/generators/library/lib/normalize-options.ts b/packages/angular/src/generators/library/lib/normalize-options.ts index ee0b4899d6c8b..278a01138a6fe 100644 --- a/packages/angular/src/generators/library/lib/normalize-options.ts +++ b/packages/angular/src/generators/library/lib/normalize-options.ts @@ -1,22 +1,16 @@ -import { - extractLayoutDirectory, - getWorkspaceLayout, - joinPathFragments, - names, - Tree, -} from '@nx/devkit'; - -import { getImportPath } from '@nx/js/src/utils/get-import-path'; +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/linter'; - -import { Schema } from '../schema'; -import { NormalizedSchema } from './normalized-schema'; import { UnitTestRunner } from '../../../utils/test-runners'; import { normalizeNewProjectPrefix } from '../../utils/project'; +import { Schema } from '../schema'; +import { NormalizedSchema } from './normalized-schema'; -export function normalizeOptions(host: Tree, schema: Schema): NormalizedSchema { +export async function normalizeOptions( + host: Tree, + schema: Schema +): Promise { // Create a schema with populated default values const options: Schema = { buildable: false, @@ -33,36 +27,31 @@ export function normalizeOptions(host: Tree, schema: Schema): NormalizedSchema { ...schema, }; - const name = names(options.name).fileName; - const { layoutDirectory, projectDirectory } = extractLayoutDirectory( - options.directory - ); - const fullProjectDirectory = projectDirectory - ? `${names(projectDirectory).fileName}/${name}`.replace(/\/+/g, '/') - : name; - - const { libsDir: defaultLibsDirectory, standaloneAsDefault } = - getWorkspaceLayout(host); - const npmScope = getNpmScope(host); - const libsDir = layoutDirectory ?? defaultLibsDirectory; + const { + projectName, + names: projectNames, + projectRoot, + importPath, + } = await determineProjectNameAndRootOptions(host, { + name: options.name, + projectType: 'library', + directory: options.directory, + importPath: options.importPath, + projectNameAndRootFormat: options.projectNameAndRootFormat, + }); - const projectName = fullProjectDirectory - .replace(new RegExp('/', 'g'), '-') - .replace(/-\d+/g, ''); - const fileName = options.simpleName ? name : projectName; - const projectRoot = joinPathFragments(libsDir, fullProjectDirectory); + const fileName = options.simpleName + ? projectNames.projectSimpleName + : projectNames.projectFileName; const moduleName = `${names(fileName).className}Module`; const parsedTags = options.tags ? options.tags.split(',').map((s) => s.trim()) : []; const modulePath = `${projectRoot}/src/lib/${fileName}.module.ts`; - const prefix = normalizeNewProjectPrefix(options.prefix, npmScope, 'lib'); - - options.standaloneConfig = options.standaloneConfig ?? standaloneAsDefault; - const importPath = - options.importPath || getImportPath(host, fullProjectDirectory); + const npmScope = getNpmScope(host); + const prefix = normalizeNewProjectPrefix(options.prefix, npmScope, 'lib'); const ngCliSchematicLibRoot = projectName; const allNormalizedOptions = { @@ -74,13 +63,14 @@ export function normalizeOptions(host: Tree, schema: Schema): NormalizedSchema { projectRoot, entryFile: 'index', moduleName, - projectDirectory: fullProjectDirectory, modulePath, parsedTags, fileName, importPath, ngCliSchematicLibRoot, - standaloneComponentName: `${names(name).className}Component`, + standaloneComponentName: `${ + names(projectNames.projectSimpleName).className + }Component`, }; const { diff --git a/packages/angular/src/generators/library/lib/normalized-schema.ts b/packages/angular/src/generators/library/lib/normalized-schema.ts index 80555aa209b3f..920652d5ce2c5 100644 --- a/packages/angular/src/generators/library/lib/normalized-schema.ts +++ b/packages/angular/src/generators/library/lib/normalized-schema.ts @@ -35,7 +35,6 @@ export interface NormalizedSchema { entryFile: string; modulePath: string; moduleName: string; - projectDirectory: string; parsedTags: string[]; ngCliSchematicLibRoot: string; standaloneComponentName: string; diff --git a/packages/angular/src/generators/library/library.spec.ts b/packages/angular/src/generators/library/library.spec.ts index 502f15e8c0f51..2c528160785c0 100644 --- a/packages/angular/src/generators/library/library.spec.ts +++ b/packages/angular/src/generators/library/library.spec.ts @@ -571,7 +571,7 @@ describe('lib', () => { it('should accept numbers in the path', async () => { await runLibraryGeneratorWithOpts({ directory: 'src/1-api' }); - expect(readProjectConfiguration(tree, 'src-api-my-lib').root).toEqual( + expect(readProjectConfiguration(tree, 'src-1-api-my-lib').root).toEqual( 'src/1-api/my-lib' ); }); diff --git a/packages/angular/src/generators/library/library.ts b/packages/angular/src/generators/library/library.ts index 69ca86e3b8a8c..7a70d7a56232b 100644 --- a/packages/angular/src/generators/library/library.ts +++ b/packages/angular/src/generators/library/library.ts @@ -36,6 +36,18 @@ import { addProject } from './lib/add-project'; export async function libraryGenerator( tree: Tree, schema: Schema +): Promise { + return await libraryGeneratorInternal(tree, { + // provide a default projectNameAndRootFormat to avoid breaking changes + // to external generators invoking this one + projectNameAndRootFormat: 'derived', + ...schema, + }); +} + +export async function libraryGeneratorInternal( + tree: Tree, + schema: Schema ): Promise { // Do some validation checks if (!schema.routing && schema.lazy) { @@ -61,7 +73,7 @@ export async function libraryGenerator( ); } - const options = normalizeOptions(tree, schema); + const options = await normalizeOptions(tree, schema); const { libraryOptions } = options; const pkgVersions = versions(tree); diff --git a/packages/angular/src/generators/library/schema.d.ts b/packages/angular/src/generators/library/schema.d.ts index 99a357c9fce2a..88b39de2f9fdb 100644 --- a/packages/angular/src/generators/library/schema.d.ts +++ b/packages/angular/src/generators/library/schema.d.ts @@ -1,5 +1,6 @@ -import { UnitTestRunner } from '../../utils/test-runners'; -import { Linter } from '@nx/linter'; +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-directory-utils'; +import type { Linter } from '@nx/linter'; +import type { UnitTestRunner } from '../../utils/test-runners'; export interface Schema { name: string; @@ -8,6 +9,7 @@ export interface Schema { simpleName?: boolean; addModuleSpec?: boolean; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; sourceDir?: string; buildable?: boolean; publishable?: boolean; diff --git a/packages/angular/src/generators/library/schema.json b/packages/angular/src/generators/library/schema.json index e75089f097afa..ca2858fae504c 100644 --- a/packages/angular/src/generators/library/schema.json +++ b/packages/angular/src/generators/library/schema.json @@ -14,13 +14,18 @@ "index": 0 }, "x-prompt": "What name would you like to use for the library?", - "pattern": "^[a-zA-Z].*$" + "pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$" }, "directory": { "type": "string", "description": "A directory where the library is placed.", "x-priority": "important" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "publishable": { "type": "boolean", "default": false, diff --git a/packages/angular/src/generators/remote/remote.ts b/packages/angular/src/generators/remote/remote.ts index 8754fb98f39fc..7104f49ad8cc3 100644 --- a/packages/angular/src/generators/remote/remote.ts +++ b/packages/angular/src/generators/remote/remote.ts @@ -1,22 +1,27 @@ import { - extractLayoutDirectory, formatFiles, getProjects, runTasksInSerial, stripIndents, Tree, } from '@nx/devkit'; -import type { Schema } from './schema'; -import applicationGenerator from '../application/application'; -import { normalizeProjectName } from '../utils/project'; -import { setupMf } from '../setup-mf/setup-mf'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { lt } from 'semver'; import { E2eTestRunner } from '../../utils/test-runners'; -import { addSsr, findNextAvailablePort } from './lib'; - +import { applicationGenerator } from '../application/application'; +import { setupMf } from '../setup-mf/setup-mf'; import { getInstalledAngularVersionInfo } from '../utils/version-utils'; -import { lt } from 'semver'; +import { addSsr, findNextAvailablePort } from './lib'; +import type { Schema } from './schema'; export async function remote(tree: Tree, options: Schema) { + return await remoteInternal(tree, { + projectNameAndRootFormat: 'derived', + ...options, + }); +} + +export async function remoteInternal(tree: Tree, options: Schema) { const installedAngularVersionInfo = getInstalledAngularVersionInfo(tree); if (lt(installedAngularVersionInfo.version, '14.1.0') && options.standalone) { @@ -31,8 +36,15 @@ export async function remote(tree: Tree, options: Schema) { ); } - const { projectDirectory } = extractLayoutDirectory(options.directory); - const appName = normalizeProjectName(options.name, projectDirectory); + const { projectName: remoteProjectName, projectNameAndRootFormat } = + await determineProjectNameAndRootOptions(tree, { + name: options.name, + projectType: 'application', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + }); + options.projectNameAndRootFormat = projectNameAndRootFormat; + const port = options.port ?? findNextAvailablePort(tree); const appInstallTask = await applicationGenerator(tree, { @@ -47,7 +59,7 @@ export async function remote(tree: Tree, options: Schema) { !options.e2eTestRunner || options.e2eTestRunner === E2eTestRunner.None; await setupMf(tree, { - appName, + appName: remoteProjectName, mfType: 'remote', routing: true, host: options.host, @@ -55,7 +67,7 @@ export async function remote(tree: Tree, options: Schema) { skipPackageJson: options.skipPackageJson, skipFormat: true, skipE2E, - e2eProjectName: skipE2E ? undefined : `${appName}-e2e`, + e2eProjectName: skipE2E ? undefined : `${remoteProjectName}-e2e`, standalone: options.standalone, prefix: options.prefix, }); @@ -63,7 +75,7 @@ export async function remote(tree: Tree, options: Schema) { let installTasks = [appInstallTask]; if (options.ssr) { let ssrInstallTask = await addSsr(tree, { - appName, + appName: remoteProjectName, port, standalone: options.standalone, }); diff --git a/packages/angular/src/generators/remote/schema.d.ts b/packages/angular/src/generators/remote/schema.d.ts index 97d43327a0c39..e2ec3c49a06d5 100644 --- a/packages/angular/src/generators/remote/schema.d.ts +++ b/packages/angular/src/generators/remote/schema.d.ts @@ -1,5 +1,6 @@ -import { Linter } from '@nx/linter'; -import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners'; +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { Linter } from '@nx/linter'; +import type { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners'; import type { Styles } from '../utils/types'; export interface Schema { @@ -13,6 +14,7 @@ export interface Schema { style?: Styles; skipTests?: boolean; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; tags?: string; linter?: Linter; unitTestRunner?: UnitTestRunner; diff --git a/packages/angular/src/generators/remote/schema.json b/packages/angular/src/generators/remote/schema.json index 9c72063146039..2817cdeb89c81 100644 --- a/packages/angular/src/generators/remote/schema.json +++ b/packages/angular/src/generators/remote/schema.json @@ -19,7 +19,7 @@ "$source": "argv", "index": 0 }, - "pattern": "^[a-zA-Z].*$" + "pattern": "^[a-zA-Z][^:]*$" }, "host": { "type": "string", @@ -35,6 +35,11 @@ "description": "The directory of the new application.", "type": "string" }, + "projectNameAndRootFormat": { + "description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).", + "type": "string", + "enum": ["as-provided", "derived"] + }, "style": { "description": "The file extension to be used for style files.", "type": "string", diff --git a/packages/devkit/src/generators/project-name-and-root-utils.spec.ts b/packages/devkit/src/generators/project-name-and-root-utils.spec.ts index c16e03b41d7a6..1876b0e2540e2 100644 --- a/packages/devkit/src/generators/project-name-and-root-utils.spec.ts +++ b/packages/devkit/src/generators/project-name-and-root-utils.spec.ts @@ -50,6 +50,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@proj/lib-name', projectRoot: 'shared', + projectNameAndRootFormat: 'as-provided', }); }); @@ -69,6 +70,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@scope/lib-name', projectRoot: 'shared', + projectNameAndRootFormat: 'as-provided', }); }); @@ -89,6 +91,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@custom-scope/lib-name', projectRoot: 'shared', + projectNameAndRootFormat: 'as-provided', }); }); @@ -111,6 +114,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@scope/lib-name', projectRoot: '@scope/lib-name', + projectNameAndRootFormat: 'as-provided', }); }); @@ -135,6 +139,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: 'lib-name', projectRoot: '.', + projectNameAndRootFormat: 'as-provided', }); }); @@ -181,6 +186,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@proj/shared/lib-name', projectRoot: 'shared/lib-name', + projectNameAndRootFormat: 'derived', }); }); @@ -215,6 +221,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: 'lib-name', projectRoot: '.', + projectNameAndRootFormat: 'derived', }); }); @@ -288,6 +295,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@scope/lib-name', projectRoot: 'shared', + projectNameAndRootFormat: 'as-provided', }); // restore original interactive mode @@ -317,6 +325,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@proj/lib-name', projectRoot: 'shared', + projectNameAndRootFormat: 'as-provided', }); }); @@ -336,6 +345,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@scope/lib-name', projectRoot: 'shared', + projectNameAndRootFormat: 'as-provided', }); }); @@ -356,6 +366,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@custom-scope/lib-name', projectRoot: 'shared', + projectNameAndRootFormat: 'as-provided', }); }); @@ -379,6 +390,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@scope/lib-name', projectRoot: '@scope/lib-name', + projectNameAndRootFormat: 'as-provided', }); }); @@ -403,6 +415,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: 'lib-name', projectRoot: '.', + projectNameAndRootFormat: 'as-provided', }); }); @@ -449,6 +462,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@proj/shared/lib-name', projectRoot: 'libs/shared/lib-name', + projectNameAndRootFormat: 'derived', }); }); @@ -484,6 +498,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: 'lib-name', projectRoot: '.', + projectNameAndRootFormat: 'derived', }); }); @@ -557,6 +572,7 @@ describe('determineProjectNameAndRootOptions', () => { }, importPath: '@scope/lib-name', projectRoot: 'shared', + projectNameAndRootFormat: 'as-provided', }); // restore original interactive mode diff --git a/packages/devkit/src/generators/project-name-and-root-utils.ts b/packages/devkit/src/generators/project-name-and-root-utils.ts index db395a1de8dc2..42133b255b2d9 100644 --- a/packages/devkit/src/generators/project-name-and-root-utils.ts +++ b/packages/devkit/src/generators/project-name-and-root-utils.ts @@ -57,13 +57,20 @@ type ProjectNameAndRootFormats = { export async function determineProjectNameAndRootOptions( tree: Tree, options: ProjectGenerationOptions -): Promise { +): Promise< + ProjectNameAndRootOptions & { + projectNameAndRootFormat: ProjectNameAndRootFormat; + } +> { validateName(options.name, options.projectNameAndRootFormat); const formats = getProjectNameAndRootFormats(tree, options); const format = options.projectNameAndRootFormat ?? (await determineFormat(formats)); - return formats[format]; + return { + ...formats[format], + projectNameAndRootFormat: format, + }; } function validateName(