Skip to content

Commit eb9caa1

Browse files
authored
feat(react): use helper to determine project name and root directory in project generators (#18615)
1 parent 71d2994 commit eb9caa1

28 files changed

+346
-117
lines changed

docs/generated/packages/react/generators/application.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "application",
3-
"factory": "./src/generators/application/application#applicationGenerator",
3+
"factory": "./src/generators/application/application#applicationGeneratorInternal",
44
"schema": {
55
"$schema": "http://json-schema.org/schema",
66
"cli": "nx",
@@ -28,14 +28,19 @@
2828
"type": "string",
2929
"$default": { "$source": "argv", "index": 0 },
3030
"x-prompt": "What name would you like to use for the application?",
31-
"pattern": "^[a-zA-Z].*$"
31+
"pattern": "^[a-zA-Z][^:]*$"
3232
},
3333
"directory": {
3434
"description": "The directory of the new application.",
3535
"type": "string",
3636
"alias": "dir",
3737
"x-priority": "important"
3838
},
39+
"projectNameAndRootFormat": {
40+
"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`).",
41+
"type": "string",
42+
"enum": ["as-provided", "derived"]
43+
},
3944
"style": {
4045
"description": "The file extension to be used for style files.",
4146
"type": "string",
@@ -192,7 +197,7 @@
192197
"aliases": ["app"],
193198
"x-type": "application",
194199
"description": "Create a React application.",
195-
"implementation": "/packages/react/src/generators/application/application#applicationGenerator.ts",
200+
"implementation": "/packages/react/src/generators/application/application#applicationGeneratorInternal.ts",
196201
"hidden": false,
197202
"path": "/packages/react/src/generators/application/schema.json",
198203
"type": "generator"

docs/generated/packages/react/generators/host.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "host",
3-
"factory": "./src/generators/host/host#hostGenerator",
3+
"factory": "./src/generators/host/host#hostGeneratorInternal",
44
"schema": {
55
"$schema": "http://json-schema.org/schema",
66
"$id": "GeneratorReactHost",
@@ -14,7 +14,7 @@
1414
"description": "The name of the host application to generate the Module Federation configuration",
1515
"$default": { "$source": "argv", "index": 0 },
1616
"x-prompt": "What name would you like to use as the host application?",
17-
"pattern": "^[a-zA-Z].*$",
17+
"pattern": "^[a-zA-Z][^:]*$",
1818
"x-priority": "important"
1919
},
2020
"directory": {
@@ -23,6 +23,11 @@
2323
"alias": "dir",
2424
"x-priority": "important"
2525
},
26+
"projectNameAndRootFormat": {
27+
"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`).",
28+
"type": "string",
29+
"enum": ["as-provided", "derived"]
30+
},
2631
"style": {
2732
"description": "The file extension to be used for style files.",
2833
"type": "string",
@@ -163,7 +168,7 @@
163168
},
164169
"x-type": "application",
165170
"description": "Generate a host react application",
166-
"implementation": "/packages/react/src/generators/host/host#hostGenerator.ts",
171+
"implementation": "/packages/react/src/generators/host/host#hostGeneratorInternal.ts",
167172
"aliases": [],
168173
"hidden": false,
169174
"path": "/packages/react/src/generators/host/schema.json",

docs/generated/packages/react/generators/library.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "library",
3-
"factory": "./src/generators/library/library#libraryGenerator",
3+
"factory": "./src/generators/library/library#libraryGeneratorInternal",
44
"schema": {
55
"$schema": "http://json-schema.org/schema",
66
"cli": "nx",
@@ -24,7 +24,7 @@
2424
"description": "Library name",
2525
"$default": { "$source": "argv", "index": 0 },
2626
"x-prompt": "What name would you like to use for the library?",
27-
"pattern": "^[a-zA-Z].*$",
27+
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
2828
"x-priority": "important"
2929
},
3030
"directory": {
@@ -33,6 +33,11 @@
3333
"alias": "dir",
3434
"x-priority": "important"
3535
},
36+
"projectNameAndRootFormat": {
37+
"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`).",
38+
"type": "string",
39+
"enum": ["as-provided", "derived"]
40+
},
3641
"style": {
3742
"description": "The file extension to be used for style files.",
3843
"type": "string",
@@ -196,7 +201,7 @@
196201
"aliases": ["lib"],
197202
"x-type": "library",
198203
"description": "Create a React library.",
199-
"implementation": "/packages/react/src/generators/library/library#libraryGenerator.ts",
204+
"implementation": "/packages/react/src/generators/library/library#libraryGeneratorInternal.ts",
200205
"hidden": false,
201206
"path": "/packages/react/src/generators/library/schema.json",
202207
"type": "generator"

docs/generated/packages/react/generators/remote.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "remote",
3-
"factory": "./src/generators/remote/remote#remoteGenerator",
3+
"factory": "./src/generators/remote/remote#remoteGeneratorInternal",
44
"schema": {
55
"$schema": "http://json-schema.org/schema",
66
"$id": "GeneratorReactRemote",
@@ -14,7 +14,7 @@
1414
"description": "The name of the remote application to generate the Module Federation configuration",
1515
"$default": { "$source": "argv", "index": 0 },
1616
"x-prompt": "What name would you like to use as the remote application?",
17-
"pattern": "^[a-zA-Z].*$",
17+
"pattern": "^[a-zA-Z][^:]*$",
1818
"x-priority": "important"
1919
},
2020
"directory": {
@@ -23,6 +23,11 @@
2323
"alias": "dir",
2424
"x-priority": "important"
2525
},
26+
"projectNameAndRootFormat": {
27+
"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`).",
28+
"type": "string",
29+
"enum": ["as-provided", "derived"]
30+
},
2631
"style": {
2732
"description": "The file extension to be used for style files.",
2833
"type": "string",
@@ -162,7 +167,7 @@
162167
},
163168
"x-type": "application",
164169
"description": "Generate a remote react application",
165-
"implementation": "/packages/react/src/generators/remote/remote#remoteGenerator.ts",
170+
"implementation": "/packages/react/src/generators/remote/remote#remoteGeneratorInternal.ts",
166171
"aliases": [],
167172
"hidden": false,
168173
"path": "/packages/react/src/generators/remote/schema.json",

e2e/react-core/src/react-module-federation.test.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { stripIndents } from '@nx/devkit';
22
import {
33
checkFilesExist,
44
cleanupProject,
5-
killPort,
65
newProject,
76
readProjectConfig,
87
runCLI,
@@ -114,12 +113,29 @@ describe('React Module Federation', () => {
114113
// }
115114
}, 500_000);
116115

116+
it('should should support generating host and remote apps with the new name and root format', async () => {
117+
const shell = uniq('shell');
118+
const remote = uniq('remote');
119+
120+
runCLI(
121+
`generate @nx/react:host ${shell} --project-name-and-root-format=as-provided --no-interactive`
122+
);
123+
runCLI(
124+
`generate @nx/react:remote ${remote} --host=${shell} --project-name-and-root-format=as-provided --no-interactive`
125+
);
126+
127+
// check files are generated without the layout directory ("apps/") and
128+
// using the project name as the directory when no directory is provided
129+
checkFilesExist(`${shell}/module-federation.config.js`);
130+
checkFilesExist(`${remote}/module-federation.config.js`);
131+
132+
// check default generated host is built successfully
133+
const buildOutput = runCLI(`run ${shell}:build:development`);
134+
expect(buildOutput).toContain('Successfully ran target build');
135+
}, 500_000);
136+
117137
async function readPort(appName: string): Promise<number> {
118138
const config = await readProjectConfig(appName);
119139
return config.targets.serve.options.port;
120140
}
121141
});
122-
123-
function killPorts(ports: number[]): Promise<boolean[]> {
124-
return Promise.all(ports.map((p) => killPort(p)));
125-
}

e2e/react-core/src/react.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,52 @@ describe('React Applications', () => {
227227
);
228228
}, 250_000);
229229

230+
it('should support generating projects with the new name and root format', () => {
231+
const appName = uniq('app1');
232+
const libName = uniq('@my-org/lib1');
233+
234+
runCLI(
235+
`generate @nx/react:app ${appName} --bundler=webpack --project-name-and-root-format=as-provided --no-interactive`
236+
);
237+
238+
// check files are generated without the layout directory ("apps/") and
239+
// using the project name as the directory when no directory is provided
240+
checkFilesExist(`${appName}/src/main.tsx`);
241+
// check build works
242+
expect(runCLI(`build ${appName}`)).toContain(
243+
`Successfully ran target build for project ${appName}`
244+
);
245+
// check tests pass
246+
const appTestResult = runCLI(`test ${appName}`);
247+
expect(appTestResult).toContain(
248+
`Successfully ran target test for project ${appName}`
249+
);
250+
251+
// assert scoped project names are not supported when --project-name-and-root-format=derived
252+
expect(() =>
253+
runCLI(
254+
`generate @nx/react:lib ${libName} --unit-test-runner=jest --buildable --project-name-and-root-format=derived --no-interactive`
255+
)
256+
).toThrow();
257+
258+
runCLI(
259+
`generate @nx/react:lib ${libName} --unit-test-runner=jest --buildable --project-name-and-root-format=as-provided --no-interactive`
260+
);
261+
262+
// check files are generated without the layout directory ("libs/") and
263+
// using the project name as the directory when no directory is provided
264+
checkFilesExist(`${libName}/src/index.ts`);
265+
// check build works
266+
expect(runCLI(`build ${libName}`)).toContain(
267+
`Successfully ran target build for project ${libName}`
268+
);
269+
// check tests pass
270+
const libTestResult = runCLI(`test ${libName}`);
271+
expect(libTestResult).toContain(
272+
`Successfully ran target test for project ${libName}`
273+
);
274+
}, 500_000);
275+
230276
describe('React Applications: --style option', () => {
231277
it.each`
232278
style

packages/devkit/src/generators/project-name-and-root-utils.spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,27 @@ describe('determineProjectNameAndRootOptions', () => {
5656
});
5757
});
5858

59+
it(`should handle window's style paths correctly when format is "as-provided"`, async () => {
60+
const result = await determineProjectNameAndRootOptions(tree, {
61+
name: 'libName',
62+
directory: 'shared\\libName',
63+
projectType: 'library',
64+
projectNameAndRootFormat: 'as-provided',
65+
callingGenerator: '',
66+
});
67+
68+
expect(result).toStrictEqual({
69+
projectName: 'lib-name',
70+
names: {
71+
projectSimpleName: 'lib-name',
72+
projectFileName: 'lib-name',
73+
},
74+
importPath: '@proj/lib-name',
75+
projectRoot: 'shared/lib-name',
76+
projectNameAndRootFormat: 'as-provided',
77+
});
78+
});
79+
5980
it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => {
6081
const result = await determineProjectNameAndRootOptions(tree, {
6182
name: '@scope/libName',
@@ -253,6 +274,27 @@ describe('determineProjectNameAndRootOptions', () => {
253274
expect(result.importPath).toBe('@proj/lib-name');
254275
});
255276

277+
it(`should handle window's style paths correctly when format is "derived"`, async () => {
278+
const result = await determineProjectNameAndRootOptions(tree, {
279+
name: 'libName',
280+
directory: 'shared\\sub-dir',
281+
projectType: 'library',
282+
projectNameAndRootFormat: 'derived',
283+
callingGenerator: '',
284+
});
285+
286+
expect(result).toStrictEqual({
287+
projectName: 'shared-sub-dir-lib-name',
288+
names: {
289+
projectSimpleName: 'lib-name',
290+
projectFileName: 'shared-sub-dir-lib-name',
291+
},
292+
importPath: '@proj/shared/sub-dir/lib-name',
293+
projectRoot: 'shared/sub-dir/lib-name',
294+
projectNameAndRootFormat: 'derived',
295+
});
296+
});
297+
256298
it('should prompt for the project name and root format', async () => {
257299
// simulate interactive mode
258300
ensureInteractiveMode();
@@ -370,6 +412,27 @@ describe('determineProjectNameAndRootOptions', () => {
370412
});
371413
});
372414

415+
it(`should handle window's style paths correctly when format is "as-provided"`, async () => {
416+
const result = await determineProjectNameAndRootOptions(tree, {
417+
name: 'libName',
418+
directory: 'shared\\libName',
419+
projectType: 'library',
420+
projectNameAndRootFormat: 'as-provided',
421+
callingGenerator: '',
422+
});
423+
424+
expect(result).toStrictEqual({
425+
projectName: 'lib-name',
426+
names: {
427+
projectSimpleName: 'lib-name',
428+
projectFileName: 'lib-name',
429+
},
430+
importPath: '@proj/lib-name',
431+
projectRoot: 'shared/lib-name',
432+
projectNameAndRootFormat: 'as-provided',
433+
});
434+
});
435+
373436
it('should use a scoped package name as the project name and import path when format is "as-provided"', async () => {
374437
const result = await determineProjectNameAndRootOptions(tree, {
375438
name: '@scope/libName',
@@ -514,6 +577,27 @@ describe('determineProjectNameAndRootOptions', () => {
514577
});
515578
});
516579

580+
it(`should handle window's style paths correctly when format is "derived"`, async () => {
581+
const result = await determineProjectNameAndRootOptions(tree, {
582+
name: 'libName',
583+
directory: 'shared\\sub-dir',
584+
projectType: 'library',
585+
projectNameAndRootFormat: 'derived',
586+
callingGenerator: '',
587+
});
588+
589+
expect(result).toStrictEqual({
590+
projectName: 'shared-sub-dir-lib-name',
591+
names: {
592+
projectSimpleName: 'lib-name',
593+
projectFileName: 'shared-sub-dir-lib-name',
594+
},
595+
importPath: '@proj/shared/sub-dir/lib-name',
596+
projectRoot: 'libs/shared/sub-dir/lib-name',
597+
projectNameAndRootFormat: 'derived',
598+
});
599+
});
600+
517601
it('should throw when using a scoped package name as the project name and format is derived', async () => {
518602
await expect(
519603
determineProjectNameAndRootOptions(tree, {

packages/devkit/src/generators/project-name-and-root-utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
} from '../utils/get-workspace-layout';
99
import { names } from '../utils/names';
1010

11-
const { joinPathFragments, readJson, readNxJson, updateNxJson } = requireNx();
11+
const { joinPathFragments, normalizePath, readJson, readNxJson, updateNxJson } =
12+
requireNx();
1213

1314
export type ProjectNameAndRootFormat = 'as-provided' | 'derived';
1415
export type ProjectGenerationOptions = {
@@ -169,7 +170,9 @@ function getProjectNameAndRootFormats(
169170
options: ProjectGenerationOptions
170171
): ProjectNameAndRootFormats {
171172
const name = names(options.name).fileName;
172-
const directory = options.directory?.replace(/^\.?\//, '');
173+
const directory = options.directory
174+
? normalizePath(options.directory.replace(/^\.?\//, ''))
175+
: undefined;
173176

174177
const asProvidedProjectName = name;
175178
const asProvidedProjectDirectory = directory

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy