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,