Skip to content

Commit 363088a

Browse files
authored
feat(react): Add react-router to create-nx-workspace and react app generator (#30316)
This pull request introduces improvements to React Router integration and removes the Remix preset. ## Key Changes: - Updated `create-nx-workspace` to support React Router. - Removed the Remix option from `create-nx-workspace`, but the package remains to support existing users. ## SSR & React Router Support - New users who want SSR in their React apps can enable it via the React option and select React Router for SSR support. - The ecosystem has shifted to migrating from Remix to React Router for SSR needs. - This option is only available for plain React apps and uses Vite. Other types of React apps (Micro Frontends, Webpack, Rspack, etc.) remain unaffected. ## Default Routing Behavior `--routing` is now enabled by default when creating a React app using `create-nx-workspace`, aligning with Angular’s default behaviour.
1 parent 50802e7 commit 363088a

File tree

66 files changed

+3595
-749
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+3595
-749
lines changed

docs/generated/cli/create-nx-workspace.md

Lines changed: 33 additions & 34 deletions
Large diffs are not rendered by default.

docs/generated/packages/nx/documents/create-nx-workspace.md

Lines changed: 33 additions & 34 deletions
Large diffs are not rendered by default.

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@
7777
"routing": {
7878
"type": "boolean",
7979
"description": "Generate application with routes.",
80-
"x-prompt": "Would you like to add React Router to this application?",
80+
"x-prompt": "Would you like to add routing to this application?",
81+
"default": false
82+
},
83+
"useReactRouter": {
84+
"description": "Use React Router for routing.",
85+
"type": "boolean",
8186
"default": false
8287
},
8388
"skipFormat": {

docs/generated/packages/workspace/generators/new.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
"type": "boolean",
2626
"default": true
2727
},
28+
"useReactRouter": {
29+
"description": "Use React Router for routing.",
30+
"type": "boolean",
31+
"default": false
32+
},
2833
"standaloneApi": {
2934
"description": "Use Standalone Components if generating an Angular application.",
3035
"type": "boolean",

docs/generated/packages/workspace/generators/preset.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
"type": "boolean",
2626
"default": true
2727
},
28+
"useReactRouter": {
29+
"description": "Use React Router for routing.",
30+
"type": "boolean",
31+
"default": false
32+
},
2833
"style": {
2934
"description": "The file extension to be used for style files.",
3035
"type": "string",

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
checkFilesExist,
3+
cleanupProject,
4+
ensureCypressInstallation,
5+
newProject,
6+
readFile,
7+
runCLI,
8+
uniq,
9+
} from '@nx/e2e/utils';
10+
11+
describe('React Router Applications', () => {
12+
beforeAll(() => {
13+
newProject({ packages: ['@nx/react'] });
14+
ensureCypressInstallation();
15+
});
16+
17+
afterAll(() => cleanupProject());
18+
19+
it('should generate a react-router application', async () => {
20+
const appName = uniq('app');
21+
runCLI(
22+
`generate @nx/react:app ${appName} --use-react-router --routing --no-interactive`
23+
);
24+
25+
const packageJson = JSON.parse(readFile('package.json'));
26+
expect(packageJson.dependencies['react-router']).toBeDefined();
27+
expect(packageJson.dependencies['@react-router/node']).toBeDefined();
28+
expect(packageJson.dependencies['@react-router/serve']).toBeDefined();
29+
expect(packageJson.dependencies['isbot']).toBeDefined();
30+
31+
checkFilesExist(`${appName}/app/app.tsx`);
32+
checkFilesExist(`${appName}/app/entry.client.tsx`);
33+
checkFilesExist(`${appName}/app/entry.server.tsx`);
34+
checkFilesExist(`${appName}/app/routes.tsx`);
35+
checkFilesExist(`${appName}/react-router.config.ts`);
36+
checkFilesExist(`${appName}/vite.config.ts`);
37+
});
38+
39+
it('should be able to build a react-router application', async () => {
40+
const appName = uniq('app');
41+
runCLI(
42+
`generate @nx/react:app ${appName} --use-react-router --routing --no-interactive`
43+
);
44+
45+
const buildResult = runCLI(`build ${appName}`);
46+
expect(buildResult).toContain('Successfully ran target build');
47+
});
48+
49+
it('should be able to lint a react-router application', async () => {
50+
const appName = uniq('app');
51+
runCLI(
52+
`generate @nx/react:app ${appName} --use-react-router --routing --linter=eslint --no-interactive`
53+
);
54+
55+
const buildResult = runCLI(`lint ${appName}`);
56+
expect(buildResult).toContain('Successfully ran target lint');
57+
});
58+
59+
it('should be able to test a react-router application', async () => {
60+
const appName = uniq('app');
61+
runCLI(
62+
`generate @nx/react:app ${appName} --use-react-router --routing --unit-test-runner=vitest --no-interactive`
63+
);
64+
65+
const buildResult = runCLI(`test ${appName}`);
66+
expect(buildResult).toContain('Successfully ran target test');
67+
});
68+
});

e2e/utils/create-project-utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ export function runCreateWorkspace(
222222
cwd = e2eCwd,
223223
bundler,
224224
routing,
225+
useReactRouter,
225226
standaloneApi,
226227
docker,
227228
nextAppDir,
@@ -244,6 +245,7 @@ export function runCreateWorkspace(
244245
bundler?: 'webpack' | 'vite';
245246
standaloneApi?: boolean;
246247
routing?: boolean;
248+
useReactRouter?: boolean;
247249
docker?: boolean;
248250
nextAppDir?: boolean;
249251
nextSrcDir?: boolean;
@@ -295,6 +297,10 @@ export function runCreateWorkspace(
295297
command += ` --routing=${routing}`;
296298
}
297299

300+
if (useReactRouter !== undefined) {
301+
command += ` --useReactRouter=${useReactRouter}`;
302+
}
303+
298304
if (base) {
299305
command += ` --defaultBase="${base}"`;
300306
}

packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ export default defineConfig(() => ({
495495
watch: false,
496496
globals: true,
497497
environment: 'jsdom',
498-
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
498+
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
499499
setupFiles: ['src/test-setup.ts'],
500500
reporters: ['default'],
501501
coverage: {

packages/create-nx-workspace/bin/create-nx-workspace.ts

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ interface ReactArguments extends BaseArguments {
4848
stack: 'react';
4949
workspaceType: 'standalone' | 'integrated';
5050
appName: string;
51-
framework: 'none' | 'next' | 'remix';
51+
framework: 'none' | 'next';
5252
style: string;
5353
bundler: 'webpack' | 'vite' | 'rspack';
5454
nextAppDir: boolean;
5555
nextSrcDir: boolean;
56+
useReactRouter: boolean;
57+
routing: boolean;
5658
unitTestRunner: 'none' | 'jest' | 'vitest';
5759
e2eTestRunner: 'none' | 'cypress' | 'playwright';
5860
}
@@ -156,10 +158,14 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
156158
default: true,
157159
})
158160
.option('routing', {
159-
describe: chalk.dim`Add a routing setup for an Angular app.`,
161+
describe: chalk.dim`Add a routing setup for an Angular or React app.`,
160162
type: 'boolean',
161163
default: true,
162164
})
165+
.option('useReactRouter', {
166+
describe: chalk.dim`Generate a Server-Side Rendered (SSR) React app using React Router.`,
167+
type: 'boolean',
168+
})
163169
.option('bundler', {
164170
describe: chalk.dim`Bundler to be used to build the app.`,
165171
type: 'string',
@@ -378,8 +384,6 @@ async function determineStack(
378384
case Preset.ReactMonorepo:
379385
case Preset.NextJs:
380386
case Preset.NextJsStandalone:
381-
case Preset.RemixStandalone:
382-
case Preset.RemixMonorepo:
383387
case Preset.ReactNative:
384388
case Preset.Expo:
385389
return 'react';
@@ -591,6 +595,8 @@ async function determineReactOptions(
591595
let bundler: undefined | 'webpack' | 'vite' | 'rspack' = undefined;
592596
let unitTestRunner: undefined | 'none' | 'jest' | 'vitest' = undefined;
593597
let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined;
598+
let useReactRouter = false;
599+
let routing = true;
594600
let nextAppDir = false;
595601
let nextSrcDir = false;
596602
let linter: undefined | 'none' | 'eslint';
@@ -602,8 +608,7 @@ async function determineReactOptions(
602608
preset = parsedArgs.preset;
603609
if (
604610
preset === Preset.ReactStandalone ||
605-
preset === Preset.NextJsStandalone ||
606-
preset === Preset.RemixStandalone
611+
preset === Preset.NextJsStandalone
607612
) {
608613
appName = parsedArgs.appName ?? parsedArgs.name;
609614
} else {
@@ -629,17 +634,12 @@ async function determineReactOptions(
629634
} else {
630635
preset = Preset.NextJs;
631636
}
632-
} else if (framework === 'remix') {
633-
if (isStandalone) {
634-
preset = Preset.RemixStandalone;
635-
} else {
636-
preset = Preset.RemixMonorepo;
637-
}
638637
} else if (framework === 'react-native') {
639638
preset = Preset.ReactNative;
640639
} else if (framework === 'expo') {
641640
preset = Preset.Expo;
642641
} else {
642+
useReactRouter = await determineReactRouter(parsedArgs);
643643
if (isStandalone) {
644644
preset = Preset.ReactStandalone;
645645
} else {
@@ -649,7 +649,7 @@ async function determineReactOptions(
649649
}
650650

651651
if (preset === Preset.ReactStandalone || preset === Preset.ReactMonorepo) {
652-
bundler = await determineReactBundler(parsedArgs);
652+
bundler = useReactRouter ? 'vite' : await determineReactBundler(parsedArgs);
653653
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
654654
preferVitest: bundler === 'vite',
655655
});
@@ -661,14 +661,6 @@ async function determineReactOptions(
661661
exclude: 'vitest',
662662
});
663663
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
664-
} else if (
665-
preset === Preset.RemixMonorepo ||
666-
preset === Preset.RemixStandalone
667-
) {
668-
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
669-
preferVitest: true,
670-
});
671-
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
672664
} else if (preset === Preset.ReactNative || preset === Preset.Expo) {
673665
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
674666
exclude: 'vitest',
@@ -748,6 +740,8 @@ async function determineReactOptions(
748740
nextSrcDir,
749741
unitTestRunner,
750742
e2eTestRunner,
743+
useReactRouter,
744+
routing,
751745
linter,
752746
formatter,
753747
workspaces,
@@ -1221,9 +1215,9 @@ async function determineAppName(
12211215

12221216
async function determineReactFramework(
12231217
parsedArgs: yargs.Arguments<ReactArguments>
1224-
): Promise<'none' | 'nextjs' | 'remix' | 'expo' | 'react-native'> {
1218+
): Promise<'none' | 'nextjs' | 'expo' | 'react-native'> {
12251219
const reply = await enquirer.prompt<{
1226-
framework: 'none' | 'nextjs' | 'remix' | 'expo' | 'react-native';
1220+
framework: 'none' | 'nextjs' | 'expo' | 'react-native';
12271221
}>([
12281222
{
12291223
name: 'framework',
@@ -1233,23 +1227,19 @@ async function determineReactFramework(
12331227
{
12341228
name: 'none',
12351229
message: 'None',
1236-
hint: ' I only want react and react-dom',
1230+
hint: ' I only want react, react-dom or react-router',
12371231
},
12381232
{
12391233
name: 'nextjs',
1240-
message: 'Next.js [ https://nextjs.org/ ]',
1241-
},
1242-
{
1243-
name: 'remix',
1244-
message: 'Remix [ https://remix.run/ ]',
1234+
message: 'Next.js [ https://nextjs.org/ ]',
12451235
},
12461236
{
12471237
name: 'expo',
1248-
message: 'Expo [ https://expo.io/ ]',
1238+
message: 'Expo [ https://expo.io/ ]',
12491239
},
12501240
{
12511241
name: 'react-native',
1252-
message: 'React Native [ https://reactnative.dev/ ]',
1242+
message: 'React Native [ https://reactnative.dev/ ]',
12531243
},
12541244
],
12551245
initial: 0,
@@ -1494,3 +1484,35 @@ async function determineE2eTestRunner(
14941484
]);
14951485
return reply.e2eTestRunner;
14961486
}
1487+
1488+
async function determineReactRouter(
1489+
parsedArgs: yargs.Arguments<{
1490+
useReactRouter?: boolean;
1491+
}>
1492+
): Promise<boolean> {
1493+
if (parsedArgs.routing !== undefined && parsedArgs.routing === false)
1494+
return false;
1495+
if (parsedArgs.useReactRouter !== undefined) return parsedArgs.useReactRouter;
1496+
const reply = await enquirer.prompt<{
1497+
response: 'Yes' | 'No';
1498+
}>([
1499+
{
1500+
message:
1501+
'Would you like to use React Router for server-side rendering [https://reactrouter.com/]?',
1502+
type: 'autocomplete',
1503+
name: 'response',
1504+
skip: !parsedArgs.interactive || isCI(),
1505+
choices: [
1506+
{
1507+
name: 'Yes',
1508+
hint: 'I want to use React Router',
1509+
},
1510+
{
1511+
name: 'No',
1512+
},
1513+
],
1514+
initial: 0,
1515+
},
1516+
]);
1517+
return reply.response === 'Yes';
1518+
}

packages/create-nx-workspace/src/create-workspace.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ function getWorkspaceGlobsFromPreset(preset: string): string[] {
112112
case Preset.Nuxt:
113113
case Preset.ReactNative:
114114
case Preset.ReactMonorepo:
115-
case Preset.RemixMonorepo:
116115
case Preset.VueMonorepo:
117116
case Preset.WebComponents:
118117
return ['apps/*'];

packages/create-nx-workspace/src/utils/preset/preset.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ export enum Preset {
1313
NuxtStandalone = 'nuxt-standalone',
1414
NextJs = 'next',
1515
NextJsStandalone = 'nextjs-standalone',
16-
RemixMonorepo = 'remix-monorepo',
17-
RemixStandalone = 'remix-standalone',
1816
ReactNative = 'react-native',
1917
Expo = 'expo',
2018
Nest = 'nest',

packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export default defineConfig(() => ({
184184
watch: false,
185185
globals: true,
186186
environment: 'jsdom',
187-
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
187+
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
188188
reporters: ['default'],
189189
coverage: {
190190
reportsDirectory: '../coverage/my-app',
@@ -585,7 +585,7 @@ export default defineConfig(() => ({
585585
watch: false,
586586
globals: true,
587587
environment: 'jsdom',
588-
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
588+
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
589589
reporters: ['default'],
590590
coverage: {
591591
reportsDirectory: '../coverage/myApp',

packages/react/src/generators/application/__snapshots__/application.spec.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ export default defineConfig(() => ({
448448
watch: false,
449449
globals: true,
450450
environment: 'jsdom',
451-
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
451+
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
452452
reporters: ['default'],
453453
coverage: {
454454
reportsDirectory: '../coverage/my-app',
@@ -511,7 +511,7 @@ export default defineConfig(() => ({
511511
watch: false,
512512
globals: true,
513513
environment: 'jsdom',
514-
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
514+
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
515515
reporters: ['default'],
516516
coverage: {
517517
reportsDirectory: '../coverage/my-app',

0 commit comments

Comments
 (0)