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.
This commit is contained in:
parent
a72ffcbe70
commit
f40873ffbe
@ -207,6 +207,7 @@ const npmPackageToPluginMap: Record<string, `@nx/${string}`> = {
|
|||||||
'react-native': '@nx/react-native',
|
'react-native': '@nx/react-native',
|
||||||
'@remix-run/dev': '@nx/remix',
|
'@remix-run/dev': '@nx/remix',
|
||||||
'@rsbuild/core': '@nx/rsbuild',
|
'@rsbuild/core': '@nx/rsbuild',
|
||||||
|
'@react-router/dev': '@nx/react',
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function detectPlugins(
|
export async function detectPlugins(
|
||||||
|
|||||||
4
packages/react/router-plugin.ts
Normal file
4
packages/react/router-plugin.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export {
|
||||||
|
createNodesV2,
|
||||||
|
ReactRouterPluginOptions,
|
||||||
|
} from './src/plugins/router-plugin';
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
addDependenciesToPackageJson,
|
addDependenciesToPackageJson,
|
||||||
|
createProjectGraphAsync,
|
||||||
formatFiles,
|
formatFiles,
|
||||||
|
readNxJson,
|
||||||
removeDependenciesFromPackageJson,
|
removeDependenciesFromPackageJson,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
type GeneratorCallback,
|
type GeneratorCallback,
|
||||||
@ -9,6 +11,8 @@ import {
|
|||||||
import { nxVersion } from '../../utils/versions';
|
import { nxVersion } from '../../utils/versions';
|
||||||
import { InitSchema } from './schema';
|
import { InitSchema } from './schema';
|
||||||
import { getReactDependenciesVersionsToInstall } from '../../utils/version-utils';
|
import { getReactDependenciesVersionsToInstall } from '../../utils/version-utils';
|
||||||
|
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
|
||||||
|
import { createNodesV2 } from '../../plugins/router-plugin';
|
||||||
|
|
||||||
export async function reactInitGenerator(tree: Tree, schema: InitSchema) {
|
export async function reactInitGenerator(tree: Tree, schema: InitSchema) {
|
||||||
const tasks: GeneratorCallback[] = [];
|
const tasks: GeneratorCallback[] = [];
|
||||||
@ -32,6 +36,36 @@ export async function reactInitGenerator(tree: Tree, schema: InitSchema) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
schema.addPlugin ??=
|
||||||
|
process.env.NX_ADD_PLUGINS !== 'false' &&
|
||||||
|
nxJson.useInferencePlugins !== false;
|
||||||
|
|
||||||
|
if (schema.addPlugin) {
|
||||||
|
await addPlugin(
|
||||||
|
tree,
|
||||||
|
await createProjectGraphAsync(),
|
||||||
|
'@nx/react/router-plugin',
|
||||||
|
createNodesV2,
|
||||||
|
{
|
||||||
|
buildTargetName: ['build', 'react-router:build', 'react-router-build'],
|
||||||
|
devTargetName: ['dev', 'react-router:dev', 'react-router-dev'],
|
||||||
|
startTargetName: ['start', 'react-router-serve', 'react-router-start'],
|
||||||
|
watchDepsTargetName: [
|
||||||
|
'watch-deps',
|
||||||
|
'react-router:watch-deps',
|
||||||
|
'react-router-watch-deps',
|
||||||
|
],
|
||||||
|
buildDepsTargetName: [
|
||||||
|
'build-deps',
|
||||||
|
'react-router:build-deps',
|
||||||
|
'react-router-build-deps',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
schema.updatePackageScripts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!schema.skipFormat) {
|
if (!schema.skipFormat) {
|
||||||
await formatFiles(tree);
|
await formatFiles(tree);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,4 +2,6 @@ export interface InitSchema {
|
|||||||
skipFormat?: boolean;
|
skipFormat?: boolean;
|
||||||
skipPackageJson?: boolean;
|
skipPackageJson?: boolean;
|
||||||
keepExistingVersions?: boolean;
|
keepExistingVersions?: boolean;
|
||||||
|
updatePackageScripts?: boolean;
|
||||||
|
addPlugin?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,187 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`@nx/react/react-router-plugin React Router should create nodes by default 1`] = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"acme/react-router.config.js",
|
||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"acme": {
|
||||||
|
"metadata": {},
|
||||||
|
"projectType": "application",
|
||||||
|
"root": "acme",
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"cache": true,
|
||||||
|
"command": "react-router build",
|
||||||
|
"dependsOn": [
|
||||||
|
"^build",
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"production",
|
||||||
|
"^production",
|
||||||
|
{
|
||||||
|
"externalDependencies": [
|
||||||
|
"@react-router/dev",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "acme",
|
||||||
|
},
|
||||||
|
"outputs": [
|
||||||
|
"{workspaceRoot}/acme/build/client",
|
||||||
|
"{workspaceRoot}/acme/build/server",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"build-deps": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^build",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"command": "react-router dev",
|
||||||
|
"options": {
|
||||||
|
"cwd": "acme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"command": "react-router-serve build/server/index.js",
|
||||||
|
"dependsOn": [
|
||||||
|
"build",
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "acme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"typecheck": {
|
||||||
|
"cache": true,
|
||||||
|
"command": "tsc --noEmit",
|
||||||
|
"inputs": [
|
||||||
|
"production",
|
||||||
|
"^production",
|
||||||
|
{
|
||||||
|
"externalDependencies": [
|
||||||
|
"typescript",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"description": "Runs type-checking for the project.",
|
||||||
|
"help": {
|
||||||
|
"command": "npx tsc --help",
|
||||||
|
"example": {
|
||||||
|
"options": {
|
||||||
|
"noEmit": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"technologies": [
|
||||||
|
"typescript",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cwd": "acme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"watch-deps": {
|
||||||
|
"command": "npx nx watch --projects acme --includeDependentProjects -- npx nx build-deps acme",
|
||||||
|
"dependsOn": [
|
||||||
|
"build-deps",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`@nx/react/react-router-plugin React Router should create nodes without start target if ssr is false 1`] = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"acme/react-router.config.js",
|
||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"acme": {
|
||||||
|
"metadata": {},
|
||||||
|
"projectType": "library",
|
||||||
|
"root": "acme",
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"cache": true,
|
||||||
|
"command": "react-router build",
|
||||||
|
"dependsOn": [
|
||||||
|
"^build",
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
"production",
|
||||||
|
"^production",
|
||||||
|
{
|
||||||
|
"externalDependencies": [
|
||||||
|
"@react-router/dev",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "acme",
|
||||||
|
},
|
||||||
|
"outputs": [
|
||||||
|
"{workspaceRoot}/acme/build/client",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"build-deps": {
|
||||||
|
"dependsOn": [
|
||||||
|
"^build",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"command": "react-router dev",
|
||||||
|
"options": {
|
||||||
|
"cwd": "acme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"typecheck": {
|
||||||
|
"cache": true,
|
||||||
|
"command": "tsc --noEmit",
|
||||||
|
"inputs": [
|
||||||
|
"production",
|
||||||
|
"^production",
|
||||||
|
{
|
||||||
|
"externalDependencies": [
|
||||||
|
"typescript",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"description": "Runs type-checking for the project.",
|
||||||
|
"help": {
|
||||||
|
"command": "npx tsc --help",
|
||||||
|
"example": {
|
||||||
|
"options": {
|
||||||
|
"noEmit": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"technologies": [
|
||||||
|
"typescript",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"cwd": "acme",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"watch-deps": {
|
||||||
|
"command": "npx nx watch --projects acme --includeDependentProjects -- npx nx build-deps acme",
|
||||||
|
"dependsOn": [
|
||||||
|
"build-deps",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`;
|
||||||
95
packages/react/src/plugins/router-plugin.spec.ts
Normal file
95
packages/react/src/plugins/router-plugin.spec.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { type CreateNodesContext } from '@nx/devkit';
|
||||||
|
import { createNodesV2 } from './router-plugin';
|
||||||
|
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
|
||||||
|
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
jest.mock('nx/src/utils/cache-directory', () => ({
|
||||||
|
...jest.requireActual('nx/src/utils/cache-directory'),
|
||||||
|
workspaceDataDirectory: 'tmp/project-graph-cache',
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@nx/js/src/utils/typescript/ts-solution-setup', () => ({
|
||||||
|
...jest.requireActual('@nx/js/src/utils/typescript/ts-solution-setup'),
|
||||||
|
isUsingTsSolutionSetup: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('@nx/react/react-router-plugin', () => {
|
||||||
|
let createNodesFunction = createNodesV2[1];
|
||||||
|
let context: CreateNodesContext;
|
||||||
|
let tempFs: TempFs;
|
||||||
|
let cwd: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
(isUsingTsSolutionSetup as jest.Mock).mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('React Router', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
tempFs = new TempFs('test');
|
||||||
|
cwd = process.cwd();
|
||||||
|
process.chdir(tempFs.tempDir);
|
||||||
|
|
||||||
|
context = {
|
||||||
|
nxJsonConfiguration: {
|
||||||
|
namedInputs: {
|
||||||
|
default: ['{projectRoot}/**/*'],
|
||||||
|
production: ['!{projectRoot}/**/*.spec.ts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workspaceRoot: tempFs.tempDir,
|
||||||
|
configFiles: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await tempFs.createFiles({
|
||||||
|
'acme/react-router.config.js': 'module.exports = {}',
|
||||||
|
'acme/vite.config.js': '',
|
||||||
|
'acme/project.json': JSON.stringify({ name: 'acme' }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
tempFs.cleanup();
|
||||||
|
process.chdir(cwd);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create nodes by default', async () => {
|
||||||
|
mockConfig('acme/react-router.config.js', {}, context);
|
||||||
|
|
||||||
|
const nodes = await createNodesFunction(
|
||||||
|
['acme/react-router.config.js'],
|
||||||
|
{
|
||||||
|
buildTargetName: 'build',
|
||||||
|
devTargetName: 'dev',
|
||||||
|
startTargetName: 'start',
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(nodes).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create nodes without start target if ssr is false', async () => {
|
||||||
|
mockConfig('acme/react-router.config.js', { ssr: false }, context);
|
||||||
|
|
||||||
|
const nodes = await createNodesFunction(
|
||||||
|
['acme/react-router.config.js'],
|
||||||
|
{
|
||||||
|
buildTargetName: 'build',
|
||||||
|
devTargetName: 'dev',
|
||||||
|
startTargetName: 'start',
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(nodes).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockConfig(path: string, config, context: CreateNodesContext) {
|
||||||
|
jest.mock(join(context.workspaceRoot, path), () => config, {
|
||||||
|
virtual: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
385
packages/react/src/plugins/router-plugin.ts
Normal file
385
packages/react/src/plugins/router-plugin.ts
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
import {
|
||||||
|
type CreateNodesV2,
|
||||||
|
type CreateNodesContext,
|
||||||
|
detectPackageManager,
|
||||||
|
readJsonFile,
|
||||||
|
type TargetConfiguration,
|
||||||
|
writeJsonFile,
|
||||||
|
createNodesFromFiles,
|
||||||
|
getPackageManagerCommand,
|
||||||
|
joinPathFragments,
|
||||||
|
type ProjectConfiguration,
|
||||||
|
type CreateNodesContextV2,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { existsSync, readdirSync } from 'fs';
|
||||||
|
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
|
||||||
|
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||||
|
import { calculateHashesForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||||
|
import { getLockFileName } from '@nx/js';
|
||||||
|
import { hashObject } from 'nx/src/devkit-internals';
|
||||||
|
import { addBuildAndWatchDepsTargets } from '@nx/js/src/plugins/typescript/util';
|
||||||
|
import { isUsingTsSolutionSetup as _isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
|
||||||
|
import {
|
||||||
|
clearRequireCache,
|
||||||
|
loadConfigFile,
|
||||||
|
} from '@nx/devkit/src/utils/config-utils';
|
||||||
|
|
||||||
|
export interface ReactRouterPluginOptions {
|
||||||
|
buildTargetName?: string;
|
||||||
|
devTargetName?: string;
|
||||||
|
startTargetName?: string;
|
||||||
|
typecheckTargetName?: string;
|
||||||
|
buildDepsTargetName?: string;
|
||||||
|
watchDepsTargetName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReactRouterTargets = Pick<
|
||||||
|
ProjectConfiguration,
|
||||||
|
'targets' | 'metadata' | 'projectType'
|
||||||
|
>;
|
||||||
|
|
||||||
|
const pmCommand = getPackageManagerCommand();
|
||||||
|
const reactRouterConfigBlob = '**/react-router.config.{ts,js,cjs,cts,mjs,mts}';
|
||||||
|
|
||||||
|
function readTargetsCache(
|
||||||
|
cachePath: string
|
||||||
|
): Record<string, ReactRouterTargets> {
|
||||||
|
return process.env.NX_CACHE_PROJECT_GRAPH !== 'false' && existsSync(cachePath)
|
||||||
|
? readJsonFile(cachePath)
|
||||||
|
: {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeTargetsToCache(
|
||||||
|
cachePath: string,
|
||||||
|
results: Record<string, ReactRouterTargets>
|
||||||
|
) {
|
||||||
|
writeJsonFile(cachePath, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNodesV2: CreateNodesV2<ReactRouterPluginOptions> = [
|
||||||
|
reactRouterConfigBlob,
|
||||||
|
async (configFiles, options, context) => {
|
||||||
|
const optionsHash = hashObject(options);
|
||||||
|
const normalizedOptions = normalizeOptions(options);
|
||||||
|
const cachePath = join(
|
||||||
|
workspaceDataDirectory,
|
||||||
|
`react-router-${optionsHash}.hash`
|
||||||
|
);
|
||||||
|
const targetsCache = readTargetsCache(cachePath);
|
||||||
|
|
||||||
|
const isUsingTsSolutionSetup = _isUsingTsSolutionSetup();
|
||||||
|
|
||||||
|
const { roots: projectRoots, configFiles: validConfigFiles } =
|
||||||
|
configFiles.reduce(
|
||||||
|
(acc, configFile) => {
|
||||||
|
const potentialRoot = dirname(configFile);
|
||||||
|
if (checkIfConfigFileShouldBeProject(potentialRoot, context)) {
|
||||||
|
acc.roots.push(potentialRoot);
|
||||||
|
acc.configFiles.push(configFile);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roots: [],
|
||||||
|
configFiles: [],
|
||||||
|
} as {
|
||||||
|
roots: string[];
|
||||||
|
configFiles: string[];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const lockfile = getLockFileName(
|
||||||
|
detectPackageManager(context.workspaceRoot)
|
||||||
|
);
|
||||||
|
const hashes = await calculateHashesForCreateNodes(
|
||||||
|
projectRoots,
|
||||||
|
{ ...normalizedOptions, isUsingTsSolutionSetup },
|
||||||
|
context,
|
||||||
|
projectRoots.map((_) => [lockfile])
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await createNodesFromFiles(
|
||||||
|
async (configFile, _, context, idx) => {
|
||||||
|
const projectRoot = dirname(configFile);
|
||||||
|
|
||||||
|
const siblingFiles = readdirSync(
|
||||||
|
joinPathFragments(context.workspaceRoot, projectRoot)
|
||||||
|
);
|
||||||
|
|
||||||
|
const hash = hashes[idx] + configFile;
|
||||||
|
const { projectType, metadata, targets } = (targetsCache[hash] ??=
|
||||||
|
await buildReactRouterTargets(
|
||||||
|
configFile,
|
||||||
|
projectRoot,
|
||||||
|
normalizedOptions,
|
||||||
|
context,
|
||||||
|
siblingFiles,
|
||||||
|
isUsingTsSolutionSetup
|
||||||
|
));
|
||||||
|
|
||||||
|
const project: ProjectConfiguration = {
|
||||||
|
root: projectRoot,
|
||||||
|
targets,
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (project.targets[normalizedOptions.buildTargetName]) {
|
||||||
|
project.projectType = projectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects: {
|
||||||
|
[projectRoot]: project,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validConfigFiles,
|
||||||
|
options,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
writeTargetsToCache(cachePath, targetsCache);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function buildReactRouterTargets(
|
||||||
|
configFilePath: string,
|
||||||
|
projectRoot: string,
|
||||||
|
options: ReactRouterPluginOptions,
|
||||||
|
context: CreateNodesContext,
|
||||||
|
siblingFiles: string[],
|
||||||
|
isUsingTsSolutionSetup: boolean
|
||||||
|
): Promise<ReactRouterTargets> {
|
||||||
|
const namedInputs = getNamedInputs(projectRoot, context);
|
||||||
|
const configPath = join(context.workspaceRoot, configFilePath);
|
||||||
|
|
||||||
|
if (require.cache[configPath]) clearRequireCache();
|
||||||
|
const reactRouterConfig = await loadConfigFile(configPath);
|
||||||
|
const isLibMode =
|
||||||
|
reactRouterConfig?.ssr !== undefined && reactRouterConfig.ssr === false;
|
||||||
|
|
||||||
|
const { buildDirectory, serverBuildPath } = await getBuildPaths(
|
||||||
|
reactRouterConfig,
|
||||||
|
isLibMode
|
||||||
|
);
|
||||||
|
|
||||||
|
const targets: Record<string, TargetConfiguration> = {};
|
||||||
|
|
||||||
|
targets[options.buildTargetName] = await getBuildTargetConfig(
|
||||||
|
options.buildTargetName,
|
||||||
|
projectRoot,
|
||||||
|
buildDirectory,
|
||||||
|
serverBuildPath,
|
||||||
|
namedInputs,
|
||||||
|
isUsingTsSolutionSetup
|
||||||
|
);
|
||||||
|
|
||||||
|
targets[options.devTargetName] = await devTarget(
|
||||||
|
projectRoot,
|
||||||
|
isUsingTsSolutionSetup
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serverBuildPath) {
|
||||||
|
targets[options.startTargetName] = await startTarget(
|
||||||
|
projectRoot,
|
||||||
|
serverBuildPath,
|
||||||
|
options.buildTargetName,
|
||||||
|
isUsingTsSolutionSetup
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
targets[options.typecheckTargetName] = await typecheckTarget(
|
||||||
|
projectRoot,
|
||||||
|
options.typecheckTargetName,
|
||||||
|
namedInputs,
|
||||||
|
siblingFiles,
|
||||||
|
isUsingTsSolutionSetup
|
||||||
|
);
|
||||||
|
|
||||||
|
addBuildAndWatchDepsTargets(
|
||||||
|
context.workspaceRoot,
|
||||||
|
projectRoot,
|
||||||
|
targets,
|
||||||
|
options,
|
||||||
|
pmCommand
|
||||||
|
);
|
||||||
|
const metadata = {};
|
||||||
|
return {
|
||||||
|
targets,
|
||||||
|
metadata,
|
||||||
|
projectType: isLibMode ? 'library' : 'application',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBuildTargetConfig(
|
||||||
|
buildTargetName: string,
|
||||||
|
projectRoot: string,
|
||||||
|
buildDirectory: string,
|
||||||
|
serverBuildDirectory: string,
|
||||||
|
namedInputs: { [inputName: string]: any[] },
|
||||||
|
isUsingTsSolutionSetup: boolean
|
||||||
|
) {
|
||||||
|
const basePath =
|
||||||
|
projectRoot === '.'
|
||||||
|
? `{workspaceRoot}`
|
||||||
|
: joinPathFragments(`{workspaceRoot}`, projectRoot);
|
||||||
|
|
||||||
|
const outputs = [
|
||||||
|
joinPathFragments(basePath, buildDirectory),
|
||||||
|
...(serverBuildDirectory
|
||||||
|
? [joinPathFragments(basePath, serverBuildDirectory)]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const buildTarget: TargetConfiguration = {
|
||||||
|
cache: true,
|
||||||
|
dependsOn: [`^${buildTargetName}`],
|
||||||
|
inputs: [
|
||||||
|
...('production' in namedInputs
|
||||||
|
? ['production', '^production']
|
||||||
|
: ['default', '^default']),
|
||||||
|
{ externalDependencies: ['@react-router/dev'] },
|
||||||
|
],
|
||||||
|
outputs,
|
||||||
|
command: 'react-router build',
|
||||||
|
options: { cwd: projectRoot },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isUsingTsSolutionSetup) {
|
||||||
|
buildTarget.syncGenerators = ['@nx/js:typescript-sync'];
|
||||||
|
}
|
||||||
|
return buildTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBuildPaths(reactRouterConfig, isLibMode: boolean) {
|
||||||
|
return {
|
||||||
|
buildDirectory: reactRouterConfig?.buildDirectory ?? 'build/client',
|
||||||
|
...(isLibMode
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
serverBuildPath: reactRouterConfig?.buildDirectory
|
||||||
|
? join(dirname(reactRouterConfig.buildDirectory), `server`)
|
||||||
|
: 'build/server',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function devTarget(projectRoot: string, isUsingTsSolutionSetup: boolean) {
|
||||||
|
const devTarget: TargetConfiguration = {
|
||||||
|
command: 'react-router dev',
|
||||||
|
options: { cwd: projectRoot },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isUsingTsSolutionSetup) {
|
||||||
|
devTarget.syncGenerators = ['@nx/js:typescript-sync'];
|
||||||
|
}
|
||||||
|
return devTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startTarget(
|
||||||
|
projectRoot: string,
|
||||||
|
serverBuildPath: string,
|
||||||
|
buildTargetName: string,
|
||||||
|
isUsingTsSolutionSetup: boolean
|
||||||
|
) {
|
||||||
|
const serverPath =
|
||||||
|
serverBuildPath === 'build/server'
|
||||||
|
? `${serverBuildPath}/index.js`
|
||||||
|
: serverBuildPath;
|
||||||
|
|
||||||
|
const startTarget: TargetConfiguration = {
|
||||||
|
dependsOn: [buildTargetName],
|
||||||
|
command: `react-router-serve ${serverPath}`,
|
||||||
|
options: { cwd: projectRoot },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isUsingTsSolutionSetup) {
|
||||||
|
startTarget.syncGenerators = ['@nx/js:typescript-sync'];
|
||||||
|
}
|
||||||
|
return startTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function typecheckTarget(
|
||||||
|
projectRoot: string,
|
||||||
|
typecheckTargetName: string,
|
||||||
|
namedInputs: { [inputName: string]: any[] },
|
||||||
|
siblingFiles: string[],
|
||||||
|
isUsingTsSolutionSetup: boolean
|
||||||
|
) {
|
||||||
|
const hasTsConfigAppJson = siblingFiles.includes('tsconfig.app.json');
|
||||||
|
const typecheckTarget: TargetConfiguration = {
|
||||||
|
cache: true,
|
||||||
|
inputs: [
|
||||||
|
...('production' in namedInputs
|
||||||
|
? ['production', '^production']
|
||||||
|
: ['default', '^default']),
|
||||||
|
{ externalDependencies: ['typescript'] },
|
||||||
|
],
|
||||||
|
command: isUsingTsSolutionSetup
|
||||||
|
? `tsc --build --emitDeclarationOnly`
|
||||||
|
: `tsc${hasTsConfigAppJson ? ` -p tsconfig.app.json` : ``} --noEmit`,
|
||||||
|
options: {
|
||||||
|
cwd: projectRoot,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
description: `Runs type-checking for the project.`,
|
||||||
|
technologies: ['typescript'],
|
||||||
|
help: {
|
||||||
|
command: isUsingTsSolutionSetup
|
||||||
|
? `${pmCommand.exec} tsc --build --help`
|
||||||
|
: `${pmCommand.exec} tsc${
|
||||||
|
hasTsConfigAppJson ? ` -p tsconfig.app.json` : ``
|
||||||
|
} --help`,
|
||||||
|
example: isUsingTsSolutionSetup
|
||||||
|
? { args: ['--force'] }
|
||||||
|
: { options: { noEmit: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isUsingTsSolutionSetup) {
|
||||||
|
typecheckTarget.dependsOn = [`^${typecheckTargetName}`];
|
||||||
|
typecheckTarget.syncGenerators = ['@nx/js:typescript-sync'];
|
||||||
|
}
|
||||||
|
return typecheckTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptions(options: ReactRouterPluginOptions) {
|
||||||
|
options ??= {};
|
||||||
|
options.buildTargetName ??= 'build';
|
||||||
|
options.devTargetName ??= 'dev';
|
||||||
|
options.startTargetName ??= 'start';
|
||||||
|
options.typecheckTargetName ??= 'typecheck';
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIfConfigFileShouldBeProject(
|
||||||
|
projectRoot: string,
|
||||||
|
context: CreateNodesContext | CreateNodesContextV2
|
||||||
|
): boolean {
|
||||||
|
// Do not create a project if package.json and project.json isn't there.
|
||||||
|
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
|
||||||
|
return hasRequiredConfigs(siblingFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRequiredConfigs(files: string[]): boolean {
|
||||||
|
const lowerFiles = files.map((file) => file.toLowerCase());
|
||||||
|
|
||||||
|
// Check if vite.config.{ext} is present
|
||||||
|
const hasViteConfig = lowerFiles.some((file) => {
|
||||||
|
const parts = file.split('.');
|
||||||
|
return parts[0] === 'vite' && parts[1] === 'config' && parts.length > 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasViteConfig) return false;
|
||||||
|
|
||||||
|
const hasProjectOrPackageJson =
|
||||||
|
lowerFiles.includes('project.json') || lowerFiles.includes('package.json');
|
||||||
|
|
||||||
|
return hasProjectOrPackageJson;
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ export const emotionBabelPlugin = '11.11.0';
|
|||||||
export const styledJsxVersion = '5.1.2';
|
export const styledJsxVersion = '5.1.2';
|
||||||
|
|
||||||
export const reactRouterDomVersion = '6.29.0';
|
export const reactRouterDomVersion = '6.29.0';
|
||||||
|
export const reactRouterVersion = '7.1.5';
|
||||||
|
|
||||||
export const testingLibraryReactVersion = '16.1.0';
|
export const testingLibraryReactVersion = '16.1.0';
|
||||||
export const testingLibraryDomVersion = '10.4.0';
|
export const testingLibraryDomVersion = '10.4.0';
|
||||||
|
|||||||
@ -69,6 +69,39 @@ describe('@nx/vite/plugin', () => {
|
|||||||
expect(nodes).toMatchSnapshot();
|
expect(nodes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not create nodes when react-router.config is present', async () => {
|
||||||
|
tempFs.createFileSync('react-router.config.ts', '');
|
||||||
|
|
||||||
|
const nodes = await createNodesFunction(
|
||||||
|
['vite.config.ts'],
|
||||||
|
{
|
||||||
|
buildTargetName: 'build',
|
||||||
|
serveTargetName: 'serve',
|
||||||
|
previewTargetName: 'preview',
|
||||||
|
testTargetName: 'test',
|
||||||
|
serveStaticTargetName: 'serve-static',
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(nodes).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"vite.config.ts",
|
||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
".": {
|
||||||
|
"metadata": {},
|
||||||
|
"root": ".",
|
||||||
|
"targets": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
it('should create nodes when rollupOptions contains input', async () => {
|
it('should create nodes when rollupOptions contains input', async () => {
|
||||||
// Don't need index.html if we're setting inputs
|
// Don't need index.html if we're setting inputs
|
||||||
tempFs.removeFileSync('index.html');
|
tempFs.removeFileSync('index.html');
|
||||||
@ -252,6 +285,39 @@ describe('@nx/vite/plugin', () => {
|
|||||||
|
|
||||||
expect(nodes).toMatchSnapshot();
|
expect(nodes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not create nodes when react-router.config is present', async () => {
|
||||||
|
tempFs.createFileSync('my-app/react-router.config.ts', '');
|
||||||
|
|
||||||
|
const nodes = await createNodesFunction(
|
||||||
|
['my-app/vite.config.ts'],
|
||||||
|
{
|
||||||
|
buildTargetName: 'build',
|
||||||
|
serveTargetName: 'serve',
|
||||||
|
previewTargetName: 'preview',
|
||||||
|
testTargetName: 'test',
|
||||||
|
serveStaticTargetName: 'serve-static',
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(nodes).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"my-app/vite.config.ts",
|
||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"my-app": {
|
||||||
|
"metadata": {},
|
||||||
|
"root": "my-app",
|
||||||
|
"targets": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Library mode', () => {
|
describe('Library mode', () => {
|
||||||
|
|||||||
@ -122,6 +122,15 @@ export const createNodesV2: CreateNodesV2<VitePluginOptions> = [
|
|||||||
minimatch(p, 'tsconfig*{.json,.*.json}')
|
minimatch(p, 'tsconfig*{.json,.*.json}')
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
|
const hasReactRouterConfig = siblingFiles.some((configFile) => {
|
||||||
|
const parts = configFile.split('.');
|
||||||
|
return (
|
||||||
|
parts[0] === 'react-router' &&
|
||||||
|
parts[1] === 'config' &&
|
||||||
|
parts.length > 2
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// results from vitest.config.js will be different from results of vite.config.js
|
// results from vitest.config.js will be different from results of vite.config.js
|
||||||
// but the hash will be the same because it is based on the files under the project root.
|
// but the hash will be the same because it is based on the files under the project root.
|
||||||
// Adding the config file path to the hash ensures that the final hash value is different
|
// Adding the config file path to the hash ensures that the final hash value is different
|
||||||
@ -133,6 +142,7 @@ export const createNodesV2: CreateNodesV2<VitePluginOptions> = [
|
|||||||
projectRoot,
|
projectRoot,
|
||||||
normalizedOptions,
|
normalizedOptions,
|
||||||
tsConfigFiles,
|
tsConfigFiles,
|
||||||
|
hasReactRouterConfig,
|
||||||
isUsingTsSolutionSetup,
|
isUsingTsSolutionSetup,
|
||||||
context
|
context
|
||||||
));
|
));
|
||||||
@ -185,6 +195,13 @@ export const createNodes: CreateNodes<VitePluginOptions> = [
|
|||||||
siblingFiles.filter((p) => minimatch(p, 'tsconfig*{.json,.*.json}')) ??
|
siblingFiles.filter((p) => minimatch(p, 'tsconfig*{.json,.*.json}')) ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
|
const hasReactRouterConfig = siblingFiles.some((configFile) => {
|
||||||
|
const parts = configFile.split('.');
|
||||||
|
return (
|
||||||
|
parts[0] === 'react-router' && parts[1] === 'config' && parts.length > 2
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const normalizedOptions = normalizeOptions(options);
|
const normalizedOptions = normalizeOptions(options);
|
||||||
|
|
||||||
const isUsingTsSolutionSetup = _isUsingTsSolutionSetup();
|
const isUsingTsSolutionSetup = _isUsingTsSolutionSetup();
|
||||||
@ -194,6 +211,7 @@ export const createNodes: CreateNodes<VitePluginOptions> = [
|
|||||||
projectRoot,
|
projectRoot,
|
||||||
normalizedOptions,
|
normalizedOptions,
|
||||||
tsConfigFiles,
|
tsConfigFiles,
|
||||||
|
hasReactRouterConfig,
|
||||||
isUsingTsSolutionSetup,
|
isUsingTsSolutionSetup,
|
||||||
context
|
context
|
||||||
);
|
);
|
||||||
@ -222,6 +240,7 @@ async function buildViteTargets(
|
|||||||
projectRoot: string,
|
projectRoot: string,
|
||||||
options: VitePluginOptions,
|
options: VitePluginOptions,
|
||||||
tsConfigFiles: string[],
|
tsConfigFiles: string[],
|
||||||
|
hasReactRouterConfig: boolean,
|
||||||
isUsingTsSolutionSetup: boolean,
|
isUsingTsSolutionSetup: boolean,
|
||||||
context: CreateNodesContext
|
context: CreateNodesContext
|
||||||
): Promise<ViteTargets> {
|
): Promise<ViteTargets> {
|
||||||
@ -253,6 +272,19 @@ async function buildViteTargets(
|
|||||||
|
|
||||||
const targets: Record<string, TargetConfiguration> = {};
|
const targets: Record<string, TargetConfiguration> = {};
|
||||||
|
|
||||||
|
// if file is vitest.config or vite.config has definition for test, create target for test
|
||||||
|
if (configFilePath.includes('vitest.config') || hasTest) {
|
||||||
|
targets[options.testTargetName] = await testTarget(
|
||||||
|
namedInputs,
|
||||||
|
testOutputs,
|
||||||
|
projectRoot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasReactRouterConfig) {
|
||||||
|
// If we have a react-router config, we can skip the rest of the targets
|
||||||
|
return { targets, metadata: {}, projectType: 'application' };
|
||||||
|
}
|
||||||
// If file is not vitest.config and buildable, create targets for build, serve, preview and serve-static
|
// If file is not vitest.config and buildable, create targets for build, serve, preview and serve-static
|
||||||
const hasRemixPlugin =
|
const hasRemixPlugin =
|
||||||
viteBuildConfig.plugins &&
|
viteBuildConfig.plugins &&
|
||||||
@ -335,15 +367,6 @@ async function buildViteTargets(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if file is vitest.config or vite.config has definition for test, create target for test
|
|
||||||
if (configFilePath.includes('vitest.config') || hasTest) {
|
|
||||||
targets[options.testTargetName] = await testTarget(
|
|
||||||
namedInputs,
|
|
||||||
testOutputs,
|
|
||||||
projectRoot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
addBuildAndWatchDepsTargets(
|
addBuildAndWatchDepsTargets(
|
||||||
context.workspaceRoot,
|
context.workspaceRoot,
|
||||||
projectRoot,
|
projectRoot,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user