From f40873ffbe1981abd6dd8612268f01ca5c300b7d Mon Sep 17 00:00:00 2001 From: Nicholas Cunningham Date: Fri, 14 Mar 2025 11:08:21 -0600 Subject: [PATCH] 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. --- packages/nx/src/command-line/init/init-v2.ts | 1 + packages/react/router-plugin.ts | 4 + packages/react/src/generators/init/init.ts | 34 ++ .../react/src/generators/init/schema.d.ts | 2 + .../__snapshots__/router-plugin.spec.ts.snap | 187 +++++++++ .../react/src/plugins/router-plugin.spec.ts | 95 +++++ packages/react/src/plugins/router-plugin.ts | 385 ++++++++++++++++++ packages/react/src/utils/versions.ts | 1 + packages/vite/src/plugins/plugin.spec.ts | 66 +++ packages/vite/src/plugins/plugin.ts | 41 +- 10 files changed, 807 insertions(+), 9 deletions(-) create mode 100644 packages/react/router-plugin.ts create mode 100644 packages/react/src/plugins/__snapshots__/router-plugin.spec.ts.snap create mode 100644 packages/react/src/plugins/router-plugin.spec.ts create mode 100644 packages/react/src/plugins/router-plugin.ts diff --git a/packages/nx/src/command-line/init/init-v2.ts b/packages/nx/src/command-line/init/init-v2.ts index 7bd438527c..617f6f6727 100644 --- a/packages/nx/src/command-line/init/init-v2.ts +++ b/packages/nx/src/command-line/init/init-v2.ts @@ -207,6 +207,7 @@ const npmPackageToPluginMap: Record = { 'react-native': '@nx/react-native', '@remix-run/dev': '@nx/remix', '@rsbuild/core': '@nx/rsbuild', + '@react-router/dev': '@nx/react', }; export async function detectPlugins( diff --git a/packages/react/router-plugin.ts b/packages/react/router-plugin.ts new file mode 100644 index 0000000000..7eb8d99b8b --- /dev/null +++ b/packages/react/router-plugin.ts @@ -0,0 +1,4 @@ +export { + createNodesV2, + ReactRouterPluginOptions, +} from './src/plugins/router-plugin'; diff --git a/packages/react/src/generators/init/init.ts b/packages/react/src/generators/init/init.ts index ac267d2fd3..933a84af45 100755 --- a/packages/react/src/generators/init/init.ts +++ b/packages/react/src/generators/init/init.ts @@ -1,6 +1,8 @@ import { addDependenciesToPackageJson, + createProjectGraphAsync, formatFiles, + readNxJson, removeDependenciesFromPackageJson, runTasksInSerial, type GeneratorCallback, @@ -9,6 +11,8 @@ import { import { nxVersion } from '../../utils/versions'; import { InitSchema } from './schema'; 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) { 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) { await formatFiles(tree); } diff --git a/packages/react/src/generators/init/schema.d.ts b/packages/react/src/generators/init/schema.d.ts index 7ff98c42cf..27f8f5afac 100644 --- a/packages/react/src/generators/init/schema.d.ts +++ b/packages/react/src/generators/init/schema.d.ts @@ -2,4 +2,6 @@ export interface InitSchema { skipFormat?: boolean; skipPackageJson?: boolean; keepExistingVersions?: boolean; + updatePackageScripts?: boolean; + addPlugin?: boolean; } diff --git a/packages/react/src/plugins/__snapshots__/router-plugin.spec.ts.snap b/packages/react/src/plugins/__snapshots__/router-plugin.spec.ts.snap new file mode 100644 index 0000000000..71e4a2f8ce --- /dev/null +++ b/packages/react/src/plugins/__snapshots__/router-plugin.spec.ts.snap @@ -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", + ], + }, + }, + }, + }, + }, + ], +] +`; diff --git a/packages/react/src/plugins/router-plugin.spec.ts b/packages/react/src/plugins/router-plugin.spec.ts new file mode 100644 index 0000000000..0ed473df96 --- /dev/null +++ b/packages/react/src/plugins/router-plugin.spec.ts @@ -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, + }); + } +}); diff --git a/packages/react/src/plugins/router-plugin.ts b/packages/react/src/plugins/router-plugin.ts new file mode 100644 index 0000000000..2c0954cc99 --- /dev/null +++ b/packages/react/src/plugins/router-plugin.ts @@ -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 { + return process.env.NX_CACHE_PROJECT_GRAPH !== 'false' && existsSync(cachePath) + ? readJsonFile(cachePath) + : {}; +} + +function writeTargetsToCache( + cachePath: string, + results: Record +) { + writeJsonFile(cachePath, results); +} + +export const createNodesV2: CreateNodesV2 = [ + 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 { + 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 = {}; + + 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; +} diff --git a/packages/react/src/utils/versions.ts b/packages/react/src/utils/versions.ts index 3d04476a83..f7de67d8c8 100755 --- a/packages/react/src/utils/versions.ts +++ b/packages/react/src/utils/versions.ts @@ -32,6 +32,7 @@ export const emotionBabelPlugin = '11.11.0'; export const styledJsxVersion = '5.1.2'; export const reactRouterDomVersion = '6.29.0'; +export const reactRouterVersion = '7.1.5'; export const testingLibraryReactVersion = '16.1.0'; export const testingLibraryDomVersion = '10.4.0'; diff --git a/packages/vite/src/plugins/plugin.spec.ts b/packages/vite/src/plugins/plugin.spec.ts index ddbfdd7407..cfc03ca1c7 100644 --- a/packages/vite/src/plugins/plugin.spec.ts +++ b/packages/vite/src/plugins/plugin.spec.ts @@ -69,6 +69,39 @@ describe('@nx/vite/plugin', () => { 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 () => { // Don't need index.html if we're setting inputs tempFs.removeFileSync('index.html'); @@ -252,6 +285,39 @@ describe('@nx/vite/plugin', () => { 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', () => { diff --git a/packages/vite/src/plugins/plugin.ts b/packages/vite/src/plugins/plugin.ts index 9b3973b65a..15dd835c15 100644 --- a/packages/vite/src/plugins/plugin.ts +++ b/packages/vite/src/plugins/plugin.ts @@ -122,6 +122,15 @@ export const createNodesV2: CreateNodesV2 = [ 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 // 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 @@ -133,6 +142,7 @@ export const createNodesV2: CreateNodesV2 = [ projectRoot, normalizedOptions, tsConfigFiles, + hasReactRouterConfig, isUsingTsSolutionSetup, context )); @@ -185,6 +195,13 @@ export const createNodes: CreateNodes = [ 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 isUsingTsSolutionSetup = _isUsingTsSolutionSetup(); @@ -194,6 +211,7 @@ export const createNodes: CreateNodes = [ projectRoot, normalizedOptions, tsConfigFiles, + hasReactRouterConfig, isUsingTsSolutionSetup, context ); @@ -222,6 +240,7 @@ async function buildViteTargets( projectRoot: string, options: VitePluginOptions, tsConfigFiles: string[], + hasReactRouterConfig: boolean, isUsingTsSolutionSetup: boolean, context: CreateNodesContext ): Promise { @@ -253,6 +272,19 @@ async function buildViteTargets( const targets: Record = {}; + // 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 const hasRemixPlugin = 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( context.workspaceRoot, projectRoot,