Skip to content

Commit 59f653f

Browse files
committed
feat(react): Add react router framework to create-nx-workspace and react generator
This also removes Remix from create-nx-workspace (React->Remix)
1 parent cbf80c1 commit 59f653f

File tree

66 files changed

+3598
-754
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

+3598
-754
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 --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 --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 --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 --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
}

e2e/vite/src/vite-legacy.test.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -346,18 +346,30 @@ export default App;
346346
cleanupProject();
347347
});
348348

349-
it('should build app from libs source', () => {
350-
const results = runCLI(`build ${app} --buildLibsFromSource=true`);
351-
expect(results).toContain('Successfully ran target build for project');
352-
// this should be more modules than build from dist
353-
expect(results).toContain('38 modules transformed');
354-
});
349+
it('should build app from libs source and dist', () => {
350+
const getModulesTransformed = (output: string) => {
351+
const match = output.match(/(\d+) modules transformed/);
352+
return match ? parseInt(match[1], 10) : null;
353+
};
354+
355+
const modulesFromSourceResult = runCLI(
356+
`build ${app} --buildLibsFromSource=true`
357+
);
358+
const modulesFromDistResult = runCLI(
359+
`build ${app} --buildLibsFromSource=false`
360+
);
355361

356-
it('should build app from libs dist', () => {
357-
const results = runCLI(`build ${app} --buildLibsFromSource=false`);
358-
expect(results).toContain('Successfully ran target build for project');
359-
// this should be less modules than building from source
360-
expect(results).toContain('36 modules transformed');
362+
const modulesFromSource = getModulesTransformed(modulesFromSourceResult);
363+
const modulesFromDist = getModulesTransformed(modulesFromDistResult);
364+
365+
expect(modulesFromSourceResult).toContain(
366+
'Successfully ran target build for project'
367+
);
368+
expect(modulesFromDistResult).toContain(
369+
'Successfully ran target build for project'
370+
);
371+
// this should be more modules than build from dist
372+
expect(modulesFromSource).toBeGreaterThan(modulesFromDist);
361373
});
362374

363375
it('should build app from libs without package.json in lib', () => {

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',
@@ -376,8 +382,6 @@ async function determineStack(
376382
case Preset.ReactMonorepo:
377383
case Preset.NextJs:
378384
case Preset.NextJsStandalone:
379-
case Preset.RemixStandalone:
380-
case Preset.RemixMonorepo:
381385
case Preset.ReactNative:
382386
case Preset.Expo:
383387
return 'react';
@@ -590,6 +594,8 @@ async function determineReactOptions(
590594
let bundler: undefined | 'webpack' | 'vite' | 'rspack' = undefined;
591595
let unitTestRunner: undefined | 'none' | 'jest' | 'vitest' = undefined;
592596
let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined;
597+
let useReactRouter = false;
598+
let routing = true;
593599
let nextAppDir = false;
594600
let nextSrcDir = false;
595601
let linter: undefined | 'none' | 'eslint';
@@ -601,8 +607,7 @@ async function determineReactOptions(
601607
preset = parsedArgs.preset;
602608
if (
603609
preset === Preset.ReactStandalone ||
604-
preset === Preset.NextJsStandalone ||
605-
preset === Preset.RemixStandalone
610+
preset === Preset.NextJsStandalone
606611
) {
607612
appName = parsedArgs.appName ?? parsedArgs.name;
608613
} else {
@@ -628,17 +633,12 @@ async function determineReactOptions(
628633
} else {
629634
preset = Preset.NextJs;
630635
}
631-
} else if (framework === 'remix') {
632-
if (isStandalone) {
633-
preset = Preset.RemixStandalone;
634-
} else {
635-
preset = Preset.RemixMonorepo;
636-
}
637636
} else if (framework === 'react-native') {
638637
preset = Preset.ReactNative;
639638
} else if (framework === 'expo') {
640639
preset = Preset.Expo;
641640
} else {
641+
useReactRouter = await determineReactRouter(parsedArgs);
642642
if (isStandalone) {
643643
preset = Preset.ReactStandalone;
644644
} else {
@@ -648,7 +648,7 @@ async function determineReactOptions(
648648
}
649649

650650
if (preset === Preset.ReactStandalone || preset === Preset.ReactMonorepo) {
651-
bundler = await determineReactBundler(parsedArgs);
651+
bundler = useReactRouter ? 'vite' : await determineReactBundler(parsedArgs);
652652
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
653653
preferVitest: bundler === 'vite',
654654
});
@@ -660,14 +660,6 @@ async function determineReactOptions(
660660
exclude: 'vitest',
661661
});
662662
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
663-
} else if (
664-
preset === Preset.RemixMonorepo ||
665-
preset === Preset.RemixStandalone
666-
) {
667-
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
668-
preferVitest: true,
669-
});
670-
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
671663
} else if (preset === Preset.ReactNative || preset === Preset.Expo) {
672664
unitTestRunner = await determineUnitTestRunner(parsedArgs, {
673665
exclude: 'vitest',
@@ -747,6 +739,8 @@ async function determineReactOptions(
747739
nextSrcDir,
748740
unitTestRunner,
749741
e2eTestRunner,
742+
useReactRouter,
743+
routing,
750744
linter,
751745
formatter,
752746
workspaces,
@@ -1220,9 +1214,9 @@ async function determineAppName(
12201214

12211215
async function determineReactFramework(
12221216
parsedArgs: yargs.Arguments<ReactArguments>
1223-
): Promise<'none' | 'nextjs' | 'remix' | 'expo' | 'react-native'> {
1217+
): Promise<'none' | 'nextjs' | 'expo' | 'react-native'> {
12241218
const reply = await enquirer.prompt<{
1225-
framework: 'none' | 'nextjs' | 'remix' | 'expo' | 'react-native';
1219+
framework: 'none' | 'nextjs' | 'expo' | 'react-native';
12261220
}>([
12271221
{
12281222
name: 'framework',
@@ -1232,23 +1226,19 @@ async function determineReactFramework(
12321226
{
12331227
name: 'none',
12341228
message: 'None',
1235-
hint: ' I only want react and react-dom',
1229+
hint: ' I only want react, react-dom or react-router',
12361230
},
12371231
{
12381232
name: 'nextjs',
1239-
message: 'Next.js [ https://nextjs.org/ ]',
1240-
},
1241-
{
1242-
name: 'remix',
1243-
message: 'Remix [ https://remix.run/ ]',
1233+
message: 'Next.js [ https://nextjs.org/ ]',
12441234
},
12451235
{
12461236
name: 'expo',
1247-
message: 'Expo [ https://expo.io/ ]',
1237+
message: 'Expo [ https://expo.io/ ]',
12481238
},
12491239
{
12501240
name: 'react-native',
1251-
message: 'React Native [ https://reactnative.dev/ ]',
1241+
message: 'React Native [ https://reactnative.dev/ ]',
12521242
},
12531243
],
12541244
initial: 0,
@@ -1493,3 +1483,35 @@ async function determineE2eTestRunner(
14931483
]);
14941484
return reply.e2eTestRunner;
14951485
}
1486+
1487+
async function determineReactRouter(
1488+
parsedArgs: yargs.Arguments<{
1489+
useReactRouter?: boolean;
1490+
}>
1491+
): Promise<boolean> {
1492+
if (parsedArgs.routing !== undefined && parsedArgs.routing === false)
1493+
return false;
1494+
if (parsedArgs.useReactRouter !== undefined) return parsedArgs.useReactRouter;
1495+
const reply = await enquirer.prompt<{
1496+
response: 'Yes' | 'No';
1497+
}>([
1498+
{
1499+
message:
1500+
'Would you like to use React Router for server-side rendering [https://reactrouter.com/]?',
1501+
type: 'autocomplete',
1502+
name: 'response',
1503+
skip: !parsedArgs.interactive || isCI(),
1504+
choices: [
1505+
{
1506+
name: 'Yes',
1507+
hint: 'I want to use React Router',
1508+
},
1509+
{
1510+
name: 'No',
1511+
},
1512+
],
1513+
initial: 0,
1514+
},
1515+
]);
1516+
return reply.response === 'Yes';
1517+
}

0 commit comments

Comments
 (0)