rsbuild-plugin-react-router 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +55 -24
  2. package/dist/451.js +36 -20
  3. package/dist/build-manifest.d.ts +6 -3
  4. package/dist/concurrency.d.ts +2 -1
  5. package/dist/config-imports.d.ts +7 -0
  6. package/dist/dev-background-resources.d.ts +38 -0
  7. package/dist/dev-generation.d.ts +2 -2
  8. package/dist/dev-runtime-artifacts.d.ts +9 -1
  9. package/dist/dev-runtime-compilation.d.ts +17 -2
  10. package/dist/dev-server.d.ts +5 -0
  11. package/dist/effect-runtime.d.ts +18 -0
  12. package/dist/export-utils.d.ts +2 -2
  13. package/dist/index.cjs +11448 -891
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +7950 -758
  16. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  17. package/dist/lazy-compilation.d.ts +2 -1
  18. package/dist/manifest.d.ts +19 -4
  19. package/dist/parallel-route-transforms.d.ts +17 -2
  20. package/dist/prerender-build.d.ts +4 -0
  21. package/dist/prerender.d.ts +0 -1
  22. package/dist/react-router-config.d.ts +8 -5
  23. package/dist/server-build-resolution.d.ts +3 -0
  24. package/dist/ssr-externals.d.ts +1 -0
  25. package/dist/typegen.d.ts +14 -1
  26. package/dist/types.d.ts +15 -6
  27. package/package.json +11 -9
  28. package/src/build-manifest.ts +110 -73
  29. package/src/concurrency.ts +3 -22
  30. package/src/config-imports.ts +38 -0
  31. package/src/dev-background-resources.ts +255 -0
  32. package/src/dev-generation.ts +43 -53
  33. package/src/dev-runtime-artifacts.ts +50 -18
  34. package/src/dev-runtime-compilation.ts +80 -1
  35. package/src/dev-runtime-controller.ts +111 -31
  36. package/src/dev-runtime-session.ts +18 -11
  37. package/src/dev-server.ts +31 -1
  38. package/src/effect-runtime.ts +130 -0
  39. package/src/export-utils.ts +82 -23
  40. package/src/index.ts +118 -153
  41. package/src/lazy-compilation-prewarm.ts +279 -0
  42. package/src/lazy-compilation.ts +12 -5
  43. package/src/manifest.ts +366 -255
  44. package/src/modify-browser-manifest.ts +2 -1
  45. package/src/parallel-route-transforms.ts +195 -69
  46. package/src/prerender-build.ts +125 -61
  47. package/src/prerender.ts +0 -18
  48. package/src/react-router-config.ts +98 -73
  49. package/src/route-artifacts.ts +2 -2
  50. package/src/route-export-resolution.ts +3 -3
  51. package/src/route-watch.ts +119 -84
  52. package/src/server-build-resolution.ts +131 -0
  53. package/src/server-utils.ts +7 -106
  54. package/src/ssr-externals.ts +1 -1
  55. package/src/typegen.ts +162 -33
  56. package/src/types.ts +16 -6
@@ -0,0 +1,25 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import type { ReactRouterManifestForDev } from './manifest.js';
3
+ type LazyCompilationPrewarmConfig = {
4
+ entry: boolean;
5
+ routeLimit: number;
6
+ delayMs: number;
7
+ };
8
+ type LazyCompilationPrewarmController = {
9
+ setServerOrigin(origin: string): void;
10
+ setManifest(manifest: ReactRouterManifestForDev | null): void;
11
+ schedule(): void;
12
+ cancelEffect(): Effect.Effect<void, Error, never>;
13
+ };
14
+ export type RspackLazyCompilationTriggerClient = {
15
+ extractModuleKeys(source: string): string[];
16
+ trigger(origin: string, keys: readonly string[]): Effect.Effect<void, Error, never>;
17
+ };
18
+ export declare const normalizeLazyCompilationPrewarmOptions: (options: boolean | undefined) => LazyCompilationPrewarmConfig | null;
19
+ export declare const collectLazyCompilationPrewarmAssets: (manifest: ReactRouterManifestForDev, config: LazyCompilationPrewarmConfig) => string[];
20
+ export declare const createRspackLazyCompilationTriggerClient: (triggerPrefix?: string) => RspackLazyCompilationTriggerClient;
21
+ export declare const createLazyCompilationPrewarmController: ({ config, onError, }: {
22
+ config: LazyCompilationPrewarmConfig;
23
+ onError: (error: Error) => void;
24
+ }) => LazyCompilationPrewarmController;
25
+ export {};
@@ -1,5 +1,6 @@
1
1
  import type { PluginOptions } from './types.js';
2
- export declare const guardReactRouterLazyCompilation: ({ lazyCompilation, entryClientPath, }: {
2
+ export declare const guardReactRouterLazyCompilation: ({ lazyCompilation, entryClientPath, prewarmReactRouterModules, }: {
3
3
  lazyCompilation: PluginOptions["lazyCompilation"] | undefined;
4
4
  entryClientPath: string;
5
+ prewarmReactRouterModules?: boolean;
5
6
  }) => PluginOptions["lazyCompilation"] | undefined;
@@ -1,6 +1,18 @@
1
1
  import type { Route, PluginOptions, RouteManifestItem } from './types.js';
2
- import type { RouteConfigEntry } from '@react-router/dev/routes';
3
2
  import { type RouteChunkCache } from './route-chunks.js';
3
+ /**
4
+ * Structural equivalent of `RouteConfigEntry` from `@react-router/dev/routes`,
5
+ * inlined so the public declaration files do not reference a devDependency
6
+ * (which would fail to resolve for consumers with `skipLibCheck: false`).
7
+ */
8
+ export interface RouteConfigEntry {
9
+ id?: string;
10
+ path?: string;
11
+ index?: boolean;
12
+ caseSensitive?: boolean;
13
+ file: string;
14
+ children?: RouteConfigEntry[];
15
+ }
4
16
  export declare function configRoutesToRouteManifest(appDirectory: string, routes: RouteConfigEntry[], rootId?: string): Record<string, Route>;
5
17
  export declare function configRoutesToRouteManifestEntries(appDirectory: string, routes: RouteConfigEntry[], rootId?: string): Array<[string, Route]>;
6
18
  type RouteChunkManifestOptions = {
@@ -33,9 +45,12 @@ type ReactRouterManifestStatsChunk = {
33
45
  type ReactRouterManifestStatsEntrypoint = {
34
46
  getFiles?: () => Iterable<string>;
35
47
  };
48
+ type ReactRouterManifestStatsLookup<T> = Iterable<[string, T]> & {
49
+ get?: (name: string) => T | undefined;
50
+ };
36
51
  type ReactRouterManifestStatsCompilation = {
37
- namedChunks: Iterable<[string, ReactRouterManifestStatsChunk]>;
38
- entrypoints?: Iterable<[string, ReactRouterManifestStatsEntrypoint]>;
52
+ namedChunks: ReactRouterManifestStatsLookup<ReactRouterManifestStatsChunk>;
53
+ entrypoints?: ReactRouterManifestStatsLookup<ReactRouterManifestStatsEntrypoint>;
39
54
  };
40
55
  export declare const createReactRouterManifestStats: (compilation: ReactRouterManifestStatsCompilation | undefined, chunkNames?: ReadonlySet<string>) => ReactRouterManifestStats | undefined;
41
56
  export type RouteManifestModuleExports = Record<string, readonly string[]>;
@@ -49,6 +64,6 @@ export declare const getReactRouterManifestPath: ({ version, isBuild, entryModul
49
64
  entryModulePath?: string;
50
65
  }) => string;
51
66
  export declare const getReactRouterManifestChunkNames: (routes: Record<string, Route>, splitRouteModules?: boolean | "enforce") => Set<string>;
52
- export declare function generateReactRouterManifestForDev(routes: Record<string, Route>, _options: PluginOptions, clientStats: ReactRouterManifestStats | undefined, context: string, assetPrefix?: string, routeChunkOptions?: RouteChunkManifestOptions): Promise<ReactRouterManifestGenerationResult>;
67
+ export declare function generateReactRouterManifestForDev(routes: Record<string, Route>, options: PluginOptions, clientStats: ReactRouterManifestStats | undefined, context: string, assetPrefix?: string, routeChunkOptions?: RouteChunkManifestOptions): Promise<ReactRouterManifestGenerationResult>;
53
68
  export declare function getReactRouterManifestForDev(...args: Parameters<typeof generateReactRouterManifestForDev>): Promise<ReactRouterManifestForDev>;
54
69
  export {};
@@ -1,14 +1,29 @@
1
1
  import { type RouteTransformResult, type RouteTransformTask, type RouteTransformTaskOptions } from './route-transform-tasks.js';
2
2
  import type { PluginOptions } from './types.js';
3
+ import type { WorkerRequest, WorkerResponse } from './parallel-route-transform-protocol.js';
3
4
  export type ParallelRouteTransformConfig = NonNullable<PluginOptions['parallelRouteTransform']> extends infer Config ? Exclude<Config, false> : never;
4
5
  export type RouteTransformExecutorOptions = RouteTransformTaskOptions & {
5
6
  parallelRouteTransform?: PluginOptions['parallelRouteTransform'];
6
7
  splitRouteModules?: boolean;
8
+ isBuild?: boolean;
7
9
  };
8
10
  export type RouteTransformExecutor = {
9
11
  run: (task: RouteTransformTask) => Promise<RouteTransformResult>;
12
+ prewarm: () => void;
10
13
  close: () => Promise<void>;
11
14
  };
12
- export declare const getDefaultWorkerCount: (cpuCount?: number) => number;
15
+ type RouteTransformWorker = {
16
+ on(event: 'message', handler: (response: WorkerResponse) => void): unknown;
17
+ on(event: 'error', handler: (error: Error) => void): unknown;
18
+ on(event: 'exit', handler: (code: number) => void): unknown;
19
+ postMessage(message: WorkerRequest): void;
20
+ terminate(): Promise<number> | number;
21
+ };
22
+ type RouteTransformWorkerFactory = () => RouteTransformWorker;
23
+ export declare const getDefaultWorkerCount: (cpuCount?: number, { isBuild }?: {
24
+ isBuild?: boolean;
25
+ }) => number;
13
26
  export declare const shouldParallelizeRouteTransforms: (routeCount: number) => boolean;
14
- export declare const createRouteTransformExecutor: ({ parallelRouteTransform, routeChunkCache, splitRouteModules, }?: RouteTransformExecutorOptions) => RouteTransformExecutor;
27
+ export declare const createRouteTransformExecutor: ({ parallelRouteTransform, routeChunkCache, splitRouteModules, isBuild, }?: RouteTransformExecutorOptions) => RouteTransformExecutor;
28
+ export declare const createRouteTransformExecutorForTesting: (options: RouteTransformExecutorOptions, createWorker: RouteTransformWorkerFactory) => RouteTransformExecutor;
29
+ export {};
@@ -1,3 +1,4 @@
1
+ import * as Effect from 'effect/Effect';
1
2
  import type { RsbuildPluginAPI } from '@rsbuild/core';
2
3
  import { getBuildManifest } from './build-manifest.js';
3
4
  import { getReactRouterManifestForDev, type ReactRouterManifestStats, type RouteManifestModuleExports } from './manifest.js';
@@ -28,5 +29,8 @@ type RunReactRouterPrerenderBuildOptions = {
28
29
  resolvedConfigWithRoutes: ResolvedReactRouterConfig;
29
30
  buildEnd: Config['buildEnd'];
30
31
  };
32
+ export declare const createBuildRequestEffect: <T>(input: string | URL, init: RequestInit | undefined, handle: (request: Request) => Promise<T>) => Effect.Effect<T, Error, never>;
33
+ export declare const withBuildRequest: <T>(input: string | URL, init: RequestInit | undefined, handle: (request: Request) => Promise<T>) => Promise<T>;
34
+ export declare const createBoundedPrerenderTasksEffect: (prerenderPaths: string[], concurrency: number, renderPath: (path: string) => Effect.Effect<void, Error, never>) => Effect.Effect<void, Error, never>;
31
35
  export declare const runReactRouterPrerenderBuild: (options: RunReactRouterPrerenderBuildOptions) => Promise<void>;
32
36
  export {};
@@ -33,7 +33,6 @@ type SsrFalsePrerenderExportOptions = {
33
33
  };
34
34
  export declare const createPrerenderRoutes: (manifest: Record<string, any>, parentId?: string, grouped?: Record<string, any[]>) => MatchRouteObject[];
35
35
  export declare const normalizePrerenderMatchPath: (path: string) => string;
36
- export declare const withBuildRequest: <T>(input: string | URL, init: RequestInit | undefined, handle: (request: Request) => Promise<T>) => Promise<T>;
37
36
  export declare const getSsrFalsePrerenderExportErrors: ({ routes, manifestRoutes, routeExports, prerenderPaths, }: SsrFalsePrerenderExportOptions) => string[];
38
37
  export declare const getStaticPrerenderPaths: (routes: RouteConfigEntry[]) => StaticPrerenderPaths;
39
38
  export declare const resolvePrerenderPaths: (prerender: PrerenderConfig, ssr: boolean, routes: RouteConfigEntry[], options?: PrerenderResolveOptions) => Promise<string[]>;
@@ -1,6 +1,7 @@
1
1
  import type { BuildManifest as ReactRouterBuildManifest, Config as ReactRouterConfig } from '@react-router/dev/config';
2
2
  import type { NormalizedConfig } from '@rsbuild/core';
3
3
  import type { RouteConfigEntry } from '@react-router/dev/routes';
4
+ import * as Effect from 'effect/Effect';
4
5
  export type BuildEndHook = {
5
6
  bivarianceHack(args: {
6
7
  buildManifest: ReactRouterBuildManifest | undefined;
@@ -32,6 +33,11 @@ type RouteManifestEntry = {
32
33
  file: string;
33
34
  };
34
35
  type RouteManifest = Record<string, RouteManifestEntry>;
36
+ type ResolveReactRouterConfigResult = {
37
+ resolved: ResolvedReactRouterConfig;
38
+ presets: NonNullable<Config['presets']>;
39
+ hasConfiguredServerModuleFormat: boolean;
40
+ };
35
41
  export type ResolvedReactRouterConfig = Readonly<{
36
42
  appDirectory: string;
37
43
  basename: string;
@@ -50,9 +56,6 @@ export type ResolvedReactRouterConfig = Readonly<{
50
56
  allowedActionOrigins: string[] | false;
51
57
  unstable_routeConfig: RouteConfigEntry[];
52
58
  }>;
53
- export declare const resolveReactRouterConfig: (reactRouterUserConfig: Config) => Promise<{
54
- resolved: ResolvedReactRouterConfig;
55
- presets: NonNullable<Config["presets"]>;
56
- hasConfiguredServerModuleFormat: boolean;
57
- }>;
59
+ export declare const resolveReactRouterConfigEffect: (reactRouterUserConfig: Config) => Effect.Effect<ResolveReactRouterConfigResult, Error, never>;
60
+ export declare const resolveReactRouterConfig: (reactRouterUserConfig: Config) => Promise<ResolveReactRouterConfigResult>;
58
61
  export {};
@@ -0,0 +1,3 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import type { ServerBuild } from 'react-router';
3
+ export declare function resolveServerBuildModuleEffect(buildModule: unknown, source: string): Effect.Effect<ServerBuild, Error, never>;
@@ -1 +1,2 @@
1
+ export declare function resolvePackageJson(name: string, rootDirectory: string): string | null;
1
2
  export declare function getSsrExternals(rootDirectory: string): string[];
package/dist/typegen.d.ts CHANGED
@@ -1,2 +1,15 @@
1
1
  import type { RsbuildPluginAPI } from '@rsbuild/core';
2
- export declare const registerReactRouterTypegen: (api: RsbuildPluginAPI) => void;
2
+ type Execa = typeof import('execa').execa;
3
+ type LoadExeca = () => Promise<Execa>;
4
+ export type ReactRouterTypegenRunner = {
5
+ startWatch(): Promise<void>;
6
+ closeWatch(): Promise<void>;
7
+ runBuild(): Promise<void>;
8
+ };
9
+ export declare const createReactRouterTypegenRunner: (loadExeca?: LoadExeca, appDirectory?: string) => ReactRouterTypegenRunner;
10
+ export declare const registerReactRouterTypegen: (api: RsbuildPluginAPI, { runner, devWatchDelayMs, appDirectory, }?: {
11
+ runner?: ReactRouterTypegenRunner;
12
+ devWatchDelayMs?: number;
13
+ appDirectory?: string;
14
+ }) => void;
15
+ export {};
package/dist/types.d.ts CHANGED
@@ -17,8 +17,8 @@ export type PluginOptions = {
17
17
  customServer?: boolean;
18
18
  /**
19
19
  * The output format for server builds.
20
- * When set to "module", no package.json will be emitted.
21
- * @default "module"
20
+ * When omitted, React Router's `serverModuleFormat` selects the emitted
21
+ * Rsbuild format (`"esm"` -> `"module"`, `"cjs"` -> `"commonjs"`).
22
22
  */
23
23
  serverOutput?: 'module' | 'commonjs';
24
24
  /**
@@ -26,14 +26,23 @@ export type PluginOptions = {
26
26
  */
27
27
  federation?: boolean;
28
28
  /**
29
- * Opt in to Rsbuild's dev-only lazy compilation behavior.
29
+ * Rsbuild dev-only lazy compilation behavior.
30
30
  *
31
- * React Router hydration modules remain eager so initial dev requests can
32
- * load the browser manifest and route modules without lazy proxy delays.
31
+ * React Router's browser manifest remains eager so initial dev requests can
32
+ * discover browser assets without lazy proxy delays.
33
33
  *
34
- * @default undefined
34
+ * Pass `false` to disable.
35
+ * @default true
35
36
  */
36
37
  lazyCompilation?: NonNullable<RsbuildConfig['dev']>['lazyCompilation'];
38
+ /**
39
+ * Prewarm Rspack lazy-compilation proxy modules after dev compiles.
40
+ * This depends on Rspack's generated lazy-compilation client shape and should
41
+ * be treated as experimental.
42
+ *
43
+ * @default false
44
+ */
45
+ unstableLazyCompilationPrewarm?: boolean;
37
46
  /**
38
47
  * Emit structured React Router plugin timing logs.
39
48
  * @default false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rsbuild-plugin-react-router",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "React Router plugin for Rsbuild",
5
5
  "repository": {
6
6
  "type": "git",
@@ -78,6 +78,7 @@
78
78
  "@types/node": "^25.0.10",
79
79
  "@types/react": "^19.2.10",
80
80
  "@types/react-dom": "^19.2.3",
81
+ "effect": "^3.21.4",
81
82
  "es-module-lexer": "1.7.0",
82
83
  "kill-port": "^2.0.1",
83
84
  "pkg-pr-new": "^0.0.75",
@@ -100,13 +101,14 @@
100
101
  },
101
102
  "scripts": {
102
103
  "build": "rslib build",
103
- "bench": "node scripts/bench-builds.mjs",
104
- "bench:ci-report": "node scripts/report-benchmark-ci.mjs",
105
- "bench:compare": "node scripts/compare-benchmarks.mjs",
106
- "bench:smoke": "node scripts/bench-builds.mjs --profile smoke --iterations 1 --warmup 0 --format both --out .benchmark/results/smoke",
107
- "bench:baseline": "node scripts/bench-builds.mjs --profile default --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/baseline",
108
- "bench:full": "node scripts/bench-builds.mjs --profile full --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/full",
109
- "bench:large": "node scripts/bench-builds.mjs --profile large --iterations 1 --warmup 0 --clean cold --format both --out .benchmark/results/large",
104
+ "bench": "node scripts/bench-builds.mts",
105
+ "bench:ci-report": "node scripts/report-benchmark-ci.mts",
106
+ "bench:compare": "node scripts/compare-benchmarks.mts",
107
+ "bench:smoke": "node scripts/bench-builds.mts --profile smoke --iterations 1 --warmup 0 --format both --out .benchmark/results/smoke",
108
+ "bench:baseline": "node scripts/bench-builds.mts --profile default --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/baseline",
109
+ "bench:full": "node scripts/bench-builds.mts --profile full --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/full",
110
+ "bench:large": "node scripts/bench-builds.mts --profile large --iterations 1 --warmup 0 --clean cold --format both --out .benchmark/results/large",
111
+ "bench:synthetic-app": "node scripts/bench-synthetic-app.mjs",
110
112
  "e2e": "pnpm build && pnpm test:package-interop && pnpm --filter './examples/{default-template,spa-mode,prerender,custom-node-server,cloudflare,client-only}' test:e2e",
111
113
  "dev": "rslib build --watch",
112
114
  "test": "rstest run",
@@ -114,7 +116,7 @@
114
116
  "test:coverage": "rstest run --coverage",
115
117
  "test:core": "rstest run -c ./rstest.config.ts",
116
118
  "test:core:watch": "rstest watch -c ./rstest.config.ts",
117
- "test:package-interop": "node scripts/test-package-interop.mjs",
119
+ "test:package-interop": "node scripts/test-package-interop.mts",
118
120
  "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
119
121
  "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"",
120
122
  "changeset": "changeset",
@@ -1,4 +1,7 @@
1
1
  import { relative, resolve } from 'pathe';
2
+ import * as Effect from 'effect/Effect';
3
+ import { getCappedPluginConcurrency } from './concurrency.js';
4
+ import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
2
5
  import type { Config } from './react-router-config.js';
3
6
  import type { Route } from './types.js';
4
7
 
@@ -57,11 +60,26 @@ const configRouteToBranchRoute = (route: Route) => ({
57
60
  index: route.index,
58
61
  });
59
62
 
60
- export const getBuildManifest = async ({
61
- reactRouterConfig,
62
- routes,
63
- rootDirectory,
64
- }: {
63
+ const validateServerBundleId = (
64
+ serverBundleId: string,
65
+ future: Required<Config>['future']
66
+ ): Error | undefined => {
67
+ if (future?.v8_viteEnvironmentApi) {
68
+ return /^[a-zA-Z0-9_]+$/.test(serverBundleId)
69
+ ? undefined
70
+ : new Error(
71
+ 'The "serverBundles" function must only return strings containing alphanumeric characters and underscores.'
72
+ );
73
+ }
74
+
75
+ return /^[a-zA-Z0-9-_]+$/.test(serverBundleId)
76
+ ? undefined
77
+ : new Error(
78
+ 'The "serverBundles" function must only return strings containing alphanumeric characters, hyphens and underscores.'
79
+ );
80
+ };
81
+
82
+ type GetBuildManifestOptions = {
65
83
  reactRouterConfig: Required<
66
84
  Pick<
67
85
  Config,
@@ -71,80 +89,99 @@ export const getBuildManifest = async ({
71
89
  Pick<Config, 'serverBundles'>;
72
90
  routes: Record<string, Route>;
73
91
  rootDirectory: string;
74
- }): Promise<BuildManifest | undefined> => {
75
- const {
76
- serverBundles,
77
- appDirectory,
78
- buildDirectory,
79
- serverBuildFile,
80
- future,
81
- } = reactRouterConfig;
82
-
83
- if (!serverBundles) {
84
- return { routes };
85
- }
92
+ };
86
93
 
87
- const rootRelativeRoutes = Object.fromEntries(
88
- Object.entries(routes).map(([id, route]) => {
89
- const filePath = resolve(appDirectory, route.file);
90
- return [
91
- id,
92
- { ...route, file: normalizePath(relative(rootDirectory, filePath)) },
93
- ];
94
- })
95
- );
94
+ export const getBuildManifestEffect = ({
95
+ reactRouterConfig,
96
+ routes,
97
+ rootDirectory,
98
+ }: GetBuildManifestOptions): Effect.Effect<
99
+ BuildManifest | undefined,
100
+ Error,
101
+ never
102
+ > =>
103
+ Effect.gen(function* () {
104
+ const {
105
+ serverBundles,
106
+ appDirectory,
107
+ buildDirectory,
108
+ serverBuildFile,
109
+ future,
110
+ } = reactRouterConfig;
111
+
112
+ if (!serverBundles) {
113
+ return { routes };
114
+ }
96
115
 
97
- const serverBuildDirectory = resolve(buildDirectory, 'server');
98
-
99
- const buildManifest: BuildManifest = {
100
- routes: rootRelativeRoutes,
101
- serverBundles: {},
102
- routeIdToServerBundleId: {},
103
- };
104
-
105
- await Promise.all(
106
- getAddressableRoutes(routes).map(async route => {
107
- const branch = getRouteBranch(routes, route.id);
108
- const serverBundleId = await serverBundles({
109
- branch: branch.map(branchRoute =>
110
- configRouteToBranchRoute({
111
- ...branchRoute,
112
- file: resolve(appDirectory, branchRoute.file),
113
- })
114
- ),
115
- });
116
-
117
- if (typeof serverBundleId !== 'string') {
118
- throw new Error('The "serverBundles" function must return a string');
119
- }
116
+ const rootRelativeRoutes = Object.fromEntries(
117
+ Object.entries(routes).map(([id, route]) => {
118
+ const filePath = resolve(appDirectory, route.file);
119
+ return [
120
+ id,
121
+ { ...route, file: normalizePath(relative(rootDirectory, filePath)) },
122
+ ];
123
+ })
124
+ );
125
+
126
+ const serverBuildDirectory = resolve(buildDirectory, 'server');
127
+
128
+ const buildManifest: BuildManifest = {
129
+ routes: rootRelativeRoutes,
130
+ serverBundles: {},
131
+ routeIdToServerBundleId: {},
132
+ };
120
133
 
121
- if (future?.v8_viteEnvironmentApi) {
122
- if (!/^[a-zA-Z0-9_]+$/.test(serverBundleId)) {
123
- throw new Error(
124
- 'The "serverBundles" function must only return strings containing alphanumeric characters and underscores.'
134
+ yield* Effect.forEach(
135
+ getAddressableRoutes(routes),
136
+ route =>
137
+ Effect.gen(function* () {
138
+ const branch = getRouteBranch(routes, route.id);
139
+ const serverBundleId = yield* tryPluginPromise(() =>
140
+ serverBundles({
141
+ branch: branch.map(branchRoute =>
142
+ configRouteToBranchRoute({
143
+ ...branchRoute,
144
+ file: resolve(appDirectory, branchRoute.file),
145
+ })
146
+ ),
147
+ })
125
148
  );
126
- }
127
- } else if (!/^[a-zA-Z0-9-_]+$/.test(serverBundleId)) {
128
- throw new Error(
129
- 'The "serverBundles" function must only return strings containing alphanumeric characters, hyphens and underscores.'
130
- );
131
- }
132
149
 
133
- buildManifest.routeIdToServerBundleId![route.id] = serverBundleId;
134
- buildManifest.serverBundles![serverBundleId] ??= {
135
- id: serverBundleId,
136
- file: normalizePath(
137
- relative(
138
- rootDirectory,
139
- resolve(serverBuildDirectory, serverBundleId, serverBuildFile)
140
- )
141
- ),
142
- };
143
- })
144
- );
150
+ if (typeof serverBundleId !== 'string') {
151
+ return yield* Effect.fail(
152
+ new Error('The "serverBundles" function must return a string')
153
+ );
154
+ }
145
155
 
146
- return buildManifest;
147
- };
156
+ const validationError = validateServerBundleId(
157
+ serverBundleId,
158
+ future
159
+ );
160
+ if (validationError) {
161
+ return yield* Effect.fail(validationError);
162
+ }
163
+
164
+ buildManifest.routeIdToServerBundleId![route.id] = serverBundleId;
165
+ buildManifest.serverBundles![serverBundleId] ??= {
166
+ id: serverBundleId,
167
+ file: normalizePath(
168
+ relative(
169
+ rootDirectory,
170
+ resolve(serverBuildDirectory, serverBundleId, serverBuildFile)
171
+ )
172
+ ),
173
+ };
174
+ }),
175
+ { concurrency: getCappedPluginConcurrency(), discard: true }
176
+ );
177
+
178
+ return buildManifest;
179
+ });
180
+
181
+ export const getBuildManifest = (
182
+ options: GetBuildManifestOptions
183
+ ): Promise<BuildManifest | undefined> =>
184
+ runPluginEffect(getBuildManifestEffect(options));
148
185
 
149
186
  export const getRoutesByServerBundleId = (
150
187
  buildManifest: BuildManifest | undefined,
@@ -2,7 +2,7 @@ import { availableParallelism, cpus } from 'node:os';
2
2
 
3
3
  const DEFAULT_RESERVED_CORES = 2;
4
4
 
5
- const getAvailableCpuCount = (): number =>
5
+ export const getAvailableCpuCount = (): number =>
6
6
  typeof availableParallelism === 'function'
7
7
  ? availableParallelism()
8
8
  : cpus().length;
@@ -11,24 +11,5 @@ export const getDefaultConcurrency = (
11
11
  cpuCount: number = getAvailableCpuCount()
12
12
  ): number => Math.max(0, Math.floor(cpuCount) - DEFAULT_RESERVED_CORES);
13
13
 
14
- export const mapWithConcurrency = async <Item, Result>(
15
- items: readonly Item[],
16
- concurrency: number,
17
- worker: (item: Item, index: number) => Promise<Result>
18
- ): Promise<Result[]> => {
19
- const results = new Array<Result>(items.length);
20
- let nextIndex = 0;
21
- const workerCount = Math.max(1, Math.min(concurrency, items.length));
22
- await Promise.all(
23
- Array.from({ length: workerCount }, async () => {
24
- while (true) {
25
- const index = nextIndex++;
26
- if (index >= items.length) {
27
- return;
28
- }
29
- results[index] = await worker(items[index], index);
30
- }
31
- })
32
- );
33
- return results;
34
- };
14
+ export const getCappedPluginConcurrency = (cap = 16): number =>
15
+ Math.max(1, Math.min(cap, getDefaultConcurrency() || 1));
@@ -1,6 +1,9 @@
1
1
  import type { ModuleCache } from 'jiti';
2
+ import { createJiti } from 'jiti';
2
3
  import { resolve } from 'pathe';
3
4
 
5
+ type ConfigImporter = Pick<ReturnType<typeof createJiti>, 'import'>;
6
+
4
7
  const normalizePath = (filePath: string): string => resolve(filePath);
5
8
 
6
9
  const isNodeModulePath = (filePath: string): boolean =>
@@ -43,3 +46,38 @@ export const clearConfigImportCache = (
43
46
  }
44
47
  }
45
48
  };
49
+
50
+ export const importConfigWithWatchPaths = async <T>(
51
+ configPath: string,
52
+ load: (importer: ConfigImporter) => PromiseLike<T> | T = async importer =>
53
+ importer.import<T>(configPath, { default: true })
54
+ ): Promise<{ value: Awaited<T>; watchPaths: string | string[] }> => {
55
+ const jiti = createJiti(process.cwd(), {
56
+ moduleCache: true,
57
+ });
58
+ const previousCacheKeys = new Set(Object.keys(jiti.cache));
59
+ let importPaths: string[] = [];
60
+
61
+ try {
62
+ const value = await load(jiti);
63
+ importPaths = collectConfigImportWatchPaths(
64
+ configPath,
65
+ jiti.cache,
66
+ previousCacheKeys
67
+ );
68
+ return {
69
+ value,
70
+ watchPaths:
71
+ importPaths.length > 0 ? [configPath, ...importPaths] : configPath,
72
+ };
73
+ } finally {
74
+ if (importPaths.length === 0) {
75
+ importPaths = collectConfigImportWatchPaths(
76
+ configPath,
77
+ jiti.cache,
78
+ previousCacheKeys
79
+ );
80
+ }
81
+ clearConfigImportCache(jiti.cache, [configPath, ...importPaths]);
82
+ }
83
+ };