feat(react): use helper to determine project name and root directory in project generators (#18615)

This commit is contained in:
Leosvel Pérez Espinosa 2023-08-18 18:12:23 +01:00 committed by GitHub
parent 71d2994be9
commit eb9caa1ade
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 346 additions and 117 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",
@ -28,7 +28,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][^:]*$"
}, },
"directory": { "directory": {
"description": "The directory of the new application.", "description": "The directory of the new application.",
@ -36,6 +36,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",
@ -192,7 +197,7 @@
"aliases": ["app"], "aliases": ["app"],
"x-type": "application", "x-type": "application",
"description": "Create a React application.", "description": "Create a React application.",
"implementation": "/packages/react/src/generators/application/application#applicationGenerator.ts", "implementation": "/packages/react/src/generators/application/application#applicationGeneratorInternal.ts",
"hidden": false, "hidden": false,
"path": "/packages/react/src/generators/application/schema.json", "path": "/packages/react/src/generators/application/schema.json",
"type": "generator" "type": "generator"

View File

@ -1,6 +1,6 @@
{ {
"name": "host", "name": "host",
"factory": "./src/generators/host/host#hostGenerator", "factory": "./src/generators/host/host#hostGeneratorInternal",
"schema": { "schema": {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"$id": "GeneratorReactHost", "$id": "GeneratorReactHost",
@ -14,7 +14,7 @@
"description": "The name of the host application to generate the Module Federation configuration", "description": "The name of the host application to generate the Module Federation configuration",
"$default": { "$source": "argv", "index": 0 }, "$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use as the host application?", "x-prompt": "What name would you like to use as the host application?",
"pattern": "^[a-zA-Z].*$", "pattern": "^[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",
@ -163,7 +168,7 @@
}, },
"x-type": "application", "x-type": "application",
"description": "Generate a host react application", "description": "Generate a host react application",
"implementation": "/packages/react/src/generators/host/host#hostGenerator.ts", "implementation": "/packages/react/src/generators/host/host#hostGeneratorInternal.ts",
"aliases": [], "aliases": [],
"hidden": false, "hidden": false,
"path": "/packages/react/src/generators/host/schema.json", "path": "/packages/react/src/generators/host/schema.json",

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",
@ -24,7 +24,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": {
@ -33,6 +33,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",
@ -196,7 +201,7 @@
"aliases": ["lib"], "aliases": ["lib"],
"x-type": "library", "x-type": "library",
"description": "Create a React library.", "description": "Create a React library.",
"implementation": "/packages/react/src/generators/library/library#libraryGenerator.ts", "implementation": "/packages/react/src/generators/library/library#libraryGeneratorInternal.ts",
"hidden": false, "hidden": false,
"path": "/packages/react/src/generators/library/schema.json", "path": "/packages/react/src/generators/library/schema.json",
"type": "generator" "type": "generator"

View File

@ -1,6 +1,6 @@
{ {
"name": "remote", "name": "remote",
"factory": "./src/generators/remote/remote#remoteGenerator", "factory": "./src/generators/remote/remote#remoteGeneratorInternal",
"schema": { "schema": {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"$id": "GeneratorReactRemote", "$id": "GeneratorReactRemote",
@ -14,7 +14,7 @@
"description": "The name of the remote application to generate the Module Federation configuration", "description": "The name of the remote application to generate the Module Federation configuration",
"$default": { "$source": "argv", "index": 0 }, "$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use as the remote application?", "x-prompt": "What name would you like to use as the remote application?",
"pattern": "^[a-zA-Z].*$", "pattern": "^[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",
@ -162,7 +167,7 @@
}, },
"x-type": "application", "x-type": "application",
"description": "Generate a remote react application", "description": "Generate a remote react application",
"implementation": "/packages/react/src/generators/remote/remote#remoteGenerator.ts", "implementation": "/packages/react/src/generators/remote/remote#remoteGeneratorInternal.ts",
"aliases": [], "aliases": [],
"hidden": false, "hidden": false,
"path": "/packages/react/src/generators/remote/schema.json", "path": "/packages/react/src/generators/remote/schema.json",

View File

@ -2,7 +2,6 @@ import { stripIndents } from '@nx/devkit';
import { import {
checkFilesExist, checkFilesExist,
cleanupProject, cleanupProject,
killPort,
newProject, newProject,
readProjectConfig, readProjectConfig,
runCLI, runCLI,
@ -114,12 +113,29 @@ describe('React Module Federation', () => {
// } // }
}, 500_000); }, 500_000);
it('should should support generating host and remote apps with the new name and root format', async () => {
const shell = uniq('shell');
const remote = uniq('remote');
runCLI(
`generate @nx/react:host ${shell} --project-name-and-root-format=as-provided --no-interactive`
);
runCLI(
`generate @nx/react:remote ${remote} --host=${shell} --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(`${shell}/module-federation.config.js`);
checkFilesExist(`${remote}/module-federation.config.js`);
// check default generated host is built successfully
const buildOutput = runCLI(`run ${shell}:build:development`);
expect(buildOutput).toContain('Successfully ran target build');
}, 500_000);
async function readPort(appName: string): Promise<number> { async function readPort(appName: string): Promise<number> {
const config = await readProjectConfig(appName); const config = await readProjectConfig(appName);
return config.targets.serve.options.port; return config.targets.serve.options.port;
} }
}); });
function killPorts(ports: number[]): Promise<boolean[]> {
return Promise.all(ports.map((p) => killPort(p)));
}

View File

@ -227,6 +227,52 @@ describe('React Applications', () => {
); );
}, 250_000); }, 250_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/react:app ${appName} --bundler=webpack --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.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/react:lib ${libName} --unit-test-runner=jest --buildable --project-name-and-root-format=derived --no-interactive`
)
).toThrow();
runCLI(
`generate @nx/react:lib ${libName} --unit-test-runner=jest --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('React Applications: --style option', () => { describe('React Applications: --style option', () => {
it.each` it.each`
style style

View File

@ -56,6 +56,27 @@ describe('determineProjectNameAndRootOptions', () => {
}); });
}); });
it(`should handle window's style paths correctly when format is "as-provided"`, async () => {
const result = await determineProjectNameAndRootOptions(tree, {
name: 'libName',
directory: 'shared\\libName',
projectType: 'library',
projectNameAndRootFormat: 'as-provided',
callingGenerator: '',
});
expect(result).toStrictEqual({
projectName: 'lib-name',
names: {
projectSimpleName: 'lib-name',
projectFileName: 'lib-name',
},
importPath: '@proj/lib-name',
projectRoot: 'shared/lib-name',
projectNameAndRootFormat: 'as-provided',
});
});
it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => { it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => {
const result = await determineProjectNameAndRootOptions(tree, { const result = await determineProjectNameAndRootOptions(tree, {
name: '@scope/libName', name: '@scope/libName',
@ -253,6 +274,27 @@ describe('determineProjectNameAndRootOptions', () => {
expect(result.importPath).toBe('@proj/lib-name'); expect(result.importPath).toBe('@proj/lib-name');
}); });
it(`should handle window's style paths correctly when format is "derived"`, async () => {
const result = await determineProjectNameAndRootOptions(tree, {
name: 'libName',
directory: 'shared\\sub-dir',
projectType: 'library',
projectNameAndRootFormat: 'derived',
callingGenerator: '',
});
expect(result).toStrictEqual({
projectName: 'shared-sub-dir-lib-name',
names: {
projectSimpleName: 'lib-name',
projectFileName: 'shared-sub-dir-lib-name',
},
importPath: '@proj/shared/sub-dir/lib-name',
projectRoot: 'shared/sub-dir/lib-name',
projectNameAndRootFormat: 'derived',
});
});
it('should prompt for the project name and root format', async () => { it('should prompt for the project name and root format', async () => {
// simulate interactive mode // simulate interactive mode
ensureInteractiveMode(); ensureInteractiveMode();
@ -370,6 +412,27 @@ describe('determineProjectNameAndRootOptions', () => {
}); });
}); });
it(`should handle window's style paths correctly when format is "as-provided"`, async () => {
const result = await determineProjectNameAndRootOptions(tree, {
name: 'libName',
directory: 'shared\\libName',
projectType: 'library',
projectNameAndRootFormat: 'as-provided',
callingGenerator: '',
});
expect(result).toStrictEqual({
projectName: 'lib-name',
names: {
projectSimpleName: 'lib-name',
projectFileName: 'lib-name',
},
importPath: '@proj/lib-name',
projectRoot: 'shared/lib-name',
projectNameAndRootFormat: 'as-provided',
});
});
it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => { it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => {
const result = await determineProjectNameAndRootOptions(tree, { const result = await determineProjectNameAndRootOptions(tree, {
name: '@scope/libName', name: '@scope/libName',
@ -514,6 +577,27 @@ describe('determineProjectNameAndRootOptions', () => {
}); });
}); });
it(`should handle window's style paths correctly when format is "derived"`, async () => {
const result = await determineProjectNameAndRootOptions(tree, {
name: 'libName',
directory: 'shared\\sub-dir',
projectType: 'library',
projectNameAndRootFormat: 'derived',
callingGenerator: '',
});
expect(result).toStrictEqual({
projectName: 'shared-sub-dir-lib-name',
names: {
projectSimpleName: 'lib-name',
projectFileName: 'shared-sub-dir-lib-name',
},
importPath: '@proj/shared/sub-dir/lib-name',
projectRoot: 'libs/shared/sub-dir/lib-name',
projectNameAndRootFormat: 'derived',
});
});
it('should throw when using a scoped package name as the project name and format is derived', async () => { it('should throw when using a scoped package name as the project name and format is derived', async () => {
await expect( await expect(
determineProjectNameAndRootOptions(tree, { determineProjectNameAndRootOptions(tree, {

View File

@ -8,7 +8,8 @@ import {
} from '../utils/get-workspace-layout'; } from '../utils/get-workspace-layout';
import { names } from '../utils/names'; import { names } from '../utils/names';
const { joinPathFragments, readJson, readNxJson, updateNxJson } = requireNx(); const { joinPathFragments, normalizePath, readJson, readNxJson, updateNxJson } =
requireNx();
export type ProjectNameAndRootFormat = 'as-provided' | 'derived'; export type ProjectNameAndRootFormat = 'as-provided' | 'derived';
export type ProjectGenerationOptions = { export type ProjectGenerationOptions = {
@ -169,7 +170,9 @@ function getProjectNameAndRootFormats(
options: ProjectGenerationOptions options: ProjectGenerationOptions
): ProjectNameAndRootFormats { ): ProjectNameAndRootFormats {
const name = names(options.name).fileName; const name = names(options.name).fileName;
const directory = options.directory?.replace(/^\.?\//, ''); const directory = options.directory
? normalizePath(options.directory.replace(/^\.?\//, ''))
: undefined;
const asProvidedProjectName = name; const asProvidedProjectName = name;
const asProvidedProjectDirectory = directory const asProvidedProjectDirectory = directory

View File

@ -114,7 +114,7 @@
}, },
"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",
@ -122,7 +122,7 @@
}, },
"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",
@ -179,14 +179,14 @@
}, },
"host": { "host": {
"factory": "./src/generators/host/host#hostGenerator", "factory": "./src/generators/host/host#hostGeneratorInternal",
"schema": "./src/generators/host/schema.json", "schema": "./src/generators/host/schema.json",
"x-type": "application", "x-type": "application",
"description": "Generate a host react application" "description": "Generate a host react application"
}, },
"remote": { "remote": {
"factory": "./src/generators/remote/remote#remoteGenerator", "factory": "./src/generators/remote/remote#remoteGeneratorInternal",
"schema": "./src/generators/remote/schema.json", "schema": "./src/generators/remote/schema.json",
"x-type": "application", "x-type": "application",
"description": "Generate a remote react application" "description": "Generate a remote react application"

View File

@ -284,7 +284,8 @@ describe('app', () => {
appTree.exists('apps/my-dir/my-app-e2e/src/example.spec.ts') appTree.exists('apps/my-dir/my-app-e2e/src/example.spec.ts')
).toBeTruthy(); ).toBeTruthy();
expect( expect(
readProjectConfiguration(appTree, 'my-app-e2e')?.targets?.e2e?.executor readProjectConfiguration(appTree, 'my-dir-my-app-e2e')?.targets?.e2e
?.executor
).toEqual('@nx/playwright:playwright'); ).toEqual('@nx/playwright:playwright');
}); });
}); });

View File

@ -85,10 +85,20 @@ async function addLinting(host: Tree, options: NormalizedSchema) {
export async function applicationGenerator( export async function applicationGenerator(
host: Tree, host: Tree,
schema: Schema schema: Schema
): Promise<GeneratorCallback> {
return await applicationGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function applicationGeneratorInternal(
host: Tree,
schema: Schema
): Promise<GeneratorCallback> { ): Promise<GeneratorCallback> {
const tasks = []; const tasks = [];
const options = normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
showPossibleWarnings(host, options); showPossibleWarnings(host, options);
const initTask = await reactInitGenerator(host, { const initTask = await reactInitGenerator(host, {

View File

@ -29,7 +29,9 @@ export async function addE2e(
return await cypressProjectGenerator(tree, { return await cypressProjectGenerator(tree, {
...options, ...options,
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,
bundler: options.bundler === 'rspack' ? 'webpack' : options.bundler, bundler: options.bundler === 'rspack' ? 'webpack' : options.bundler,
skipFormat: true, skipFormat: true,

View File

@ -1,13 +1,7 @@
import { NormalizedSchema, Schema } from '../schema'; import { Tree, extractLayoutDirectory, names } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { assertValidStyle } from '../../../utils/assertion'; import { assertValidStyle } from '../../../utils/assertion';
import { import { NormalizedSchema, Schema } from '../schema';
extractLayoutDirectory,
getWorkspaceLayout,
joinPathFragments,
names,
normalizePath,
Tree,
} from '@nx/devkit';
import { findFreePort } from './find-free-port'; import { findFreePort } from './find-free-port';
export function normalizeDirectory(options: Schema) { export function normalizeDirectory(options: Schema) {
@ -22,24 +16,40 @@ export function normalizeProjectName(options: Schema) {
return normalizeDirectory(options).replace(new RegExp('/', 'g'), '-'); return normalizeDirectory(options).replace(new RegExp('/', 'g'), '-');
} }
export function normalizeOptions<T extends Schema = Schema>( export async function normalizeOptions<T extends Schema = Schema>(
host: Tree, host: Tree,
options: Schema options: Schema,
): NormalizedSchema<T> { callingGenerator = '@nx/react:application'
const appDirectory = normalizeDirectory(options); ): Promise<NormalizedSchema<T>> {
const appProjectName = normalizeProjectName(options); const {
const e2eProjectName = options.rootProject projectName: appProjectName,
? 'e2e' projectRoot: appProjectRoot,
: `${names(options.name).fileName}-e2e`; projectNameAndRootFormat,
} = await determineProjectNameAndRootOptions(host, {
name: options.name,
projectType: 'application',
directory: options.directory,
projectNameAndRootFormat: options.projectNameAndRootFormat,
rootProject: options.rootProject,
callingGenerator,
});
options.rootProject = appProjectRoot === '.';
options.projectNameAndRootFormat = projectNameAndRootFormat;
const { layoutDirectory } = extractLayoutDirectory(options.directory); let e2eProjectName = 'e2e';
const appsDir = layoutDirectory ?? getWorkspaceLayout(host).appsDir; let e2eProjectRoot = 'e2e';
const appProjectRoot = options.rootProject if (!options.rootProject) {
? '.' const projectNameAndRoot = await determineProjectNameAndRootOptions(host, {
: normalizePath(`${appsDir}/${appDirectory}`); name: `${options.name}-e2e`,
const e2eProjectRoot = options.rootProject projectType: 'application',
? 'e2e' directory: options.directory,
: joinPathFragments(appsDir, `${appDirectory}-e2e`); projectNameAndRootFormat: options.projectNameAndRootFormat,
rootProject: options.rootProject,
callingGenerator,
});
e2eProjectName = projectNameAndRoot.projectName;
e2eProjectRoot = projectNameAndRoot.projectRoot;
}
const parsedTags = options.tags const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())

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 '../../../typings/style'; import type { Linter } from '@nx/linter';
import type { SupportedStyles } from '../../../typings/style';
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' | 'vitest' | 'none'; unitTestRunner?: 'jest' | 'vitest' | 'none';
inSourceTests?: boolean; inSourceTests?: boolean;

View File

@ -28,7 +28,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][^:]*$"
}, },
"directory": { "directory": {
"description": "The directory of the new application.", "description": "The directory of the new application.",
@ -36,6 +36,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",

View File

@ -21,8 +21,19 @@ import setupSsrGenerator from '../setup-ssr/setup-ssr';
import { setupSsrForHost } from './lib/setup-ssr-for-host'; import { setupSsrForHost } from './lib/setup-ssr-for-host';
export async function hostGenerator(host: Tree, schema: Schema) { export async function hostGenerator(host: Tree, schema: Schema) {
return hostGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function hostGeneratorInternal(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options = normalizeOptions<Schema>(host, schema); const options = await normalizeOptions<Schema>(
host,
schema,
'@nx/react:host'
);
const initTask = await applicationGenerator(host, { const initTask = await applicationGenerator(host, {
...options, ...options,

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 '../../../typings'; import type { Linter } from '@nx/linter';
import type { SupportedStyles } from '../../../typings';
export interface Schema { export interface Schema {
classComponent?: boolean; classComponent?: boolean;
compiler?: 'babel' | 'swc'; compiler?: 'babel' | 'swc';
devServerPort?: number; devServerPort?: number;
directory?: string; directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
e2eTestRunner: 'cypress' | 'none'; e2eTestRunner: 'cypress' | 'none';
globalCss?: boolean; globalCss?: boolean;
js?: boolean; js?: boolean;

View File

@ -14,7 +14,7 @@
"index": 0 "index": 0
}, },
"x-prompt": "What name would you like to use as the host application?", "x-prompt": "What name would you like to use as the host application?",
"pattern": "^[a-zA-Z].*$", "pattern": "^[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",

View File

@ -2,7 +2,6 @@ import { Tree } from 'nx/src/generators/tree';
import { import {
addDependenciesToPackageJson, addDependenciesToPackageJson,
ensurePackage, ensurePackage,
getWorkspaceLayout,
joinPathFragments, joinPathFragments,
readProjectConfiguration, readProjectConfiguration,
updateProjectConfiguration, updateProjectConfiguration,
@ -37,7 +36,6 @@ export async function addRollupBuildTarget(
const { targets } = readProjectConfiguration(host, options.name); const { targets } = readProjectConfiguration(host, options.name);
const { libsDir } = getWorkspaceLayout(host);
const external: string[] = ['react', 'react-dom']; const external: string[] = ['react', 'react-dom'];
if (options.style === '@emotion/styled') { if (options.style === '@emotion/styled') {
@ -50,10 +48,7 @@ export async function addRollupBuildTarget(
executor: '@nx/rollup:rollup', executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'], outputs: ['{options.outputPath}'],
options: { options: {
outputPath: outputPath: joinPathFragments('dist', options.projectRoot),
libsDir !== '.'
? `dist/${libsDir}/${options.projectDirectory}`
: `dist/${options.projectDirectory}`,
tsConfig: `${options.projectRoot}/tsconfig.lib.json`, tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
project: `${options.projectRoot}/package.json`, project: `${options.projectRoot}/package.json`,
entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`), entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`),

View File

@ -11,7 +11,7 @@ describe('normalizeOptions', () => {
}); });
it('should set unitTestRunner=jest and bundler=none by default', async () => { it('should set unitTestRunner=jest and bundler=none by default', async () => {
const options = normalizeOptions(tree, { const options = await normalizeOptions(tree, {
name: 'test', name: 'test',
style: 'css', style: 'css',
linter: Linter.None, linter: Linter.None,
@ -27,7 +27,7 @@ describe('normalizeOptions', () => {
}); });
it('should set buildable to true when bundler is not "none"', async () => { it('should set buildable to true when bundler is not "none"', async () => {
let options = normalizeOptions(tree, { let options = await normalizeOptions(tree, {
name: 'test', name: 'test',
style: 'css', style: 'css',
linter: Linter.None, linter: Linter.None,
@ -39,7 +39,7 @@ describe('normalizeOptions', () => {
bundler: 'rollup', bundler: 'rollup',
}); });
options = normalizeOptions(tree, { options = await normalizeOptions(tree, {
name: 'test', name: 'test',
style: 'css', style: 'css',
linter: Linter.None, linter: Linter.None,
@ -53,7 +53,7 @@ describe('normalizeOptions', () => {
}); });
it('should set unitTestRunner=vitest by default when bundler is vite', async () => { it('should set unitTestRunner=vitest by default when bundler is vite', async () => {
const options = normalizeOptions(tree, { const options = await normalizeOptions(tree, {
name: 'test', name: 'test',
style: 'css', style: 'css',
linter: Linter.None, linter: Linter.None,
@ -70,7 +70,7 @@ describe('normalizeOptions', () => {
}); });
it('should set maintain unitTestRunner when bundler is vite', async () => { it('should set maintain unitTestRunner when bundler is vite', async () => {
const options = normalizeOptions(tree, { const options = await normalizeOptions(tree, {
name: 'test', name: 'test',
style: 'css', style: 'css',
linter: Linter.None, linter: Linter.None,
@ -86,8 +86,8 @@ describe('normalizeOptions', () => {
}); });
}); });
it('should set bundler to rollup if buildable is true not no bundler is passed', () => { it('should set bundler to rollup if buildable is true not no bundler is passed', async () => {
const options = normalizeOptions(tree, { const options = await normalizeOptions(tree, {
name: 'test', name: 'test',
style: 'css', style: 'css',
linter: Linter.None, linter: Linter.None,
@ -102,8 +102,8 @@ describe('normalizeOptions', () => {
}); });
}); });
it('should set bundler to rollup if buildable is true and bundler is none ', () => { it('should set bundler to rollup if buildable is true and bundler is none ', async () => {
const options = normalizeOptions(tree, { const options = await normalizeOptions(tree, {
name: 'test', name: 'test',
style: 'css', style: 'css',
linter: Linter.None, linter: Linter.None,

View File

@ -1,43 +1,34 @@
import { import { getProjects, logger, normalizePath, Tree } from '@nx/devkit';
extractLayoutDirectory, import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
getProjects,
getWorkspaceLayout,
joinPathFragments,
logger,
names,
normalizePath,
Tree,
} from '@nx/devkit';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { assertValidStyle } from '../../../utils/assertion'; import { assertValidStyle } from '../../../utils/assertion';
import { NormalizedSchema, Schema } from '../schema'; import { NormalizedSchema, Schema } from '../schema';
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 {
const { projectDirectory, layoutDirectory } = extractLayoutDirectory( projectName,
options.directory names: projectNames,
); projectRoot,
const fullProjectDirectory = projectDirectory importPath,
? `${names(projectDirectory).fileName}/${name}` } = await determineProjectNameAndRootOptions(host, {
: name; name: options.name,
projectType: 'library',
directory: options.directory,
importPath: options.importPath,
projectNameAndRootFormat: options.projectNameAndRootFormat,
callingGenerator: '@nx/react:library',
});
const projectName = fullProjectDirectory.replace(new RegExp('/', 'g'), '-'); const fileName = options.simpleName
const fileName = options.simpleName ? name : projectName; ? projectNames.projectSimpleName
const { libsDir: defaultLibsDir } = getWorkspaceLayout(host); : projectNames.projectFileName;
const libsDir = layoutDirectory ?? defaultLibsDir;
const projectRoot = joinPathFragments(libsDir, fullProjectDirectory);
const parsedTags = options.tags const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
const importPath =
options.importPath || getImportPath(host, fullProjectDirectory);
let bundler = options.bundler ?? 'none'; let bundler = options.bundler ?? 'none';
if (bundler === 'none') { if (bundler === 'none') {
@ -60,13 +51,11 @@ export function normalizeOptions(
compiler: options.compiler ?? 'babel', compiler: options.compiler ?? 'babel',
bundler, bundler,
fileName, fileName,
routePath: `/${name}`, routePath: `/${projectNames.projectSimpleName}`,
name: projectName, name: projectName,
projectRoot, projectRoot,
projectDirectory: fullProjectDirectory,
parsedTags, parsedTags,
importPath, importPath,
libsDir,
} as NormalizedSchema; } as NormalizedSchema;
// Libraries with a bundler or is publishable must also be buildable. // Libraries with a bundler or is publishable must also be buildable.

View File

@ -17,7 +17,6 @@ import { addInitialRoutes } from '../../../utils/ast-utils';
import { maybeJs } from './maybe-js'; import { maybeJs } from './maybe-js';
import { reactRouterDomVersion } from '../../../utils/versions'; import { reactRouterDomVersion } from '../../../utils/versions';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
let tsModule: typeof import('typescript'); let tsModule: typeof import('typescript');
@ -82,7 +81,7 @@ export function updateAppRoutes(host: Tree, options: NormalizedSchema) {
addRoute(appComponentPath, componentSource, { addRoute(appComponentPath, componentSource, {
routePath: options.routePath, routePath: options.routePath,
componentName: names(options.name).className, componentName: names(options.name).className,
moduleName: getImportPath(host, options.projectDirectory), moduleName: options.importPath,
}) })
); );
host.write(appComponentPath, changes); host.write(appComponentPath, changes);

View File

@ -27,9 +27,16 @@ import { installCommonDependencies } from './lib/install-common-dependencies';
import { setDefaults } from './lib/set-defaults'; import { setDefaults } from './lib/set-defaults';
export async function libraryGenerator(host: Tree, schema: Schema) { export async function libraryGenerator(host: Tree, schema: Schema) {
return await libraryGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options = normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
if (options.publishable === true && !schema.importPath) { if (options.publishable === true && !schema.importPath) {
throw new Error( throw new Error(
`For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)` `For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)`

View File

@ -1,5 +1,6 @@
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/linter'; import type { Linter } from '@nx/linter';
import { SupportedStyles } from '../../../typings/style'; import type { SupportedStyles } from '../../../typings/style';
export interface Schema { export interface Schema {
appProject?: string; appProject?: string;
@ -8,6 +9,7 @@ export interface Schema {
compiler?: 'babel' | 'swc'; compiler?: 'babel' | 'swc';
component?: boolean; component?: boolean;
directory?: string; directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
globalCss?: boolean; globalCss?: boolean;
importPath?: string; importPath?: string;
inSourceTests?: boolean; inSourceTests?: boolean;
@ -35,10 +37,8 @@ export interface NormalizedSchema extends Schema {
fileName: string; fileName: string;
projectRoot: string; projectRoot: string;
routePath: string; routePath: string;
projectDirectory: string;
parsedTags: string[]; parsedTags: string[];
appMain?: string; appMain?: string;
appSourceRoot?: string; appSourceRoot?: string;
libsDir?: string;
unitTestRunner: 'jest' | 'vitest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';
} }

View File

@ -24,7 +24,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": {
@ -33,6 +33,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",

View File

@ -39,8 +39,19 @@ export function addModuleFederationFiles(
} }
export async function remoteGenerator(host: Tree, schema: Schema) { export async function remoteGenerator(host: Tree, schema: Schema) {
return await remoteGeneratorInternal(host, {
projectNameAndRootFormat: 'derived',
...schema,
});
}
export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options = normalizeOptions<Schema>(host, schema); const options = await normalizeOptions<Schema>(
host,
schema,
'@nx/react:remote'
);
const initAppTask = await applicationGenerator(host, { const initAppTask = await applicationGenerator(host, {
...options, ...options,
// Only webpack works with module federation for now. // Only webpack works with module federation for now.

View File

@ -1,12 +1,13 @@
import { Linter } from '@nx/linter'; import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/linter';
import { SupportedStyles } from '../../../typings'; import type { SupportedStyles } from '../../../typings';
export interface Schema { export interface Schema {
classComponent?: boolean; classComponent?: boolean;
compiler?: 'babel' | 'swc'; compiler?: 'babel' | 'swc';
devServerPort?: number; devServerPort?: number;
directory?: string; directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
e2eTestRunner: 'cypress' | 'none'; e2eTestRunner: 'cypress' | 'none';
globalCss?: boolean; globalCss?: boolean;
host?: string; host?: string;

View File

@ -14,7 +14,7 @@
"index": 0 "index": 0
}, },
"x-prompt": "What name would you like to use as the remote application?", "x-prompt": "What name would you like to use as the remote application?",
"pattern": "^[a-zA-Z].*$", "pattern": "^[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",