feat(nextjs): use helper to determine project name and root in projects generators (#18733)

This commit is contained in:
Leosvel Pérez Espinosa 2023-08-21 14:42:30 +01:00 committed by GitHub
parent 2638bb0db2
commit 18ba93ad5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 153 additions and 64 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "application", "name": "application",
"factory": "./src/generators/application/application#applicationGenerator", "factory": "./src/generators/application/application#applicationGeneratorInternal",
"schema": { "schema": {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"cli": "nx", "cli": "nx",
@ -14,7 +14,7 @@
"type": "string", "type": "string",
"$default": { "$source": "argv", "index": 0 }, "$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the application?", "x-prompt": "What name would you like to use for the application?",
"pattern": "^[a-zA-Z].*$", "pattern": "^[a-zA-Z][^:]*$",
"x-priority": "important" "x-priority": "important"
}, },
"directory": { "directory": {
@ -23,6 +23,11 @@
"alias": "d", "alias": "d",
"x-priority": "important" "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": { "style": {
"description": "The file extension to be used for style files.", "description": "The file extension to be used for style files.",
"type": "string", "type": "string",
@ -138,7 +143,7 @@
"aliases": ["app"], "aliases": ["app"],
"x-type": "application", "x-type": "application",
"description": "Create an 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, "hidden": false,
"path": "/packages/next/src/generators/application/schema.json", "path": "/packages/next/src/generators/application/schema.json",
"type": "generator" "type": "generator"

View File

@ -1,6 +1,6 @@
{ {
"name": "library", "name": "library",
"factory": "./src/generators/library/library#libraryGenerator", "factory": "./src/generators/library/library#libraryGeneratorInternal",
"schema": { "schema": {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"cli": "nx", "cli": "nx",
@ -14,7 +14,7 @@
"description": "Library name", "description": "Library name",
"$default": { "$source": "argv", "index": 0 }, "$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-Z].*$", "pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
"x-priority": "important" "x-priority": "important"
}, },
"directory": { "directory": {
@ -23,6 +23,11 @@
"alias": "dir", "alias": "dir",
"x-priority": "important" "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": { "style": {
"description": "The file extension to be used for style files.", "description": "The file extension to be used for style files.",
"type": "string", "type": "string",
@ -154,7 +159,7 @@
"aliases": ["lib"], "aliases": ["lib"],
"x-type": "library", "x-type": "library",
"description": "Create a 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, "hidden": false,
"path": "/packages/next/src/generators/library/schema.json", "path": "/packages/next/src/generators/library/schema.json",
"type": "generator" "type": "generator"

View File

@ -213,6 +213,47 @@ describe('Next.js Applications', () => {
await killPort(selfContainedPort); await killPort(selfContainedPort);
}, 600_000); }, 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', () => { it('should build and install pruned lock file', () => {
const appName = uniq('app'); const appName = uniq('app');
runCLI(`generate @nx/next:app ${appName} --no-interactive --style=css`); runCLI(`generate @nx/next:app ${appName} --no-interactive --style=css`);

View File

@ -40,7 +40,7 @@
"hidden": true "hidden": true
}, },
"application": { "application": {
"factory": "./src/generators/application/application#applicationGenerator", "factory": "./src/generators/application/application#applicationGeneratorInternal",
"schema": "./src/generators/application/schema.json", "schema": "./src/generators/application/schema.json",
"aliases": ["app"], "aliases": ["app"],
"x-type": "application", "x-type": "application",
@ -57,7 +57,7 @@
"description": "Create a component." "description": "Create a component."
}, },
"library": { "library": {
"factory": "./src/generators/library/library#libraryGenerator", "factory": "./src/generators/library/library#libraryGeneratorInternal",
"schema": "./src/generators/library/schema.json", "schema": "./src/generators/library/schema.json",
"aliases": ["lib"], "aliases": ["lib"],
"x-type": "library", "x-type": "library",

View File

@ -23,8 +23,15 @@ import { updateCypressTsConfig } from './lib/update-cypress-tsconfig';
import { showPossibleWarnings } from './lib/show-possible-warnings'; import { showPossibleWarnings } from './lib/show-possible-warnings';
export async function applicationGenerator(host: Tree, schema: Schema) { 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 tasks: GeneratorCallback[] = [];
const options = normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
showPossibleWarnings(host, options); showPossibleWarnings(host, options);
@ -58,7 +65,7 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
if (options.customServer) { if (options.customServer) {
await customServerGenerator(host, { await customServerGenerator(host, {
project: options.name, project: options.projectName,
compiler: options.swc ? 'swc' : 'tsc', compiler: options.swc ? 'swc' : 'tsc',
}); });
} }

View File

@ -19,7 +19,9 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
...options, ...options,
linter: Linter.EsLint, linter: Linter.EsLint,
name: options.e2eProjectName, 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, project: options.projectName,
skipFormat: true, skipFormat: true,
}); });
@ -43,7 +45,7 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
setParserOptionsProject: options.setParserOptionsProject, setParserOptionsProject: options.setParserOptionsProject,
webServerAddress: 'http://127.0.0.1:4200', webServerAddress: 'http://127.0.0.1:4200',
webServerCommand: `${getPackageManagerCommand().exec} nx serve ${ webServerCommand: `${getPackageManagerCommand().exec} nx serve ${
options.name options.projectName
}`, }`,
}); });
} }

View File

@ -41,7 +41,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
host, host,
options.appProjectRoot options.appProjectRoot
), ),
appContent: createAppJsx(options.name), appContent: createAppJsx(options.projectName),
styleContent: createStyleRules(), styleContent: createStyleRules(),
pageStyleContent: `.page {}`, pageStyleContent: `.page {}`,
@ -133,7 +133,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
...(updatedJson.exclude || []), ...(updatedJson.exclude || []),
...(appJSON.exclude || []), ...(appJSON.exclude || []),
'**e2e/**/*', '**e2e/**/*',
`dist/${options.name}/**/*`, `dist/${options.projectName}/**/*`,
]), ]),
], ],
}; };

View File

@ -1,13 +1,7 @@
import { assertValidStyle } from '@nx/react/src/utils/assertion'; import { joinPathFragments, names, Tree } from '@nx/devkit';
import { import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
extractLayoutDirectory,
getWorkspaceLayout,
joinPathFragments,
names,
Tree,
} from '@nx/devkit';
import { Linter } from '@nx/linter'; import { Linter } from '@nx/linter';
import { assertValidStyle } from '@nx/react/src/utils/assertion';
import { Schema } from '../schema'; import { Schema } from '../schema';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
@ -22,32 +16,42 @@ export interface NormalizedSchema extends Schema {
js?: boolean; js?: boolean;
} }
export function normalizeOptions( export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): NormalizedSchema { ): Promise<NormalizedSchema> {
const { layoutDirectory, projectDirectory } = extractLayoutDirectory( const {
options.directory 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;
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 name = names(options.name).fileName; 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`;
const appProjectRoot = options.rootProject
? '.'
: joinPathFragments(appsDir, appDirectory);
const e2eProjectRoot = options.rootProject
? 'e2e'
: joinPathFragments(appsDir, `${appDirectory}-e2e`);
const outputPath = joinPathFragments( const outputPath = joinPathFragments(
'dist', 'dist',
appProjectRoot, appProjectRoot,

View File

@ -1,11 +1,13 @@
import { Linter } from '@nx/linter'; import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { SupportedStyles } from '@nx/react'; import type { Linter } from '@nx/linter';
import type { SupportedStyles } from '@nx/react';
export interface Schema { export interface Schema {
name: string; name: string;
style?: SupportedStyles; style?: SupportedStyles;
skipFormat?: boolean; skipFormat?: boolean;
directory?: string; directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
tags?: string; tags?: string;
unitTestRunner?: 'jest' | 'none'; unitTestRunner?: 'jest' | 'none';
e2eTestRunner?: 'cypress' | 'playwright' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'none';

View File

@ -14,7 +14,7 @@
"index": 0 "index": 0
}, },
"x-prompt": "What name would you like to use for the application?", "x-prompt": "What name would you like to use for the application?",
"pattern": "^[a-zA-Z].*$", "pattern": "^[a-zA-Z][^:]*$",
"x-priority": "important" "x-priority": "important"
}, },
"directory": { "directory": {
@ -23,6 +23,11 @@
"alias": "d", "alias": "d",
"x-priority": "important" "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": { "style": {
"description": "The file extension to be used for style files.", "description": "The file extension to be used for style files.",
"type": "string", "type": "string",

View File

@ -10,8 +10,8 @@ describe('normalizeOptions', () => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
}); });
it('should set importPath and projectRoot', () => { it('should set importPath and projectRoot', async () => {
const options = normalizeOptions(tree, { const options = await normalizeOptions(tree, {
name: 'my-lib', name: 'my-lib',
style: 'css', style: 'css',
linter: Linter.None, linter: Linter.None,

View File

@ -1,5 +1,5 @@
import { getWorkspaceLayout, joinPathFragments, names, Tree } from '@nx/devkit'; import { Tree } from '@nx/devkit';
import { getImportPath } from '@nx/js/src/utils/get-import-path'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Schema } from '../schema'; import { Schema } from '../schema';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
@ -7,20 +7,24 @@ export interface NormalizedSchema extends Schema {
projectRoot: string; projectRoot: string;
} }
export function normalizeOptions( export async function normalizeOptions(
host: Tree, host: Tree,
options: Schema options: Schema
): NormalizedSchema { ): Promise<NormalizedSchema> {
const name = names(options.name).fileName; const { projectRoot, importPath, projectNameAndRootFormat } =
const projectDirectory = options.directory await determineProjectNameAndRootOptions(host, {
? `${names(options.directory).fileName}/${name}` name: options.name,
: 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 { return {
...options, ...options,
importPath: options.importPath ?? getImportPath(host, projectDirectory), importPath,
projectRoot, projectRoot,
}; };
} }

View File

@ -15,7 +15,14 @@ import { Schema } from './schema';
import { normalizeOptions } from './lib/normalize-options'; import { normalizeOptions } from './lib/normalize-options';
export async function libraryGenerator(host: Tree, rawOptions: Schema) { 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 tasks: GeneratorCallback[] = [];
const initTask = await nextInitGenerator(host, { const initTask = await nextInitGenerator(host, {
...options, ...options,

View File

@ -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'; import type { SupportedStyles } from '@nx/react';
export interface Schema { export interface Schema {
name: string; name: string;
directory?: string; directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
style: SupportedStyles; style: SupportedStyles;
skipTsConfig?: boolean; skipTsConfig?: boolean;
skipFormat?: boolean; skipFormat?: boolean;

View File

@ -14,7 +14,7 @@
"index": 0 "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-Z].*$", "pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
"x-priority": "important" "x-priority": "important"
}, },
"directory": { "directory": {
@ -23,6 +23,11 @@
"alias": "dir", "alias": "dir",
"x-priority": "important" "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": { "style": {
"description": "The file extension to be used for style files.", "description": "The file extension to be used for style files.",
"type": "string", "type": "string",