From 18ba93ad5d6fc2f6b1964fcef79b8bd7287466e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Mon, 21 Aug 2023 14:42:30 +0100 Subject: [PATCH] feat(nextjs): use helper to determine project name and root in projects generators (#18733) --- .../packages/next/generators/application.json | 11 +++- .../packages/next/generators/library.json | 11 +++- e2e/next/src/next.test.ts | 41 ++++++++++++ packages/next/generators.json | 4 +- .../src/generators/application/application.ts | 11 +++- .../src/generators/application/lib/add-e2e.ts | 6 +- .../lib/create-application-files.ts | 4 +- .../application/lib/normalize-options.ts | 64 ++++++++++--------- .../src/generators/application/schema.d.ts | 6 +- .../src/generators/application/schema.json | 7 +- .../library/lib/normalize-options.spec.ts | 4 +- .../library/lib/normalize-options.ts | 26 ++++---- .../next/src/generators/library/library.ts | 9 ++- .../next/src/generators/library/schema.d.ts | 4 +- .../next/src/generators/library/schema.json | 7 +- 15 files changed, 152 insertions(+), 63 deletions(-) diff --git a/docs/generated/packages/next/generators/application.json b/docs/generated/packages/next/generators/application.json index 594c4d7df9eb6..9df9b163b63d8 100644 --- a/docs/generated/packages/next/generators/application.json +++ b/docs/generated/packages/next/generators/application.json @@ -1,6 +1,6 @@ { "name": "application", - "factory": "./src/generators/application/application#applicationGenerator", + "factory": "./src/generators/application/application#applicationGeneratorInternal", "schema": { "$schema": "http://json-schema.org/schema", "cli": "nx", @@ -14,7 +14,7 @@ "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][^:]*$", "x-priority": "important" }, "directory": { @@ -23,6 +23,11 @@ "alias": "d", "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", @@ -138,7 +143,7 @@ "aliases": ["app"], "x-type": "application", "description": "Create an application.", - "implementation": "/packages/next/src/generators/application/application#applicationGenerator.ts", + "implementation": "/packages/next/src/generators/application/application#applicationGeneratorInternal.ts", "hidden": false, "path": "/packages/next/src/generators/application/schema.json", "type": "generator" diff --git a/docs/generated/packages/next/generators/library.json b/docs/generated/packages/next/generators/library.json index 58758ef113373..a51864e25701f 100644 --- a/docs/generated/packages/next/generators/library.json +++ b/docs/generated/packages/next/generators/library.json @@ -1,6 +1,6 @@ { "name": "library", - "factory": "./src/generators/library/library#libraryGenerator", + "factory": "./src/generators/library/library#libraryGeneratorInternal", "schema": { "$schema": "http://json-schema.org/schema", "cli": "nx", @@ -14,7 +14,7 @@ "description": "Library name", "$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][^:]*)$", "x-priority": "important" }, "directory": { @@ -23,6 +23,11 @@ "alias": "dir", "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", @@ -154,7 +159,7 @@ "aliases": ["lib"], "x-type": "library", "description": "Create a library.", - "implementation": "/packages/next/src/generators/library/library#libraryGenerator.ts", + "implementation": "/packages/next/src/generators/library/library#libraryGeneratorInternal.ts", "hidden": false, "path": "/packages/next/src/generators/library/schema.json", "type": "generator" diff --git a/e2e/next/src/next.test.ts b/e2e/next/src/next.test.ts index 2234e54dd5390..710054d1875b1 100644 --- a/e2e/next/src/next.test.ts +++ b/e2e/next/src/next.test.ts @@ -213,6 +213,47 @@ describe('Next.js Applications', () => { await killPort(selfContainedPort); }, 600_000); + 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/next: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}/app/page.tsx`); + // 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/next:lib ${libName} --buildable --project-name-and-root-format=derived --no-interactive` + ) + ).toThrow(); + + runCLI( + `generate @nx/next: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}` + ); + }, 600_000); + it('should build and install pruned lock file', () => { const appName = uniq('app'); runCLI(`generate @nx/next:app ${appName} --no-interactive --style=css`); diff --git a/packages/next/generators.json b/packages/next/generators.json index 4c3c342badf8b..8ad7a43caad3b 100644 --- a/packages/next/generators.json +++ b/packages/next/generators.json @@ -40,7 +40,7 @@ "hidden": true }, "application": { - "factory": "./src/generators/application/application#applicationGenerator", + "factory": "./src/generators/application/application#applicationGeneratorInternal", "schema": "./src/generators/application/schema.json", "aliases": ["app"], "x-type": "application", @@ -57,7 +57,7 @@ "description": "Create a component." }, "library": { - "factory": "./src/generators/library/library#libraryGenerator", + "factory": "./src/generators/library/library#libraryGeneratorInternal", "schema": "./src/generators/library/schema.json", "aliases": ["lib"], "x-type": "library", diff --git a/packages/next/src/generators/application/application.ts b/packages/next/src/generators/application/application.ts index 0a1c99f07663b..8b67f6d27fba5 100644 --- a/packages/next/src/generators/application/application.ts +++ b/packages/next/src/generators/application/application.ts @@ -23,8 +23,15 @@ import { updateCypressTsConfig } from './lib/update-cypress-tsconfig'; import { showPossibleWarnings } from './lib/show-possible-warnings'; export async function applicationGenerator(host: Tree, schema: Schema) { + return await applicationGeneratorInternal(host, { + projectNameAndRootFormat: 'derived', + ...schema, + }); +} + +export async function applicationGeneratorInternal(host: Tree, schema: Schema) { const tasks: GeneratorCallback[] = []; - const options = normalizeOptions(host, schema); + const options = await normalizeOptions(host, schema); showPossibleWarnings(host, options); @@ -58,7 +65,7 @@ export async function applicationGenerator(host: Tree, schema: Schema) { if (options.customServer) { await customServerGenerator(host, { - project: options.name, + project: options.projectName, compiler: options.swc ? 'swc' : 'tsc', }); } diff --git a/packages/next/src/generators/application/lib/add-e2e.ts b/packages/next/src/generators/application/lib/add-e2e.ts index 47a55da28f5cd..0f3f1857f70d9 100644 --- a/packages/next/src/generators/application/lib/add-e2e.ts +++ b/packages/next/src/generators/application/lib/add-e2e.ts @@ -19,7 +19,9 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { ...options, linter: Linter.EsLint, 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.projectName, skipFormat: true, }); @@ -43,7 +45,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) { setParserOptionsProject: options.setParserOptionsProject, webServerAddress: 'http://127.0.0.1:4200', webServerCommand: `${getPackageManagerCommand().exec} nx serve ${ - options.name + options.projectName }`, }); } diff --git a/packages/next/src/generators/application/lib/create-application-files.ts b/packages/next/src/generators/application/lib/create-application-files.ts index 5068cc2e9fc18..9d5e5993ab037 100644 --- a/packages/next/src/generators/application/lib/create-application-files.ts +++ b/packages/next/src/generators/application/lib/create-application-files.ts @@ -41,7 +41,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { host, options.appProjectRoot ), - appContent: createAppJsx(options.name), + appContent: createAppJsx(options.projectName), styleContent: createStyleRules(), pageStyleContent: `.page {}`, @@ -133,7 +133,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { ...(updatedJson.exclude || []), ...(appJSON.exclude || []), '**e2e/**/*', - `dist/${options.name}/**/*`, + `dist/${options.projectName}/**/*`, ]), ], }; diff --git a/packages/next/src/generators/application/lib/normalize-options.ts b/packages/next/src/generators/application/lib/normalize-options.ts index d6fbe2b4c76e2..b031d02f2babe 100644 --- a/packages/next/src/generators/application/lib/normalize-options.ts +++ b/packages/next/src/generators/application/lib/normalize-options.ts @@ -1,13 +1,7 @@ -import { assertValidStyle } from '@nx/react/src/utils/assertion'; -import { - extractLayoutDirectory, - getWorkspaceLayout, - joinPathFragments, - names, - Tree, -} from '@nx/devkit'; +import { joinPathFragments, names, Tree } from '@nx/devkit'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Linter } from '@nx/linter'; - +import { assertValidStyle } from '@nx/react/src/utils/assertion'; import { Schema } from '../schema'; export interface NormalizedSchema extends Schema { @@ -22,31 +16,41 @@ export interface NormalizedSchema extends Schema { js?: boolean; } -export function normalizeOptions( +export async function normalizeOptions( host: Tree, options: Schema -): NormalizedSchema { - const { layoutDirectory, projectDirectory } = extractLayoutDirectory( - options.directory - ); - const name = names(options.name).fileName; - - const appDirectory = projectDirectory - ? `${names(projectDirectory).fileName}/${names(options.name).fileName}` - : names(options.name).fileName; - - const appsDir = layoutDirectory ?? getWorkspaceLayout(host).appsDir; - - const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-'); - const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-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, + callingGenerator: '@nx/next:application', + }); + options.rootProject = appProjectRoot === '.'; + options.projectNameAndRootFormat = projectNameAndRootFormat; - const appProjectRoot = options.rootProject - ? '.' - : joinPathFragments(appsDir, appDirectory); + 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, + callingGenerator: '@nx/next:application', + }); + e2eProjectName = projectNameAndRoot.projectName; + e2eProjectRoot = projectNameAndRoot.projectRoot; + } - const e2eProjectRoot = options.rootProject - ? 'e2e' - : joinPathFragments(appsDir, `${appDirectory}-e2e`); + const name = names(options.name).fileName; const outputPath = joinPathFragments( 'dist', diff --git a/packages/next/src/generators/application/schema.d.ts b/packages/next/src/generators/application/schema.d.ts index f45cb9e1554b7..08d8283ca904d 100644 --- a/packages/next/src/generators/application/schema.d.ts +++ b/packages/next/src/generators/application/schema.d.ts @@ -1,11 +1,13 @@ -import { Linter } from '@nx/linter'; -import { SupportedStyles } from '@nx/react'; +import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { Linter } from '@nx/linter'; +import type { SupportedStyles } from '@nx/react'; export interface Schema { name: string; style?: SupportedStyles; skipFormat?: boolean; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; tags?: string; unitTestRunner?: 'jest' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'none'; diff --git a/packages/next/src/generators/application/schema.json b/packages/next/src/generators/application/schema.json index fc9ddf32a0170..20ab3307e3058 100644 --- a/packages/next/src/generators/application/schema.json +++ b/packages/next/src/generators/application/schema.json @@ -14,7 +14,7 @@ "index": 0 }, "x-prompt": "What name would you like to use for the application?", - "pattern": "^[a-zA-Z].*$", + "pattern": "^[a-zA-Z][^:]*$", "x-priority": "important" }, "directory": { @@ -23,6 +23,11 @@ "alias": "d", "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/next/src/generators/library/lib/normalize-options.spec.ts b/packages/next/src/generators/library/lib/normalize-options.spec.ts index d7c6bbdb37a3c..b15bba233259b 100644 --- a/packages/next/src/generators/library/lib/normalize-options.spec.ts +++ b/packages/next/src/generators/library/lib/normalize-options.spec.ts @@ -10,8 +10,8 @@ describe('normalizeOptions', () => { tree = createTreeWithEmptyWorkspace(); }); - it('should set importPath and projectRoot', () => { - const options = normalizeOptions(tree, { + it('should set importPath and projectRoot', async () => { + const options = await normalizeOptions(tree, { name: 'my-lib', style: 'css', linter: Linter.None, diff --git a/packages/next/src/generators/library/lib/normalize-options.ts b/packages/next/src/generators/library/lib/normalize-options.ts index 436235b490669..e3688017b63bc 100644 --- a/packages/next/src/generators/library/lib/normalize-options.ts +++ b/packages/next/src/generators/library/lib/normalize-options.ts @@ -1,5 +1,5 @@ -import { getWorkspaceLayout, joinPathFragments, names, Tree } from '@nx/devkit'; -import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import { Tree } from '@nx/devkit'; +import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Schema } from '../schema'; export interface NormalizedSchema extends Schema { @@ -7,20 +7,24 @@ export interface NormalizedSchema extends Schema { projectRoot: string; } -export function normalizeOptions( +export async function normalizeOptions( host: Tree, options: Schema -): NormalizedSchema { - const name = names(options.name).fileName; - const projectDirectory = options.directory - ? `${names(options.directory).fileName}/${name}` - : name; +): Promise { + const { projectRoot, importPath, projectNameAndRootFormat } = + await determineProjectNameAndRootOptions(host, { + name: options.name, + projectType: 'library', + directory: options.directory, + importPath: options.importPath, + projectNameAndRootFormat: options.projectNameAndRootFormat, + callingGenerator: '@nx/next:library', + }); + options.projectNameAndRootFormat = projectNameAndRootFormat; - const { libsDir } = getWorkspaceLayout(host); - const projectRoot = joinPathFragments(libsDir, projectDirectory); return { ...options, - importPath: options.importPath ?? getImportPath(host, projectDirectory), + importPath, projectRoot, }; } diff --git a/packages/next/src/generators/library/library.ts b/packages/next/src/generators/library/library.ts index 7079679932250..812393fa736ac 100644 --- a/packages/next/src/generators/library/library.ts +++ b/packages/next/src/generators/library/library.ts @@ -15,7 +15,14 @@ import { Schema } from './schema'; import { normalizeOptions } from './lib/normalize-options'; export async function libraryGenerator(host: Tree, rawOptions: Schema) { - const options = normalizeOptions(host, rawOptions); + return await libraryGeneratorInternal(host, { + projectNameAndRootFormat: 'derived', + ...rawOptions, + }); +} + +export async function libraryGeneratorInternal(host: Tree, rawOptions: Schema) { + const options = await normalizeOptions(host, rawOptions); const tasks: GeneratorCallback[] = []; const initTask = await nextInitGenerator(host, { ...options, diff --git a/packages/next/src/generators/library/schema.d.ts b/packages/next/src/generators/library/schema.d.ts index cb3cf33c46e7c..ca0d398c6a587 100644 --- a/packages/next/src/generators/library/schema.d.ts +++ b/packages/next/src/generators/library/schema.d.ts @@ -1,9 +1,11 @@ -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 { SupportedStyles } from '@nx/react'; export interface Schema { name: string; directory?: string; + projectNameAndRootFormat?: ProjectNameAndRootFormat; style: SupportedStyles; skipTsConfig?: boolean; skipFormat?: boolean; diff --git a/packages/next/src/generators/library/schema.json b/packages/next/src/generators/library/schema.json index 95d7ed3d3b66c..4bfd9a32597f1 100644 --- a/packages/next/src/generators/library/schema.json +++ b/packages/next/src/generators/library/schema.json @@ -14,7 +14,7 @@ "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][^:]*)$", "x-priority": "important" }, "directory": { @@ -23,6 +23,11 @@ "alias": "dir", "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",