rsbuild-plugin-react-router
Version:
React Router plugin for Rsbuild
279 lines (264 loc) • 7.87 kB
text/typescript
import type { RsbuildPluginAPI, TransformHandler } from '@rsbuild/core';
import jsesc from 'jsesc';
import { relative } from 'pathe';
import { PLUGIN_NAME } from './constants.js';
import {
getReactRouterManifestForDev,
type ReactRouterManifestStats,
} from './manifest.js';
import type { RouteTransformExecutor } from './parallel-route-transforms.js';
import type { ReactRouterPerformanceProfiler } from './performance.js';
import { createBundlerRouteExportResolver } from './route-export-resolution.js';
import type { RouteChunkConfig } from './route-chunks.js';
import type { PluginOptions, Route } from './types.js';
import { isSourceMapEnabled } from './warnings/warn-on-client-source-maps.js';
type ReactRouterManifest = Awaited<
ReturnType<typeof getReactRouterManifestForDev>
>;
type RegisterBuildOutputTransformsOptions = {
api: RsbuildPluginAPI;
resolvedServerOutput: 'module' | 'commonjs';
performanceProfiler: ReactRouterPerformanceProfiler;
getLatestServerManifest: () => ReactRouterManifest | null;
getLatestServerManifestByBundleId: (
bundleId: string
) => ReactRouterManifest | undefined;
routes: Record<string, Route>;
pluginOptions: PluginOptions;
getClientStats: () => ReactRouterManifestStats | undefined;
appDirectory: string;
getAssetPrefix: () => string;
routeChunkOptions: Parameters<typeof getReactRouterManifestForDev>[5];
routeTransformExecutor: RouteTransformExecutor;
routeByFilePath: Map<string, Route>;
routeChunkConfig: RouteChunkConfig;
isBuild: boolean;
splitRouteModules: boolean;
ssr: boolean;
isSpaMode: boolean;
rootRoutePath: string;
isDevHmrEnabled?: () => boolean;
};
export const registerBuildOutputTransforms = ({
api,
resolvedServerOutput,
performanceProfiler,
getLatestServerManifest,
getLatestServerManifestByBundleId,
routes,
pluginOptions,
getClientStats,
appDirectory,
getAssetPrefix,
routeChunkOptions,
routeTransformExecutor,
routeByFilePath,
routeChunkConfig,
isBuild,
splitRouteModules,
ssr,
isSpaMode,
rootRoutePath,
isDevHmrEnabled = () => false,
}: RegisterBuildOutputTransformsOptions): void => {
const transformRouteModule = async (args: Parameters<TransformHandler>[0]) =>
performanceProfiler.record(
args.environment?.name,
'route:module',
args.resource,
async () =>
routeTransformExecutor.run({
kind: 'routeModule',
code: args.code,
resource: args.resource,
resourcePath: args.resourcePath,
environmentName: args.environment.name,
sourceMaps: isSourceMapEnabled(
args.environment.config.output.sourceMap
),
ssr,
isBuild,
isSpaMode,
rootRoutePath,
devHmr: isDevHmrEnabled(),
})
);
api.processAssets(
{ stage: 'additional', targets: ['node'] },
({ sources, compilation }) => {
const packageJsonPath = 'package.json';
const source = new sources.RawSource(
`{"type": "${resolvedServerOutput}"}`
);
if (compilation.getAsset(packageJsonPath)) {
compilation.updateAsset(packageJsonPath, source);
} else {
compilation.emitAsset(packageJsonPath, source);
}
}
);
api.transform(
{
test: /virtual\/react-router\/(browser|server)-manifest/,
},
async args =>
performanceProfiler.record(
args.environment?.name,
'manifest:transform',
args.resource,
async () => {
if (args.environment.name === 'web') {
return {
code: `window.__reactRouterManifest = "PLACEHOLDER";`,
};
}
const bundleMatch = args.resource.match(
/virtual\/react-router\/server-manifest(?:-([^?]+))?/
);
const bundleId = bundleMatch?.[1]?.replace(/\.js$/, '');
const latestServerManifest = getLatestServerManifest();
const manifest =
(latestServerManifest
? ((bundleId && getLatestServerManifestByBundleId(bundleId)) ??
latestServerManifest)
: null) ??
(await getReactRouterManifestForDev(
routes,
pluginOptions,
getClientStats(),
appDirectory,
getAssetPrefix(),
routeChunkOptions
));
return {
code: `export default ${jsesc(manifest, { es6: true })};`,
};
}
)
);
api.transform(
{
resourceQuery: /__react-router-build-client-route/,
},
async args =>
performanceProfiler.record(
args.environment?.name,
'route:client-entry',
args.resource,
async () =>
routeTransformExecutor.run({
kind: 'routeClientEntry',
code: args.code,
resourcePath: args.resourcePath,
environmentName: args.environment?.name,
isBuild,
routeChunkConfig,
routeId: routeByFilePath.get(args.resourcePath)?.id,
devHmr: isDevHmrEnabled(),
})
)
);
api.transform(
{
resourceQuery: /route-chunk=/,
environments: ['web'],
},
async args =>
performanceProfiler.record(
args.environment?.name,
'route:chunk',
args.resource,
async () =>
routeTransformExecutor.run({
kind: 'routeChunk',
code: args.code,
resource: args.resource,
resourcePath: args.resourcePath,
isBuild,
routeChunkConfig,
})
)
);
if (isBuild && splitRouteModules) {
api.transform(
{
test: path => routeByFilePath.has(path),
resourceQuery: {
not: /__react-router-build-client-route|react-router-route|route-chunk=/,
},
environments: ['web'],
},
async args =>
performanceProfiler.record(
args.environment?.name,
'route:split-exports',
args.resource,
async () =>
routeTransformExecutor.run({
kind: 'splitRouteExports',
code: args.code,
resourcePath: args.resourcePath,
routeChunkConfig,
})
)
);
}
api.transform(
{
test: /[\\/]\.server[\\/]|\.server(\.[cm]?[jt]sx?)?$/,
environments: ['web'],
},
async args =>
performanceProfiler.record(
args.environment?.name,
'module:server-only-guard',
args.resource,
async () => {
const relativePath = relative(process.cwd(), args.resourcePath);
throw new Error(
`[${PLUGIN_NAME}] Server-only module referenced by client: ${relativePath}`
);
}
)
);
api.transform(
{
test: /[\\/]\.client[\\/]|\.client(\.[cm]?[jt]sx?)?$/,
environments: ['node'],
},
async args =>
performanceProfiler.record(
args.environment?.name,
'module:client-only-stub',
args.resource,
async () => {
return routeTransformExecutor.run({
kind: 'clientOnlyStub',
code: args.code,
resourcePath: args.resourcePath,
resolveExportAllModule:
typeof args.resolve === 'function'
? createBundlerRouteExportResolver(args.resolve)
: undefined,
});
}
)
);
api.transform(
{
resourceQuery: /\?react-router-route/,
order: 'post',
},
transformRouteModule
);
api.transform(
{
test: path => routeByFilePath.has(path),
resourceQuery: {
not: /__react-router-build-client-route|react-router-route|route-chunk=/,
},
order: 'post',
},
transformRouteModule
);
};