diff --git a/docs/generated/packages/node/generators/application.json b/docs/generated/packages/node/generators/application.json index a8e6fae87bfa6..cc408980ccae9 100644 --- a/docs/generated/packages/node/generators/application.json +++ b/docs/generated/packages/node/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", "cli": "nx", @@ -14,13 +14,19 @@ "type": "string", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What name would you like to use for the node application?", - "x-priority": "important" + "x-priority": "important", + "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"] + }, "skipFormat": { "description": "Skip formatting files", "type": "boolean", @@ -131,7 +137,7 @@ "aliases": ["app"], "x-type": "application", "description": "Create a node application.", - "implementation": "/packages/node/src/generators/application/application.ts", + "implementation": "/packages/node/src/generators/application/application#applicationGeneratorInternal.ts", "hidden": false, "path": "/packages/node/src/generators/application/schema.json", "type": "generator" diff --git a/docs/generated/packages/node/generators/library.json b/docs/generated/packages/node/generators/library.json index 47c5f4a86eab8..15a267219010e 100644 --- a/docs/generated/packages/node/generators/library.json +++ b/docs/generated/packages/node/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", "cli": "nx", @@ -19,13 +19,19 @@ "type": "string", "description": "Library name", "$default": { "$source": "argv", "index": 0 }, - "x-prompt": "What name would you like to use for the library?" + "x-prompt": "What name would you like to use for the library?", + "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 lib is placed", "alias": "dir" }, + "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"] + }, "simpleModuleName": { "description": "Keep the module name simple (when using `--directory`).", "type": "boolean", @@ -131,7 +137,7 @@ "aliases": ["lib"], "x-type": "library", "description": "Create a node library.", - "implementation": "/packages/node/src/generators/library/library.ts", + "implementation": "/packages/node/src/generators/library/library#libraryGeneratorInternal.ts", "hidden": false, "path": "/packages/node/src/generators/library/schema.json", "type": "generator" diff --git a/e2e/node/src/node.test.ts b/e2e/node/src/node.test.ts index 45d264945742e..febf9604647ef 100644 --- a/e2e/node/src/node.test.ts +++ b/e2e/node/src/node.test.ts @@ -438,6 +438,52 @@ ${jslib}(); checkFilesExist(`dist/apps/_should_keep.txt`); }, 120000); + 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/node: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/main.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/node:lib ${libName} --buildable --project-name-and-root-format=derived --no-interactive` + ) + ).toThrow(); + + runCLI( + `generate @nx/node:lib ${libName} --buildable --project-name-and-root-format=as-provided --no-interactive` + ); + + // 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`); + // 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); + describe('NestJS', () => { it('should have plugin output if specified in `tsPlugins`', async () => { newProject(); diff --git a/packages/node/generators.json b/packages/node/generators.json index 59c0edb0f32cb..4da769bc749c6 100644 --- a/packages/node/generators.json +++ b/packages/node/generators.json @@ -11,14 +11,14 @@ "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", "description": "Create a node application." }, "library": { - "factory": "./src/generators/library/library", + "factory": "./src/generators/library/library#libraryGeneratorInternal", "schema": "./src/generators/library/schema.json", "aliases": ["lib"], "x-type": "library", diff --git a/packages/node/src/generators/application/application.spec.ts b/packages/node/src/generators/application/application.spec.ts index 6384729c0ff4f..823cffb1d85d3 100644 --- a/packages/node/src/generators/application/application.spec.ts +++ b/packages/node/src/generators/application/application.spec.ts @@ -197,7 +197,7 @@ describe('app', () => { expect(() => readProjectConfiguration(tree, 'my-dir-my-node-app-e2e') - ).toThrow(/Cannot find/); + ).not.toThrow(); }); it('should update tags', async () => { diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index cb9ff86aac9f1..a7349b4c05971 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -3,11 +3,9 @@ import { addProjectConfiguration, convertNxGenerator, ensurePackage, - extractLayoutDirectory, formatFiles, generateFiles, GeneratorCallback, - getWorkspaceLayout, joinPathFragments, logger, names, @@ -22,13 +20,13 @@ import { updateProjectConfiguration, updateTsConfigsToJs, } from '@nx/devkit'; -import { Linter, lintProjectGenerator } from '@nx/linter'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { configurationGenerator } from '@nx/jest'; - import { getRelativePathToRootTsConfig, tsConfigBaseOptions } from '@nx/js'; +import { esbuildVersion } from '@nx/js/src/utils/versions'; +import { Linter, lintProjectGenerator } from '@nx/linter'; +import { mapLintPattern } from '@nx/linter/src/generators/lint-project/lint-project'; import { join } from 'path'; - -import { initGenerator } from '../init/init'; import { expressTypingsVersion, expressVersion, @@ -41,11 +39,9 @@ import { nxVersion, } from '../../utils/versions'; import { e2eProjectGenerator } from '../e2e-project/e2e-project'; +import { initGenerator } from '../init/init'; import { setupDockerGenerator } from '../setup-docker/setup-docker'; - import { Schema } from './schema'; -import { mapLintPattern } from '@nx/linter/src/generators/lint-project/lint-project'; -import { esbuildVersion } from '@nx/js/src/utils/versions'; export interface NormalizedSchema extends Schema { appProjectRoot: string; @@ -364,7 +360,14 @@ function updateTsConfigOptions(tree: Tree, options: NormalizedSchema) { } export async function applicationGenerator(tree: Tree, schema: Schema) { - const options = normalizeOptions(tree, schema); + return await applicationGeneratorInternal(tree, { + projectNameAndRootFormat: 'derived', + ...schema, + }); +} + +export async function applicationGeneratorInternal(tree: Tree, schema: Schema) { + const options = await normalizeOptions(tree, schema); const tasks: GeneratorCallback[] = []; if (options.framework === 'nest') { @@ -414,6 +417,8 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { ...options, projectType: options.framework === 'none' ? 'cli' : 'server', name: options.rootProject ? 'e2e' : `${options.name}-e2e`, + directory: options.rootProject ? 'e2e' : `${options.appProjectRoot}-e2e`, + projectNameAndRootFormat: 'as-provided', project: options.name, port: options.port, isNest: options.isNest, @@ -447,21 +452,24 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { return runTasksInSerial(...tasks); } -function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { - const { layoutDirectory, projectDirectory } = extractLayoutDirectory( - options.directory - ); - const appsDir = layoutDirectory ?? getWorkspaceLayout(host).appsDir; - - const appDirectory = projectDirectory - ? `${names(projectDirectory).fileName}/${names(options.name).fileName}` - : names(options.name).fileName; - - const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-'); - - const appProjectRoot = options.rootProject - ? '.' - : joinPathFragments(appsDir, appDirectory); +async function normalizeOptions( + host: Tree, + options: Schema +): Promise { + const { + projectName: appProjectName, + projectRoot: appProjectRoot, + projectNameAndRootFormat, + } = await determineProjectNameAndRootOptions(host, { + name: options.name, + projectType: 'application', + directory: options.directory, + projectNameAndRootFormat: options.projectNameAndRootFormat, + rootProject: options.rootProject, + callingGenerator: '@nx/node:application', + }); + options.rootProject = appProjectRoot === '.'; + options.projectNameAndRootFormat = projectNameAndRootFormat; options.bundler = options.bundler ?? 'esbuild'; options.e2eTestRunner = options.e2eTestRunner ?? 'jest'; @@ -472,7 +480,7 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { return { ...options, - name: names(appProjectName).fileName, + name: appProjectName, frontendProject: options.frontendProject ? names(options.frontendProject).fileName : undefined, diff --git a/packages/node/src/generators/application/schema.d.ts b/packages/node/src/generators/application/schema.d.ts index 70f30e83a5be6..5f7de5dae1b58 100644 --- a/packages/node/src/generators/application/schema.d.ts +++ b/packages/node/src/generators/application/schema.d.ts @@ -1,10 +1,12 @@ -import { Linter } from '@nx/linter'; +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { Linter } from '@nx/linter'; export interface Schema { name: string; skipFormat?: boolean; skipPackageJson?: boolean; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; unitTestRunner?: 'jest' | 'none'; e2eTestRunner?: 'jest' | 'none'; linter?: Linter; diff --git a/packages/node/src/generators/application/schema.json b/packages/node/src/generators/application/schema.json index a1118404bfafc..3e2cf2917e996 100644 --- a/packages/node/src/generators/application/schema.json +++ b/packages/node/src/generators/application/schema.json @@ -14,13 +14,19 @@ "index": 0 }, "x-prompt": "What name would you like to use for the node application?", - "x-priority": "important" + "x-priority": "important", + "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"] + }, "skipFormat": { "description": "Skip formatting files", "type": "boolean", diff --git a/packages/node/src/generators/e2e-project/e2e-project.ts b/packages/node/src/generators/e2e-project/e2e-project.ts index f32bbfe955280..4569436ff7747 100644 --- a/packages/node/src/generators/e2e-project/e2e-project.ts +++ b/packages/node/src/generators/e2e-project/e2e-project.ts @@ -1,13 +1,10 @@ -import * as path from 'path'; import { addDependenciesToPackageJson, addProjectConfiguration, convertNxGenerator, - extractLayoutDirectory, formatFiles, generateFiles, GeneratorCallback, - getWorkspaceLayout, joinPathFragments, names, offsetFromRoot, @@ -16,19 +13,30 @@ import { Tree, updateJson, } from '@nx/devkit'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Linter, lintProjectGenerator } from '@nx/linter'; - -import { Schema } from './schema'; -import { axiosVersion } from '../../utils/versions'; -import { join } from 'path'; import { globalJavaScriptOverrides, globalTypeScriptOverrides, } from '@nx/linter/src/generators/init/global-eslint-config'; +import * as path from 'path'; +import { join } from 'path'; +import { axiosVersion } from '../../utils/versions'; +import { Schema } from './schema'; -export async function e2eProjectGenerator(host: Tree, _options: Schema) { +export async function e2eProjectGenerator(host: Tree, options: Schema) { + return await e2eProjectGeneratorInternal(host, { + projectNameAndRootFormat: 'derived', + ...options, + }); +} + +export async function e2eProjectGeneratorInternal( + host: Tree, + _options: Schema +) { const tasks: GeneratorCallback[] = []; - const options = normalizeOptions(host, _options); + const options = await normalizeOptions(host, _options); const appProject = readProjectConfiguration(host, options.project); addProjectConfiguration(host, options.e2eProjectName, { @@ -146,25 +154,23 @@ export async function e2eProjectGenerator(host: Tree, _options: Schema) { return runTasksInSerial(...tasks); } -function normalizeOptions( +async function normalizeOptions( tree: Tree, options: Schema -): Omit & { e2eProjectRoot: string; e2eProjectName: string } { - const { layoutDirectory, projectDirectory } = extractLayoutDirectory( - options.directory - ); - const appsDir = layoutDirectory ?? getWorkspaceLayout(tree).appsDir; - const name = options.name ?? `${options.project}-e2e`; - - const appDirectory = projectDirectory - ? `${names(projectDirectory).fileName}/${names(name).fileName}` - : names(name).fileName; - - const e2eProjectName = appDirectory.replace(new RegExp('/', 'g'), '-'); - - const e2eProjectRoot = options.rootProject - ? 'e2e' - : joinPathFragments(appsDir, appDirectory); +): Promise< + Omit & { e2eProjectRoot: string; e2eProjectName: string } +> { + const { projectName: e2eProjectName, projectRoot: e2eProjectRoot } = + await determineProjectNameAndRootOptions(tree, { + name: options.name ?? `${options.project}-e2e`, + projectType: 'library', + directory: options.rootProject ? 'e2e' : options.directory, + projectNameAndRootFormat: options.rootProject + ? 'as-provided' + : options.projectNameAndRootFormat, + // this is an internal generator, don't save defaults + callingGenerator: null, + }); return { ...options, diff --git a/packages/node/src/generators/e2e-project/schema.d.ts b/packages/node/src/generators/e2e-project/schema.d.ts index c52d11ecc202e..40c26777c6f33 100644 --- a/packages/node/src/generators/e2e-project/schema.d.ts +++ b/packages/node/src/generators/e2e-project/schema.d.ts @@ -1,7 +1,10 @@ +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; + export interface Schema { project: string; projectType: 'server' | 'cli'; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; name?: string; port?: number; linter?: 'eslint' | 'none'; diff --git a/packages/node/src/generators/e2e-project/schema.json b/packages/node/src/generators/e2e-project/schema.json index 545e481d6f72f..98094697871fd 100644 --- a/packages/node/src/generators/e2e-project/schema.json +++ b/packages/node/src/generators/e2e-project/schema.json @@ -18,6 +18,11 @@ "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"] + }, "name": { "description": "The name of the e2e project. Defaults to the project name with '-e2e' suffix.", "type": "string" diff --git a/packages/node/src/generators/library/library.ts b/packages/node/src/generators/library/library.ts index f63a6e4f055d0..4a56a55202c20 100644 --- a/packages/node/src/generators/library/library.ts +++ b/packages/node/src/generators/library/library.ts @@ -1,10 +1,8 @@ import { convertNxGenerator, - extractLayoutDirectory, formatFiles, generateFiles, GeneratorCallback, - getWorkspaceLayout, joinPathFragments, names, offsetFromRoot, @@ -15,26 +13,31 @@ import { updateProjectConfiguration, updateTsConfigsToJs, } from '@nx/devkit'; -import { Schema } from './schema'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { libraryGenerator as jsLibraryGenerator } from '@nx/js'; - -import { join } from 'path'; -import { addSwcDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; import { addSwcConfig } from '@nx/js/src/utils/swc/add-swc-config'; +import { addSwcDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; +import { join } from 'path'; import { initGenerator } from '../init/init'; -import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import { Schema } from './schema'; export interface NormalizedSchema extends Schema { - name: string; fileName: string; + projectName: string; projectRoot: string; - projectDirectory: string; parsedTags: string[]; compiler: 'swc' | 'tsc'; } export async function libraryGenerator(tree: Tree, schema: Schema) { - const options = normalizeOptions(tree, schema); + return await libraryGeneratorInternal(tree, { + projectNameAndRootFormat: 'derived', + ...schema, + }); +} + +export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { + const options = await normalizeOptions(tree, schema); const tasks: GeneratorCallback[] = [ await initGenerator(tree, { ...options, @@ -75,37 +78,42 @@ export async function libraryGenerator(tree: Tree, schema: Schema) { export default libraryGenerator; export const librarySchematic = convertNxGenerator(libraryGenerator); -function normalizeOptions(tree: Tree, options: Schema): NormalizedSchema { - const { layoutDirectory, projectDirectory } = extractLayoutDirectory( - options.directory - ); - const { npmScope, libsDir: defaultLibsDir } = getWorkspaceLayout(tree); - const libsDir = layoutDirectory ?? defaultLibsDir; - const name = names(options.name).fileName; - const fullProjectDirectory = projectDirectory - ? `${names(projectDirectory).fileName}/${name}` - : name; - - const projectName = fullProjectDirectory.replace(new RegExp('/', 'g'), '-'); +async function normalizeOptions( + tree: Tree, + options: Schema +): Promise { + const { + projectName, + names: projectNames, + projectRoot, + importPath, + projectNameAndRootFormat, + } = await determineProjectNameAndRootOptions(tree, { + name: options.name, + projectType: 'library', + directory: options.directory, + importPath: options.importPath, + projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/node:library', + }); + options.projectNameAndRootFormat = projectNameAndRootFormat; + const fileName = getCaseAwareFileName({ - fileName: options.simpleModuleName ? name : projectName, + fileName: options.simpleModuleName + ? projectNames.projectSimpleName + : projectNames.projectFileName, pascalCaseFiles: options.pascalCaseFiles, }); - const projectRoot = joinPathFragments(libsDir, fullProjectDirectory); const parsedTags = options.tags ? options.tags.split(',').map((s) => s.trim()) : []; - const importPath = - options.importPath || getImportPath(tree, fullProjectDirectory); - return { ...options, fileName, - name: projectName, + projectName, projectRoot, - projectDirectory: fullProjectDirectory, parsedTags, importPath, }; @@ -150,9 +158,7 @@ function updateProject(tree: Tree, options: NormalizedSchema) { return; } - const project = readProjectConfiguration(tree, options.name); - const { libsDir } = getWorkspaceLayout(tree); - + const project = readProjectConfiguration(tree, options.projectName); const rootProject = options.projectRoot === '.' || options.projectRoot === ''; project.targets = project.targets || {}; @@ -162,9 +168,7 @@ function updateProject(tree: Tree, options: NormalizedSchema) { options: { outputPath: joinPathFragments( 'dist', - rootProject - ? options.projectDirectory - : `${libsDir}/${options.projectDirectory}` + rootProject ? options.projectName : options.projectRoot ), tsConfig: `${options.projectRoot}/tsconfig.lib.json`, packageJson: `${options.projectRoot}/package.json`, @@ -182,5 +186,5 @@ function updateProject(tree: Tree, options: NormalizedSchema) { project.targets.build.options.srcRootForCompilationRoot = options.rootDir; } - updateProjectConfiguration(tree, options.name, project); + updateProjectConfiguration(tree, options.projectName, project); } diff --git a/packages/node/src/generators/library/schema.d.ts b/packages/node/src/generators/library/schema.d.ts index 837f21a69a9cd..f277e7d848d58 100644 --- a/packages/node/src/generators/library/schema.d.ts +++ b/packages/node/src/generators/library/schema.d.ts @@ -1,8 +1,10 @@ -import { Linter } from '@nx/linter'; +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { Linter } from '@nx/linter'; export interface Schema { name: string; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; simpleModuleName?: boolean; skipTsConfig?: boolean; skipFormat?: boolean; diff --git a/packages/node/src/generators/library/schema.json b/packages/node/src/generators/library/schema.json index c66e679f4d005..2bf7cb184627e 100644 --- a/packages/node/src/generators/library/schema.json +++ b/packages/node/src/generators/library/schema.json @@ -19,13 +19,19 @@ "$source": "argv", "index": 0 }, - "x-prompt": "What name would you like to use for the library?" + "x-prompt": "What name would you like to use for the library?", + "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 lib is placed", "alias": "dir" }, + "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"] + }, "simpleModuleName": { "description": "Keep the module name simple (when using `--directory`).", "type": "boolean",