Skip to content

Commit f40873f

Browse files
authored
feat(react): add react-router plugin (#29965)
This PR introduces the React Router plugin in Nx. The new functionality adds a react-router plugin entry into `nx.json`, projects that are React-Router V7 via `react-router.config.(m|c)?[jt]s` will have their targets inferred. ### Changes Update the React plugin to have a react-router (RR V7) plugin export. The RR V7 will only infer targets if we have a `react-router.config.(m|c)?[jt]s` and also a `vite.config.(m|c)?[jt]s`. Under the hood the RR V7 CLI uses vite for compilation. That being said, apps are not limited to only use vite for RR V7. Should you choose to use it the compilation will not be done via RR V7 CLI.
1 parent a72ffcb commit f40873f

File tree

10 files changed

+807
-9
lines changed

10 files changed

+807
-9
lines changed

packages/nx/src/command-line/init/init-v2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ const npmPackageToPluginMap: Record<string, `@nx/${string}`> = {
207207
'react-native': '@nx/react-native',
208208
'@remix-run/dev': '@nx/remix',
209209
'@rsbuild/core': '@nx/rsbuild',
210+
'@react-router/dev': '@nx/react',
210211
};
211212

212213
export async function detectPlugins(

packages/react/router-plugin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export {
2+
createNodesV2,
3+
ReactRouterPluginOptions,
4+
} from './src/plugins/router-plugin';

packages/react/src/generators/init/init.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
addDependenciesToPackageJson,
3+
createProjectGraphAsync,
34
formatFiles,
5+
readNxJson,
46
removeDependenciesFromPackageJson,
57
runTasksInSerial,
68
type GeneratorCallback,
@@ -9,6 +11,8 @@ import {
911
import { nxVersion } from '../../utils/versions';
1012
import { InitSchema } from './schema';
1113
import { getReactDependenciesVersionsToInstall } from '../../utils/version-utils';
14+
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
15+
import { createNodesV2 } from '../../plugins/router-plugin';
1216

1317
export async function reactInitGenerator(tree: Tree, schema: InitSchema) {
1418
const tasks: GeneratorCallback[] = [];
@@ -32,6 +36,36 @@ export async function reactInitGenerator(tree: Tree, schema: InitSchema) {
3236
);
3337
}
3438

39+
const nxJson = readNxJson(tree);
40+
schema.addPlugin ??=
41+
process.env.NX_ADD_PLUGINS !== 'false' &&
42+
nxJson.useInferencePlugins !== false;
43+
44+
if (schema.addPlugin) {
45+
await addPlugin(
46+
tree,
47+
await createProjectGraphAsync(),
48+
'@nx/react/router-plugin',
49+
createNodesV2,
50+
{
51+
buildTargetName: ['build', 'react-router:build', 'react-router-build'],
52+
devTargetName: ['dev', 'react-router:dev', 'react-router-dev'],
53+
startTargetName: ['start', 'react-router-serve', 'react-router-start'],
54+
watchDepsTargetName: [
55+
'watch-deps',
56+
'react-router:watch-deps',
57+
'react-router-watch-deps',
58+
],
59+
buildDepsTargetName: [
60+
'build-deps',
61+
'react-router:build-deps',
62+
'react-router-build-deps',
63+
],
64+
},
65+
schema.updatePackageScripts
66+
);
67+
}
68+
3569
if (!schema.skipFormat) {
3670
await formatFiles(tree);
3771
}

packages/react/src/generators/init/schema.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ export interface InitSchema {
22
skipFormat?: boolean;
33
skipPackageJson?: boolean;
44
keepExistingVersions?: boolean;
5+
updatePackageScripts?: boolean;
6+
addPlugin?: boolean;
57
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`@nx/react/react-router-plugin React Router should create nodes by default 1`] = `
4+
[
5+
[
6+
"acme/react-router.config.js",
7+
{
8+
"projects": {
9+
"acme": {
10+
"metadata": {},
11+
"projectType": "application",
12+
"root": "acme",
13+
"targets": {
14+
"build": {
15+
"cache": true,
16+
"command": "react-router build",
17+
"dependsOn": [
18+
"^build",
19+
],
20+
"inputs": [
21+
"production",
22+
"^production",
23+
{
24+
"externalDependencies": [
25+
"@react-router/dev",
26+
],
27+
},
28+
],
29+
"options": {
30+
"cwd": "acme",
31+
},
32+
"outputs": [
33+
"{workspaceRoot}/acme/build/client",
34+
"{workspaceRoot}/acme/build/server",
35+
],
36+
},
37+
"build-deps": {
38+
"dependsOn": [
39+
"^build",
40+
],
41+
},
42+
"dev": {
43+
"command": "react-router dev",
44+
"options": {
45+
"cwd": "acme",
46+
},
47+
},
48+
"start": {
49+
"command": "react-router-serve build/server/index.js",
50+
"dependsOn": [
51+
"build",
52+
],
53+
"options": {
54+
"cwd": "acme",
55+
},
56+
},
57+
"typecheck": {
58+
"cache": true,
59+
"command": "tsc --noEmit",
60+
"inputs": [
61+
"production",
62+
"^production",
63+
{
64+
"externalDependencies": [
65+
"typescript",
66+
],
67+
},
68+
],
69+
"metadata": {
70+
"description": "Runs type-checking for the project.",
71+
"help": {
72+
"command": "npx tsc --help",
73+
"example": {
74+
"options": {
75+
"noEmit": true,
76+
},
77+
},
78+
},
79+
"technologies": [
80+
"typescript",
81+
],
82+
},
83+
"options": {
84+
"cwd": "acme",
85+
},
86+
},
87+
"watch-deps": {
88+
"command": "npx nx watch --projects acme --includeDependentProjects -- npx nx build-deps acme",
89+
"dependsOn": [
90+
"build-deps",
91+
],
92+
},
93+
},
94+
},
95+
},
96+
},
97+
],
98+
]
99+
`;
100+
101+
exports[`@nx/react/react-router-plugin React Router should create nodes without start target if ssr is false 1`] = `
102+
[
103+
[
104+
"acme/react-router.config.js",
105+
{
106+
"projects": {
107+
"acme": {
108+
"metadata": {},
109+
"projectType": "library",
110+
"root": "acme",
111+
"targets": {
112+
"build": {
113+
"cache": true,
114+
"command": "react-router build",
115+
"dependsOn": [
116+
"^build",
117+
],
118+
"inputs": [
119+
"production",
120+
"^production",
121+
{
122+
"externalDependencies": [
123+
"@react-router/dev",
124+
],
125+
},
126+
],
127+
"options": {
128+
"cwd": "acme",
129+
},
130+
"outputs": [
131+
"{workspaceRoot}/acme/build/client",
132+
],
133+
},
134+
"build-deps": {
135+
"dependsOn": [
136+
"^build",
137+
],
138+
},
139+
"dev": {
140+
"command": "react-router dev",
141+
"options": {
142+
"cwd": "acme",
143+
},
144+
},
145+
"typecheck": {
146+
"cache": true,
147+
"command": "tsc --noEmit",
148+
"inputs": [
149+
"production",
150+
"^production",
151+
{
152+
"externalDependencies": [
153+
"typescript",
154+
],
155+
},
156+
],
157+
"metadata": {
158+
"description": "Runs type-checking for the project.",
159+
"help": {
160+
"command": "npx tsc --help",
161+
"example": {
162+
"options": {
163+
"noEmit": true,
164+
},
165+
},
166+
},
167+
"technologies": [
168+
"typescript",
169+
],
170+
},
171+
"options": {
172+
"cwd": "acme",
173+
},
174+
},
175+
"watch-deps": {
176+
"command": "npx nx watch --projects acme --includeDependentProjects -- npx nx build-deps acme",
177+
"dependsOn": [
178+
"build-deps",
179+
],
180+
},
181+
},
182+
},
183+
},
184+
},
185+
],
186+
]
187+
`;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { type CreateNodesContext } from '@nx/devkit';
2+
import { createNodesV2 } from './router-plugin';
3+
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
4+
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
5+
import { join } from 'path';
6+
7+
jest.mock('nx/src/utils/cache-directory', () => ({
8+
...jest.requireActual('nx/src/utils/cache-directory'),
9+
workspaceDataDirectory: 'tmp/project-graph-cache',
10+
}));
11+
12+
jest.mock('@nx/js/src/utils/typescript/ts-solution-setup', () => ({
13+
...jest.requireActual('@nx/js/src/utils/typescript/ts-solution-setup'),
14+
isUsingTsSolutionSetup: jest.fn(),
15+
}));
16+
17+
describe('@nx/react/react-router-plugin', () => {
18+
let createNodesFunction = createNodesV2[1];
19+
let context: CreateNodesContext;
20+
let tempFs: TempFs;
21+
let cwd: string;
22+
23+
beforeEach(() => {
24+
(isUsingTsSolutionSetup as jest.Mock).mockReturnValue(false);
25+
});
26+
27+
describe('React Router', () => {
28+
beforeEach(async () => {
29+
tempFs = new TempFs('test');
30+
cwd = process.cwd();
31+
process.chdir(tempFs.tempDir);
32+
33+
context = {
34+
nxJsonConfiguration: {
35+
namedInputs: {
36+
default: ['{projectRoot}/**/*'],
37+
production: ['!{projectRoot}/**/*.spec.ts'],
38+
},
39+
},
40+
workspaceRoot: tempFs.tempDir,
41+
configFiles: [],
42+
};
43+
44+
await tempFs.createFiles({
45+
'acme/react-router.config.js': 'module.exports = {}',
46+
'acme/vite.config.js': '',
47+
'acme/project.json': JSON.stringify({ name: 'acme' }),
48+
});
49+
});
50+
51+
afterEach(() => {
52+
jest.resetModules();
53+
tempFs.cleanup();
54+
process.chdir(cwd);
55+
});
56+
57+
it('should create nodes by default', async () => {
58+
mockConfig('acme/react-router.config.js', {}, context);
59+
60+
const nodes = await createNodesFunction(
61+
['acme/react-router.config.js'],
62+
{
63+
buildTargetName: 'build',
64+
devTargetName: 'dev',
65+
startTargetName: 'start',
66+
},
67+
context
68+
);
69+
70+
expect(nodes).toMatchSnapshot();
71+
});
72+
73+
it('should create nodes without start target if ssr is false', async () => {
74+
mockConfig('acme/react-router.config.js', { ssr: false }, context);
75+
76+
const nodes = await createNodesFunction(
77+
['acme/react-router.config.js'],
78+
{
79+
buildTargetName: 'build',
80+
devTargetName: 'dev',
81+
startTargetName: 'start',
82+
},
83+
context
84+
);
85+
86+
expect(nodes).toMatchSnapshot();
87+
});
88+
});
89+
90+
function mockConfig(path: string, config, context: CreateNodesContext) {
91+
jest.mock(join(context.workspaceRoot, path), () => config, {
92+
virtual: true,
93+
});
94+
}
95+
});

0 commit comments

Comments
 (0)