import { existsSync, readFileSync } from 'node:fs';
import fsExtra from 'fs-extra';
import type { Config } from './react-router-config.js';
import type { RouteConfigEntry } from '@react-router/dev/routes';
import {
  rspack,
  type RsbuildEntryDescription,
  type RsbuildPlugin,
  type Rspack,
} from '@rsbuild/core';
import { createJiti } from 'jiti';
import { relative, resolve } from 'pathe';

import { getDefaultConcurrency } from './concurrency.js';
import {
  BUILD_CLIENT_ROUTE_QUERY_STRING,
  JS_EXTENSIONS,
  PLUGIN_NAME,
} from './constants.js';
import { guardReactRouterLazyCompilation } from './lazy-compilation.js';
import { createReactRouterDevServerSetup } from './dev-server.js';
import {
  generateWithProps,
  findEntryFile,
  normalizeAssetPrefix,
} from './plugin-utils.js';
import type { PluginOptions } from './types.js';
import {
  generateServerBuild,
  resolveReactRouterServerBuild,
} from './server-utils.js';
import { resolvePrerenderPaths, validatePrerenderConfig } from './prerender.js';
import { runReactRouterPrerenderBuild } from './prerender-build.js';
import {
  resolveReactRouterConfig,
  type ResolvedReactRouterConfig,
} from './react-router-config.js';
import {
  getReactRouterManifestForDev,
  configRoutesToRouteManifest,
  createReactRouterManifestStats,
  type ReactRouterManifestStats,
  type RouteManifestModuleExports,
} from './manifest.js';
import { registerModifyBrowserManifestAssets } from './modify-browser-manifest.js';
import { registerBuildOutputTransforms } from './build-output-transforms.js';
import {
  getRouteChunkEntryName,
  getRouteChunkModuleId,
  routeChunkExportNames,
  type RouteChunkCache,
  type RouteChunkConfig,
} from './route-chunks.js';
import {
  createRouteTransformExecutor,
  shouldParallelizeRouteTransforms,
} from './parallel-route-transforms.js';
import { getRouteRestartMarkerPath, mergeWatchFiles } from './route-watch.js';
import { validateRouteConfig } from './route-config.js';
import {
  getBuildManifest,
  getRoutesByServerBundleId,
} from './build-manifest.js';
import {
  createReactRouterNodeEntries,
  createReactRouterServerBuildPlan,
} from './server-build-plan.js';
import { warnOnClientSourceMaps } from './warnings/warn-on-client-source-maps.js';
import { validatePluginOrderFromConfig } from './validation/validate-plugin-order.js';
import { getSsrExternals } from './ssr-externals.js';
import {
  createReactRouterPerformanceProfiler,
  roundMs,
} from './performance.js';
import { mapVirtualModules } from './virtual-modules.js';
import { createReactRouterDevRuntimeController } from './dev-runtime-controller.js';
import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
import { registerReactRouterTypegen } from './typegen.js';
import { importConfigWithWatchPaths } from './config-imports.js';
import {
  createReactRouterRouteTopology,
  createReactRouterRouteWatchFiles,
  registerReactRouterDevBackgroundResources,
} from './dev-background-resources.js';
import {
  createDevHdrRevisionSignal,
  generateDevHmrRuntimeModule,
  getDevHdrRevisionFilePath,
  isRspackSwcReactRefreshEnabled,
  resolveReactRefreshRuntimePath,
  DEV_HMR_RUNTIME_MODULE_ID,
} from './dev-hmr.js';

export type { Config as ReactRouterRsbuildConfig } from './react-router-config.js';
export { loadReactRouterServerBuild } from './dev-generation.js';
export { resolveReactRouterServerBuild };

const MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES = 4;

export const shouldParallelizeEnvironmentBuilds = ({
  isBuild,
  spareCoreCount = getDefaultConcurrency(),
}: {
  isBuild: boolean;
  spareCoreCount?: number;
}): boolean =>
  !isBuild && spareCoreCount >= MIN_PARALLEL_ENVIRONMENT_BUILD_SPARE_CORES;

type ModuleFederationPluginLike = {
  name?: string;
  _options?: { experiments?: { asyncStartup?: boolean } };
  options?: { experiments?: { asyncStartup?: boolean } };
};

const ensureFederationAsyncStartup = (
  rspackConfig: Rspack.Configuration | undefined
): void => {
  if (!rspackConfig?.plugins?.length) {
    return;
  }

  for (const plugin of rspackConfig.plugins) {
    if (!plugin || typeof plugin !== 'object') {
      continue;
    }
    const pluginName = (plugin as ModuleFederationPluginLike).name;
    if (pluginName !== 'ModuleFederationPlugin') {
      continue;
    }

    const pluginOptions =
      (plugin as ModuleFederationPluginLike)._options ??
      (plugin as ModuleFederationPluginLike).options;
    if (!pluginOptions) {
      continue;
    }

    pluginOptions.experiments = {
      ...pluginOptions.experiments,
      asyncStartup: true,
    };
  }
};

const cssUrlAssetExtensions =
  /\.(?:css|less|sass|scss|styl|stylus|pcss|postcss|sss)$/;
const urlAssetResourceQuery =
  /^(?=.*(?:\?|&)url(?:&|$))(?!.*(?:\?|&)(?:raw|inline)(?:&|$))/;

export const pluginReactRouter = (
  options: PluginOptions = {}
): RsbuildPlugin => ({
  name: PLUGIN_NAME,

  async setup(api) {
    const defaultOptions = {
      customServer: false,
      lazyCompilation: true,
      serverOutput: 'module' as const,
    };

    const pluginOptions = {
      ...defaultOptions,
      ...options,
    };
    const logPerformance = pluginOptions.logPerformance === true;
    const setupStartMs = logPerformance ? performance.now() : 0;
    const performanceProfiler = createReactRouterPerformanceProfiler({
      enabled: logPerformance,
      log: message => api.logger.info(message),
    });
    const nodeExternals = Array.from(
      new Set(['express', ...getSsrExternals(process.cwd())])
    );

    let assetPrefix = '/';

    // Best-effort configuration validation (upstream: validate-plugin-order).
    // Run during config modification phase so we don't rely on `getRsbuildConfig()`
    // being available during `setup()`.
    api.modifyRsbuildConfig({
      order: 'pre',
      handler(config) {
        const issues = validatePluginOrderFromConfig(config);
        for (const issue of issues) {
          if (issue.kind === 'error') {
            throw new Error(issue.message);
          }
          api.logger.warn(issue.message);
        }
        assetPrefix = normalizeAssetPrefix(config.output?.assetPrefix);
        return config;
      },
    });

    api.onBeforeBuild(() => {
      const normalized = api.getNormalizedConfig();
      warnOnClientSourceMaps(normalized, msg => api.logger.warn(msg), 'web');
    });

    const configPath = findEntryFile(resolve('react-router.config'));
    const configExists = existsSync(configPath);
    let configWatchPaths: string | string[] = configExists
      ? configPath
      : JS_EXTENSIONS.map(extension =>
          resolve(`react-router.config${extension}`)
        );
    let reactRouterUserConfig: Config = {};
    if (!configExists) {
      console.warn(
        'No react-router.config found, using default configuration.'
      );
    } else {
      const displayPath = relative(process.cwd(), configPath);
      try {
        const { value: imported, watchPaths } =
          await importConfigWithWatchPaths<Config>(configPath);
        configWatchPaths = watchPaths;
        if (imported === undefined) {
          throw new Error(`${displayPath} must provide a default export`);
        }
        if (typeof imported !== 'object') {
          throw new Error(`${displayPath} must export a config`);
        }
        reactRouterUserConfig = imported;
      } catch (error) {
        throw new Error(`Error loading ${displayPath}: ${error}`);
      }
    }

    const {
      resolved: resolvedConfig,
      presets: configPresets,
      hasConfiguredServerModuleFormat,
    } = await resolveReactRouterConfig(reactRouterUserConfig);

    const {
      appDirectory,
      basename,
      buildDirectory,
      future,
      allowedActionOrigins,
      routeDiscovery: userRouteDiscovery,
      ssr,
      prerender: prerenderConfig,
      serverBuildFile,
      serverModuleFormat,
      splitRouteModules,
      buildEnd,
    } = resolvedConfig;

    registerReactRouterTypegen(api, { appDirectory });

    const hasExplicitServerOutput = Object.prototype.hasOwnProperty.call(
      options,
      'serverOutput'
    );
    let resolvedServerOutput = pluginOptions.serverOutput;
    if (!hasExplicitServerOutput) {
      resolvedServerOutput =
        serverModuleFormat === 'cjs' ? 'commonjs' : 'module';
    }

    if (
      hasExplicitServerOutput &&
      hasConfiguredServerModuleFormat &&
      serverModuleFormat &&
      (resolvedServerOutput === 'commonjs' ? 'cjs' : 'esm') !==
        serverModuleFormat
    ) {
      api.logger.warn(
        `[${PLUGIN_NAME}] Both \`serverOutput\` and \`serverModuleFormat\` are set. ` +
          `Using \`serverOutput=${resolvedServerOutput}\` and ignoring ` +
          `\`serverModuleFormat=${serverModuleFormat}\`.`
      );
    }

    if (serverBuildFile && !serverBuildFile.endsWith('.js')) {
      throw new Error('The `serverBuildFile` config must end in `.js`.');
    }

    if (serverModuleFormat !== 'esm' && serverModuleFormat !== 'cjs') {
      throw new Error(
        'The `serverModuleFormat` config must be "esm" or "cjs".'
      );
    }

    const prerenderConfigError = validatePrerenderConfig(prerenderConfig);
    if (prerenderConfigError) {
      throw new Error(prerenderConfigError);
    }

    // React Router defaults to "lazy" route discovery, but "ssr:false" builds
    // have no runtime server to serve manifest patch requests, so we force
    // `mode:"initial"` in SPA mode to avoid any `/__manifest` fetches.
    let routeDiscovery: Config['routeDiscovery'];
    if (!userRouteDiscovery) {
      routeDiscovery = ssr
        ? ({ mode: 'lazy', manifestPath: '/__manifest' } as const)
        : ({ mode: 'initial' } as const);
    } else if (userRouteDiscovery.mode === 'initial') {
      routeDiscovery = userRouteDiscovery;
    } else if (userRouteDiscovery.mode === 'lazy') {
      if (!ssr) {
        throw new Error(
          'The `routeDiscovery.mode` config cannot be set to "lazy" when setting `ssr:false`'
        );
      }
      const manifestPath = userRouteDiscovery.manifestPath;
      if (manifestPath && !manifestPath.startsWith('/')) {
        throw new Error(
          'The `routeDiscovery.manifestPath` config must be a root-relative pathname beginning with a slash (i.e., "/__manifest")'
        );
      }
      routeDiscovery = userRouteDiscovery;
    }

    (globalThis as any).__reactRouterAppDirectory = resolve(appDirectory);
    const routesPath = findEntryFile(resolve(appDirectory, 'routes'));
    if (!existsSync(routesPath)) {
      throw new Error(
        `Route config file not found at "${relative(
          process.cwd(),
          routesPath
        )}".`
      );
    }

    const jiti = createJiti(process.cwd(), {
      moduleCache: false,
    });
    const importRouteConfig = async (
      importer: Pick<typeof jiti, 'import'>
    ): Promise<RouteConfigEntry[]> => {
      const routeConfigExport = await importer.import<RouteConfigEntry[]>(
        routesPath,
        {
          default: true,
        }
      );
      const routeConfigValue = await routeConfigExport;
      const validation = validateRouteConfig({
        routeConfigFile: relative(process.cwd(), routesPath),
        routeConfig: routeConfigValue,
      });
      if (!validation.valid) {
        throw new Error(validation.message);
      }
      return validation.routeConfig;
    };
    const loadRouteConfig = () => importRouteConfig(jiti);
    const { value: routeConfig, watchPaths: routeConfigWatchPaths } =
      await importConfigWithWatchPaths(routesPath, importRouteConfig);

    const entryClientPath = findEntryFile(
      resolve(appDirectory, 'entry.client')
    );
    const entryServerPath = findEntryFile(
      resolve(appDirectory, 'entry.server')
    );

    const serverAppPath = findEntryFile(
      resolve(appDirectory, '../server/index')
    );
    const hasServerApp = existsSync(serverAppPath);
    const devServerBuildEntryName = hasServerApp
      ? 'static/js/react-router-server-build'
      : 'static/js/app';

    const templateDir = resolve(__dirname, 'templates');
    const templateClientPath = resolve(templateDir, 'entry.client.js');
    const templateServerPath = resolve(templateDir, 'entry.server.js');

    const finalEntryClientPath = existsSync(entryClientPath)
      ? entryClientPath
      : templateClientPath;
    const finalEntryServerPath = existsSync(entryServerPath)
      ? entryServerPath
      : templateServerPath;

    const getRootRoutePath = () => findEntryFile(resolve(appDirectory, 'root'));
    const rootRoutePath = getRootRoutePath();
    // React Router's server build expects route files relative to `appDirectory`
    // so it can resolve them correctly during compilation.
    const rootRouteFile = relative(appDirectory, rootRoutePath);
    const routeTopology = createReactRouterRouteTopology({
      appDirectory,
      rootRouteFile,
      routeConfig,
      loadRouteConfig,
      getRootRoutePath,
    });

    const routes = {
      root: { path: '', id: 'root', file: rootRouteFile },
      ...configRoutesToRouteManifest(appDirectory, routeConfig),
    };

    const resolvedConfigWithRoutes: ResolvedReactRouterConfig = {
      ...resolvedConfig,
      appDirectory: resolve(appDirectory),
      buildDirectory: resolve(buildDirectory),
      routeDiscovery,
      prerender: prerenderConfig,
      routes,
      unstable_routeConfig: routeConfig,
      allowedActionOrigins: allowedActionOrigins ?? false,
    };

    const { buildEnd: _buildEnd, ...resolvedConfigForPreset } =
      resolvedConfigWithRoutes;
    for (const preset of configPresets) {
      await preset.reactRouterConfigResolved?.({
        reactRouterConfig: resolvedConfigForPreset,
      });
    }

    const isBuild = api.context.action === 'build';
    const shouldDependOnWebCompiler = !shouldParallelizeEnvironmentBuilds({
      isBuild,
    });
    const isPrerenderEnabled =
      prerenderConfig !== undefined && prerenderConfig !== false;
    const isSpaMode = !ssr && !isPrerenderEnabled;
    const routeCount = Object.keys(routes).length;
    const routeChunkConfig: RouteChunkConfig = {
      splitRouteModules,
      appDirectory,
      rootRouteFile,
    };
    const routeChunkCache: RouteChunkCache = new Map();
    const routeTransformExecutor = createRouteTransformExecutor({
      parallelRouteTransform:
        pluginOptions.parallelRouteTransform ??
        shouldParallelizeRouteTransforms(routeCount),
      routeChunkCache,
      splitRouteModules: Boolean(splitRouteModules),
      isBuild,
    });
    const routeChunkOptions = {
      splitRouteModules,
      rootRouteFile,
      isBuild,
      cache: routeChunkCache,
    };
    const outputClientPath = resolve(buildDirectory, 'client');
    const assetsBuildDirectory = relative(process.cwd(), outputClientPath);
    const watchDirectory = resolve(appDirectory);
    const routeRestartMarkerPath = getRouteRestartMarkerPath(outputClientPath);
    const routeWatchFiles = createReactRouterRouteWatchFiles({
      configWatchPaths,
      routeConfigWatchPaths,
      routeRestartMarkerPath,
      onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
    });
    const devBackgroundResources = registerReactRouterDevBackgroundResources({
      api,
      isBuild,
      lazyCompilationPrewarm: pluginOptions.unstableLazyCompilationPrewarm,
      routeTransformExecutor,
      routeRestartMarkerPath,
      watchDirectory,
      getRouteTopology: routeTopology.getRouteTopology,
      initialRouteTopology: routeTopology.initialRouteTopology,
      onRouteTopologyChange: pluginOptions.onRouteTopologyChange,
    });

    type ReactRouterManifest = Awaited<
      ReturnType<typeof getReactRouterManifestForDev>
    >;
    let latestBrowserManifest: ReactRouterManifest | null = null;
    let latestBrowserManifestModuleExports: RouteManifestModuleExports = {};
    let latestServerManifest: ReactRouterManifest | null = null;
    const latestServerManifestsByBundleId: Record<string, ReactRouterManifest> =
      {};

    const stageLatestManifests = (
      manifest: ReactRouterManifest,
      sri: ReactRouterManifest['sri'],
      moduleExportsByRouteId: RouteManifestModuleExports,
      compilation: Rspack.Compilation
    ) => {
      performanceProfiler.recordSync(
        'web',
        'manifest:stage',
        'virtual/react-router/browser-manifest',
        () => {
          latestBrowserManifest = manifest;
          devBackgroundResources.setManifest(manifest);
          latestBrowserManifestModuleExports = moduleExportsByRouteId;
          const baseServerManifest = {
            ...manifest,
            sri,
          };
          latestServerManifest = baseServerManifest;
          const manifestsByEntryName: Record<string, ReactRouterManifest> = {
            [devServerBuildEntryName]: baseServerManifest,
          };

          for (const { bundleId, entryName } of serverBundleEntries) {
            const bundleRoutes = routesByServerBundleId[bundleId];
            if (!bundleRoutes) {
              continue;
            }

            const routeIds = new Set(Object.keys(bundleRoutes));
            const filteredRoutes = Object.fromEntries(
              Object.entries(manifest.routes).filter(([routeId]) =>
                routeIds.has(routeId)
              )
            );
            const bundleManifest = {
              ...baseServerManifest,
              routes: filteredRoutes,
            };
            latestServerManifestsByBundleId[bundleId] = bundleManifest;
            manifestsByEntryName[entryName] = bundleManifest;
          }

          if (!isBuild) {
            devRuntime.captureWeb(compilation, manifestsByEntryName);
          }
        }
      );
    };

    const routeByFilePath = new Map(
      Object.values(routes).map(route => [
        resolve(appDirectory, route.file),
        route,
      ])
    );

    const manifestChunkNames = new Set<string>(['entry.client']);
    const webRouteEntries = Object.values(routes).reduce(
      (acc, route) => {
        const entryName = route.file.slice(0, route.file.lastIndexOf('.'));
        const routeFilePath = resolve(appDirectory, route.file);
        manifestChunkNames.add(entryName);
        acc[entryName] = {
          import: `${routeFilePath}${BUILD_CLIENT_ROUTE_QUERY_STRING}`,
          html: false,
        };

        if (isBuild && splitRouteModules && route.id !== 'root') {
          let source: string;
          try {
            source = readFileSync(routeFilePath, 'utf8');
          } catch (error) {
            if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
              return acc;
            }
            throw error;
          }
          for (const exportName of routeChunkExportNames) {
            if (!source.includes(exportName)) {
              continue;
            }
            const chunkEntryName = getRouteChunkEntryName(route.id, exportName);
            manifestChunkNames.add(chunkEntryName);
            acc[chunkEntryName] = {
              import: getRouteChunkModuleId(routeFilePath, exportName),
              html: false,
            };
          }
        }

        return acc;
      },
      {} as Record<string, RsbuildEntryDescription>
    );
    const buildManifest = await getBuildManifest({
      reactRouterConfig: resolvedConfigWithRoutes,
      routes,
      rootDirectory: process.cwd(),
    });
    const routesByServerBundleId = getRoutesByServerBundleId(
      buildManifest,
      routes
    );
    const serverBuildPlan = createReactRouterServerBuildPlan({
      routesByServerBundleId,
      serverBuildFile,
      defaultEntryName: devServerBuildEntryName,
    });
    const { serverBundleEntries } = serverBuildPlan;

    const devHmrRefreshRuntimePath = isBuild
      ? undefined
      : resolveReactRefreshRuntimePath(api.context.rootPath);
    const devHdrSignal = devHmrRefreshRuntimePath
      ? createDevHdrRevisionSignal({
          filePath: getDevHdrRevisionFilePath(api.context.rootPath),
          onError: error =>
            api.logger.debug(
              `[${PLUGIN_NAME}] Failed to signal hot data revalidation: ${error.message}`
            ),
        })
      : undefined;
    let devHmrEnabled = false;

    const devRuntime = createReactRouterDevRuntimeController({
      api,
      isBuild,
      buildPlan: serverBuildPlan,
      onNodeRebuildCommitted: () => devHdrSignal?.bump(),
    });

    let clientStats: ReactRouterManifestStats | undefined;
    api.onAfterEnvironmentCompile(({ stats, environment }) => {
      if (environment.name === 'web') {
        clientStats = createReactRouterManifestStats(
          stats?.compilation,
          manifestChunkNames
        );
      }
      if (pluginOptions.federation && ssr) {
        const serverBuildDir = resolve(buildDirectory, 'server');
        const clientBuildDir = resolve(buildDirectory, 'client');
        if (existsSync(serverBuildDir)) {
          const ssrDir = resolve(clientBuildDir, 'static');
          fsExtra.copySync(serverBuildDir, ssrDir);
        }
      }
      if (logPerformance) {
        performanceProfiler.flush(environment.name, {
          compilerLifecycleMs: roundMs(performance.now() - setupStartMs),
        });
      }
    });

    const prerenderPaths = await resolvePrerenderPaths(
      prerenderConfig,
      ssr,
      routeConfig,
      {
        logWarning: true,
        warn: message => api.logger.warn(message),
      }
    );

    api.onAfterBuild(({ environments }) =>
      runPluginEffect(
        tryPluginPromise(() =>
          runReactRouterPrerenderBuild({
            api,
            hasWebEnvironment: Boolean(environments.web),
            buildDirectory,
            serverBuildFile,
            ssr,
            isPrerenderEnabled,
            prerenderConfig,
            prerenderPaths,
            basename,
            future,
            routes,
            latestBrowserManifest,
            latestBrowserManifestModuleExports,
            clientStats,
            pluginOptions,
            appDirectory,
            assetPrefix,
            routeChunkOptions,
            buildManifest,
            resolvedConfigWithRoutes,
            buildEnd,
          })
        )
      )
    );

    const allowedActionOriginsForBuild =
      allowedActionOrigins === false ? undefined : allowedActionOrigins;

    // Public requests stay bare while Rspack resolves seeded virtual files.
    const createVirtualModulePlugin = (publicPath: string) => {
      const bundleVirtualModules = Object.fromEntries(
        Object.entries(routesByServerBundleId).map(
          ([bundleId, bundleRoutes]) => [
            `virtual/react-router/server-build-${bundleId}`,
            generateServerBuild(bundleRoutes, {
              entryServerPath: finalEntryServerPath,
              assetsBuildDirectory,
              basename,
              appDirectory,
              ssr,
              federation: options.federation,
              future,
              allowedActionOrigins: allowedActionOriginsForBuild,
              prerender: prerenderPaths,
              routeDiscovery,
              publicPath,
              serverManifestId: `virtual/react-router/server-manifest-${bundleId}`,
            }),
          ]
        )
      );
      const bundleManifestModules = Object.fromEntries(
        Object.entries(routesByServerBundleId)
          .filter(
            ([, bundleRoutes]) =>
              bundleRoutes && Object.keys(bundleRoutes).length > 0
          )
          .map(([bundleId]) => [
            `virtual/react-router/server-manifest-${bundleId}`,
            'export default {};',
          ])
      );

      return new rspack.experiments.VirtualModulesPlugin(
        mapVirtualModules({
          'virtual/react-router/browser-manifest': 'export default {};',
          'virtual/react-router/server-manifest': 'export default {};',
          'virtual/react-router/server-build': generateServerBuild(routes, {
            entryServerPath: finalEntryServerPath,
            assetsBuildDirectory,
            basename,
            appDirectory,
            ssr,
            federation: options.federation,
            future,
            allowedActionOrigins: allowedActionOriginsForBuild,
            prerender: prerenderPaths,
            routeDiscovery,
            publicPath,
          }),
          ...bundleVirtualModules,
          ...bundleManifestModules,
          'virtual/react-router/with-props': generateWithProps(),
          ...(devHmrRefreshRuntimePath
            ? {
                [DEV_HMR_RUNTIME_MODULE_ID]: generateDevHmrRuntimeModule({
                  reactRefreshRuntimePath: devHmrRefreshRuntimePath,
                  hdrRevisionFilePath: getDevHdrRevisionFilePath(
                    api.context.rootPath
                  ),
                }),
              }
            : {}),
        })
      );
    };

    api.modifyRsbuildConfig(async (config, { mergeRsbuildConfig }) => {
      assetPrefix = normalizeAssetPrefix(config.output?.assetPrefix);
      const vmodPlugin = createVirtualModulePlugin(assetPrefix);
      const useAsyncNodeChunkLoading =
        options.federation && resolvedServerOutput === 'commonjs';
      let nodeChunkLoading: 'import' | 'async-node' | 'require' = 'require';
      if (resolvedServerOutput === 'module') {
        nodeChunkLoading = 'import';
      } else if (useAsyncNodeChunkLoading) {
        nodeChunkLoading = 'async-node';
      }
      const nodeEntries = createReactRouterNodeEntries({
        hasServerApp,
        isBuild,
        serverAppPath,
        entryServerPath: finalEntryServerPath,
        defaultEntryName: devServerBuildEntryName,
        serverBundleEntries,
      });

      const configuredLazyCompilation = Object.prototype.hasOwnProperty.call(
        options,
        'lazyCompilation'
      )
        ? pluginOptions.lazyCompilation
        : (config.dev?.lazyCompilation ?? pluginOptions.lazyCompilation);
      const guardedLazyCompilation = guardReactRouterLazyCompilation({
        lazyCompilation: configuredLazyCompilation,
        entryClientPath: finalEntryClientPath,
        prewarmReactRouterModules: Boolean(
          pluginOptions.unstableLazyCompilationPrewarm
        ),
      });
      const lazyCompilation =
        guardedLazyCompilation === undefined
          ? {}
          : { lazyCompilation: guardedLazyCompilation };
      const shouldCompactFileSizeReport =
        isBuild &&
        routeCount >= 256 &&
        (config.performance?.printFileSize === undefined ||
          config.performance.printFileSize === true);

      return mergeRsbuildConfig(config, {
        ...(shouldCompactFileSizeReport
          ? {
              performance: {
                printFileSize: {
                  total: true,
                  detail: false,
                  compressed: false,
                },
              },
            }
          : {}),
        output: {
          assetPrefix: config.output?.assetPrefix || '/',
        },
        dev: {
          ...lazyCompilation,
          watchFiles: mergeWatchFiles(config.dev?.watchFiles, routeWatchFiles),
        },
        // React Router's request handler natively supports `ssr:false`
        // builds (it renders the SPA shell for document requests), so the
        // middleware is registered for SPA mode too — without it, dev
        // requests would 404 because no HTML entry exists.
        ...(pluginOptions.customServer
          ? {}
          : {
              server: {
                setup: [
                  createReactRouterDevServerSetup({
                    // Lazy: the dev runtime binding does not exist yet here.
                    loadBuild: () => devRuntime.createBuildLoader()(),
                  }),
                ],
              },
            }),
        tools: {
          rspack: {
            plugins: [vmodPlugin],
          },
        },
        environments: {
          web: {
            ...(resolvedConfigWithRoutes.subResourceIntegrity
              ? {
                  security: {
                    sri: {
                      enable: true,
                    },
                  },
                }
              : {}),
            source: {
              entry: {
                // no query needed when federation is disabled
                'entry.client': finalEntryClientPath,
                'virtual/react-router/browser-manifest': {
                  import: 'virtual/react-router/browser-manifest',
                  html: false,
                },
                ...webRouteEntries,
              },
            },
            output: {
              filename: {
                js: '[name].js',
              },
              distPath: {
                root: outputClientPath,
              },
            },
            tools: {
              rspack: {
                name: 'web',
                module: {
                  rules: [
                    {
                      resourceQuery: urlAssetResourceQuery,
                      exclude: cssUrlAssetExtensions,
                      type: 'asset/resource',
                    },
                  ],
                },
                ...(options.federation
                  ? {
                      output: {
                        chunkLoading: 'import',
                      },
                    }
                  : {}),
                externalsType: 'module',
                output: {
                  chunkFormat: 'module',
                  chunkLoading: 'import',
                  workerChunkLoading: 'import',
                  wasmLoading: 'fetch',
                  library: { type: 'module' },
                  module: true,
                },
                optimization: {
                  avoidEntryIife: true,
                  runtimeChunk: 'single',
                },
              },
            },
          },
          // Always include node environment, even for SPA mode (`ssr:false`),
          // because React Router still needs a server build to prerender the
          // root route into a hydratable `index.html` at build time.
          node: {
            source: {
              entry: nodeEntries,
            },
            output: {
              distPath: {
                root: resolve(buildDirectory, 'server'),
              },
              target: config.environments?.node?.output?.target || 'node',
              filename: {
                js: '[name].js',
              },
            },
            tools: {
              rspack: {
                target: options.federation ? 'async-node' : 'node',
                module: {
                  rules: [
                    {
                      resourceQuery: urlAssetResourceQuery,
                      exclude: cssUrlAssetExtensions,
                      type: 'asset/resource',
                    },
                  ],
                },
                externals: nodeExternals,
                ...(shouldDependOnWebCompiler ? { dependencies: ['web'] } : {}),
                externalsType: resolvedServerOutput,
                output: {
                  chunkFormat: resolvedServerOutput,
                  chunkLoading: nodeChunkLoading,
                  workerChunkLoading: nodeChunkLoading,
                  wasmLoading: 'fetch',
                  module: resolvedServerOutput === 'module',
                },
              },
            },
          },
        },
      });
    });

    api.modifyEnvironmentConfig(
      async (config, { name, mergeEnvironmentConfig }) => {
        if (name !== 'web' && name !== 'node') {
          return config;
        }

        return mergeEnvironmentConfig(config, {
          tools: {
            rspack: rspackConfig => {
              if (pluginOptions.federation) {
                ensureFederationAsyncStartup(rspackConfig);
              }

              if (name === 'web') {
                devHmrEnabled =
                  !isBuild &&
                  devHmrRefreshRuntimePath !== undefined &&
                  config.mode === 'development' &&
                  config.dev?.hmr !== false &&
                  isRspackSwcReactRefreshEnabled(rspackConfig);
                if (devHmrEnabled) {
                  devHdrSignal?.ensure();
                }
              }

              if (name === 'node') {
                const output = rspackConfig.output;
                if (output) {
                  const library = output.library;
                  const libraryOptions =
                    library &&
                    typeof library === 'object' &&
                    !Array.isArray(library)
                      ? library
                      : {};
                  rspackConfig.output = {
                    ...output,
                    library: {
                      ...libraryOptions,
                      type:
                        resolvedServerOutput === 'module'
                          ? 'module'
                          : 'commonjs2',
                    },
                  };
                }
              }

              return rspackConfig;
            },
          },
        });
      }
    );

    registerModifyBrowserManifestAssets(
      api,
      routes,
      pluginOptions,
      appDirectory,
      () => assetPrefix,
      routeChunkOptions,
      {
        subResourceIntegrity: resolvedConfigWithRoutes.subResourceIntegrity,
        future,
        manifestChunkNames,
        onManifest: (manifest, sri, moduleExportsByRouteId, context) =>
          stageLatestManifests(
            manifest,
            sri,
            moduleExportsByRouteId,
            context.compilation
          ),
      }
    );

    registerBuildOutputTransforms({
      api,
      resolvedServerOutput,
      performanceProfiler,
      getLatestServerManifest: () => latestServerManifest,
      getLatestServerManifestByBundleId: bundleId =>
        latestServerManifestsByBundleId[bundleId],
      routes,
      pluginOptions,
      getClientStats: () => clientStats,
      appDirectory,
      getAssetPrefix: () => assetPrefix,
      routeChunkOptions,
      routeTransformExecutor,
      routeByFilePath,
      routeChunkConfig,
      isBuild,
      splitRouteModules: Boolean(splitRouteModules),
      ssr,
      isSpaMode,
      rootRoutePath,
      isDevHmrEnabled: () => devHmrEnabled,
    });
  },
});
