rsbuild-plugin-react-router 0.2.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.
- package/README.md +213 -201
- package/dist/451.js +1103 -0
- package/dist/bounded-cache.d.ts +1 -0
- package/dist/build-manifest.d.ts +7 -4
- package/dist/build-output-transforms.d.ts +30 -0
- package/dist/concurrency.d.ts +3 -0
- package/dist/config-imports.d.ts +10 -0
- package/dist/constants.d.ts +3 -0
- package/dist/dev-background-resources.d.ts +38 -0
- package/dist/dev-generation.d.ts +25 -0
- package/dist/dev-runtime-artifacts.d.ts +47 -0
- package/dist/dev-runtime-compilation.d.ts +47 -0
- package/dist/dev-runtime-controller.d.ts +14 -0
- package/dist/dev-runtime-session.d.ts +34 -0
- package/dist/dev-server.d.ts +16 -2
- package/dist/effect-runtime.d.ts +18 -0
- package/dist/export-utils.d.ts +15 -7
- package/dist/index.cjs +13775 -1412
- package/dist/index.d.ts +8 -1
- package/dist/index.js +9429 -1501
- package/dist/lazy-compilation-prewarm.d.ts +25 -0
- package/dist/lazy-compilation.d.ts +6 -0
- package/dist/manifest.d.ts +48 -10
- package/dist/modify-browser-manifest.d.ts +30 -13
- package/dist/parallel-route-transform-protocol.d.ts +25 -0
- package/dist/parallel-route-transform-worker.d.ts +1 -0
- package/dist/parallel-route-transform-worker.js +38 -0
- package/dist/parallel-route-transforms.d.ts +29 -0
- package/dist/performance.d.ts +26 -0
- package/dist/plugin-utils.d.ts +2 -12
- package/dist/prerender-build.d.ts +36 -0
- package/dist/prerender.d.ts +27 -2
- package/dist/react-router-config.d.ts +15 -5
- package/dist/route-artifacts.d.ts +33 -0
- package/dist/route-ast.d.ts +21 -0
- package/dist/route-chunks.d.ts +9 -1
- package/dist/route-component-transform.d.ts +5 -0
- package/dist/route-export-pruning.d.ts +6 -0
- package/dist/route-export-resolution.d.ts +5 -0
- package/dist/route-transform-tasks.d.ts +47 -0
- package/dist/route-watch.d.ts +27 -0
- package/dist/server-build-plan.d.ts +22 -0
- package/dist/server-build-resolution.d.ts +3 -0
- package/dist/server-utils.d.ts +3 -2
- package/dist/ssr-externals.d.ts +1 -0
- package/dist/templates/entry.server.cjs +3 -3
- package/dist/templates/entry.server.js +3 -3
- package/dist/typegen.d.ts +15 -0
- package/dist/types.d.ts +41 -14
- package/dist/virtual-modules.d.ts +2 -0
- package/dist/warnings/warn-on-client-source-maps.d.ts +1 -0
- package/dist/yuku.d.ts +15 -0
- package/package.json +24 -19
- package/src/bounded-cache.ts +18 -0
- package/src/build-manifest.ts +205 -0
- package/src/build-output-transforms.ts +273 -0
- package/src/concurrency.ts +15 -0
- package/src/config-imports.ts +83 -0
- package/src/constants.ts +91 -0
- package/src/dev-background-resources.ts +255 -0
- package/src/dev-generation.ts +690 -0
- package/src/dev-runtime-artifacts.ts +239 -0
- package/src/dev-runtime-compilation.ts +171 -0
- package/src/dev-runtime-controller.ts +542 -0
- package/src/dev-runtime-session.ts +191 -0
- package/src/dev-server.ts +88 -0
- package/src/effect-runtime.ts +130 -0
- package/src/export-utils.ts +278 -0
- package/src/index.ts +990 -0
- package/src/lazy-compilation-prewarm.ts +279 -0
- package/src/lazy-compilation.ts +101 -0
- package/src/manifest.ts +591 -0
- package/src/modify-browser-manifest.ts +246 -0
- package/src/parallel-route-transform-protocol.ts +35 -0
- package/src/parallel-route-transform-worker.ts +82 -0
- package/src/parallel-route-transforms.ts +458 -0
- package/src/performance.ts +254 -0
- package/src/plugin-utils.ts +82 -0
- package/src/prerender-build.ts +687 -0
- package/src/prerender.ts +349 -0
- package/src/react-router-config.ts +245 -0
- package/src/route-artifacts.ts +155 -0
- package/src/route-ast.ts +163 -0
- package/src/route-chunks.ts +857 -0
- package/src/route-component-transform.ts +314 -0
- package/src/route-config.ts +106 -0
- package/src/route-export-pruning.ts +668 -0
- package/src/route-export-resolution.ts +329 -0
- package/src/route-transform-tasks.ts +249 -0
- package/src/route-watch.ts +357 -0
- package/src/server-build-plan.ts +91 -0
- package/src/server-build-resolution.ts +131 -0
- package/src/server-utils.ts +111 -0
- package/src/ssr-externals.ts +59 -0
- package/src/templates/context.ts +12 -0
- package/src/templates/entry.client.tsx +12 -0
- package/src/templates/entry.server.tsx +76 -0
- package/src/typegen.ts +178 -0
- package/src/types.ts +92 -0
- package/src/validation/validate-plugin-order.ts +76 -0
- package/src/virtual-modules.ts +30 -0
- package/src/warnings/warn-on-client-source-maps.ts +96 -0
- package/src/yuku.ts +67 -0
- package/dist/0~rslib-runtime.js +0 -16
- package/dist/babel.d.ts +0 -8
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { RsbuildDevServer } from '@rsbuild/core';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import { PLUGIN_NAME } from './constants.js';
|
|
4
|
+
import type { ReactRouterDevRuntime } from './dev-generation.js';
|
|
5
|
+
import type { DevCompilerPair } from './dev-runtime-compilation.js';
|
|
6
|
+
import {
|
|
7
|
+
runPluginEffect,
|
|
8
|
+
tryPluginPromise,
|
|
9
|
+
tryPluginSync,
|
|
10
|
+
} from './effect-runtime.js';
|
|
11
|
+
|
|
12
|
+
export type RuntimeBinding = {
|
|
13
|
+
id: number;
|
|
14
|
+
server: RsbuildDevServer;
|
|
15
|
+
runtime: ReactRouterDevRuntime;
|
|
16
|
+
compilers?: DevCompilerPair;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type CloseOutcome = { ok: true } | { ok: false; cause: unknown };
|
|
20
|
+
|
|
21
|
+
type CloseObservation = {
|
|
22
|
+
binding?: RuntimeBinding;
|
|
23
|
+
promise?: Promise<void>;
|
|
24
|
+
outcome?: CloseOutcome;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ControllerState =
|
|
28
|
+
| { status: 'idle' }
|
|
29
|
+
| { status: 'active'; binding: RuntimeBinding }
|
|
30
|
+
| { status: 'closing'; binding: RuntimeBinding }
|
|
31
|
+
| { status: 'terminal'; error: Error };
|
|
32
|
+
|
|
33
|
+
type CloseBinding = (binding: RuntimeBinding, error?: Error) => void;
|
|
34
|
+
|
|
35
|
+
export type DevRuntimeSessionManager = {
|
|
36
|
+
getState(): ControllerState;
|
|
37
|
+
getActiveBinding(): RuntimeBinding | undefined;
|
|
38
|
+
observeClose(server: RsbuildDevServer): void;
|
|
39
|
+
assertCanStart(): void;
|
|
40
|
+
createBinding(
|
|
41
|
+
server: RsbuildDevServer,
|
|
42
|
+
runtime: ReactRouterDevRuntime
|
|
43
|
+
): RuntimeBinding;
|
|
44
|
+
bindCloseObservation(binding: RuntimeBinding): void;
|
|
45
|
+
markClosing(binding: RuntimeBinding): void;
|
|
46
|
+
terminate(binding: RuntimeBinding, error: Error): void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const createDevRuntimeSessionManager = (
|
|
50
|
+
closeBinding: CloseBinding
|
|
51
|
+
): DevRuntimeSessionManager => {
|
|
52
|
+
let state: ControllerState = { status: 'idle' };
|
|
53
|
+
let nextSessionId = 1;
|
|
54
|
+
const closeObservationByServer = new WeakMap<
|
|
55
|
+
RsbuildDevServer,
|
|
56
|
+
CloseObservation
|
|
57
|
+
>();
|
|
58
|
+
|
|
59
|
+
const getState = (): ControllerState => state;
|
|
60
|
+
|
|
61
|
+
const getActiveBinding = (): RuntimeBinding | undefined =>
|
|
62
|
+
state.status === 'active' ? state.binding : undefined;
|
|
63
|
+
|
|
64
|
+
const isCurrentBinding = (binding: RuntimeBinding): boolean =>
|
|
65
|
+
(state.status === 'active' || state.status === 'closing') &&
|
|
66
|
+
state.binding === binding;
|
|
67
|
+
|
|
68
|
+
const completeClose = (binding: RuntimeBinding): void => {
|
|
69
|
+
if (!isCurrentBinding(binding)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (state.status === 'active') {
|
|
73
|
+
closeBinding(binding);
|
|
74
|
+
}
|
|
75
|
+
state = { status: 'idle' };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const failClose = (binding: RuntimeBinding, cause: unknown): void => {
|
|
79
|
+
if (!isCurrentBinding(binding)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const error = new Error(
|
|
83
|
+
`[${PLUGIN_NAME}] The previous development server failed to close. Restart the process before retrying because Rsbuild may not have finished tearing down its compiler and watchers.`,
|
|
84
|
+
{ cause }
|
|
85
|
+
);
|
|
86
|
+
closeBinding(binding, error);
|
|
87
|
+
state = { status: 'terminal', error };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const applyCloseOutcome = (
|
|
91
|
+
observation: CloseObservation,
|
|
92
|
+
outcome: CloseOutcome
|
|
93
|
+
): void => {
|
|
94
|
+
observation.outcome = outcome;
|
|
95
|
+
const { binding } = observation;
|
|
96
|
+
if (!binding) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (outcome.ok) {
|
|
100
|
+
completeClose(binding);
|
|
101
|
+
} else {
|
|
102
|
+
failClose(binding, outcome.cause);
|
|
103
|
+
}
|
|
104
|
+
observation.binding = undefined;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const observeClose = (server: RsbuildDevServer): CloseObservation => {
|
|
108
|
+
const existing = closeObservationByServer.get(server);
|
|
109
|
+
if (existing) {
|
|
110
|
+
return existing;
|
|
111
|
+
}
|
|
112
|
+
const observation: CloseObservation = {};
|
|
113
|
+
const close = server.close.bind(server);
|
|
114
|
+
server.close = () => {
|
|
115
|
+
if (observation.promise) {
|
|
116
|
+
return observation.promise;
|
|
117
|
+
}
|
|
118
|
+
observation.promise = runPluginEffect(
|
|
119
|
+
tryPluginPromise(close).pipe(
|
|
120
|
+
Effect.tap(() =>
|
|
121
|
+
tryPluginSync(() => applyCloseOutcome(observation, { ok: true }))
|
|
122
|
+
),
|
|
123
|
+
Effect.catchAll(cause =>
|
|
124
|
+
tryPluginSync(() =>
|
|
125
|
+
applyCloseOutcome(observation, { ok: false, cause })
|
|
126
|
+
).pipe(Effect.zipRight(Effect.fail(cause)))
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
return observation.promise;
|
|
131
|
+
};
|
|
132
|
+
closeObservationByServer.set(server, observation);
|
|
133
|
+
return observation;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
getState,
|
|
138
|
+
getActiveBinding,
|
|
139
|
+
|
|
140
|
+
observeClose(server: RsbuildDevServer): void {
|
|
141
|
+
observeClose(server);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
assertCanStart(): void {
|
|
145
|
+
if (state.status === 'terminal') {
|
|
146
|
+
throw state.error;
|
|
147
|
+
}
|
|
148
|
+
if (state.status === 'active') {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`[${PLUGIN_NAME}] A development server is already active. Await its close() before calling createDevServer() again. If startup failed before returning the server, restart the process before retrying.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (state.status === 'closing') {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`[${PLUGIN_NAME}] The previous development server is still closing. Await its close() before calling createDevServer() again.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
createBinding(
|
|
161
|
+
server: RsbuildDevServer,
|
|
162
|
+
runtime: ReactRouterDevRuntime
|
|
163
|
+
): RuntimeBinding {
|
|
164
|
+
const binding = { id: nextSessionId++, server, runtime };
|
|
165
|
+
state = { status: 'active', binding };
|
|
166
|
+
return binding;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
bindCloseObservation(binding: RuntimeBinding): void {
|
|
170
|
+
const observation = observeClose(binding.server);
|
|
171
|
+
observation.binding = binding;
|
|
172
|
+
if (observation.outcome) {
|
|
173
|
+
applyCloseOutcome(observation, observation.outcome);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
markClosing(binding: RuntimeBinding): void {
|
|
178
|
+
if (isCurrentBinding(binding)) {
|
|
179
|
+
state = { status: 'closing', binding };
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
terminate(binding: RuntimeBinding, error: Error): void {
|
|
184
|
+
if (!isCurrentBinding(binding)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
closeBinding(binding, error);
|
|
188
|
+
state = { status: 'terminal', error };
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { RsbuildConfig } from '@rsbuild/core';
|
|
3
|
+
import type { ServerBuild } from 'react-router';
|
|
4
|
+
|
|
5
|
+
export type ServerSetup = Exclude<
|
|
6
|
+
NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
|
|
7
|
+
unknown[]
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
export type DevServerMiddleware = (
|
|
11
|
+
req: IncomingMessage,
|
|
12
|
+
res: ServerResponse,
|
|
13
|
+
next: (err?: unknown) => void
|
|
14
|
+
) => Promise<void>;
|
|
15
|
+
|
|
16
|
+
type RequestHandler = (request: Request) => Response | Promise<Response>;
|
|
17
|
+
type BuildProvider = () => Promise<ServerBuild>;
|
|
18
|
+
|
|
19
|
+
export type DevServerMiddlewareDependencies = {
|
|
20
|
+
loadBuild: BuildProvider;
|
|
21
|
+
createRequestHandler?: (
|
|
22
|
+
build: BuildProvider,
|
|
23
|
+
mode: 'development'
|
|
24
|
+
) => RequestHandler;
|
|
25
|
+
createRequestListener?: (
|
|
26
|
+
handler: RequestHandler
|
|
27
|
+
) => (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const createDevServerMiddleware = (
|
|
31
|
+
dependencies: DevServerMiddlewareDependencies
|
|
32
|
+
): DevServerMiddleware => {
|
|
33
|
+
let listenerPromise:
|
|
34
|
+
| Promise<
|
|
35
|
+
(req: IncomingMessage, res: ServerResponse) => void | Promise<void>
|
|
36
|
+
>
|
|
37
|
+
| undefined;
|
|
38
|
+
|
|
39
|
+
const getListener = () => {
|
|
40
|
+
listenerPromise ??= (async () => {
|
|
41
|
+
const createRequestHandler =
|
|
42
|
+
dependencies.createRequestHandler ??
|
|
43
|
+
(await import('react-router')).createRequestHandler;
|
|
44
|
+
const createRequestListener =
|
|
45
|
+
dependencies.createRequestListener ??
|
|
46
|
+
(await import('@remix-run/node-fetch-server')).createRequestListener;
|
|
47
|
+
const requestHandler = createRequestHandler(
|
|
48
|
+
dependencies.loadBuild,
|
|
49
|
+
'development'
|
|
50
|
+
);
|
|
51
|
+
return createRequestListener(request => requestHandler(request));
|
|
52
|
+
})();
|
|
53
|
+
return listenerPromise;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Warm the handler imports now so the first request does not pay them.
|
|
57
|
+
// On failure, reset so the first real request retries and surfaces the
|
|
58
|
+
// error through the middleware's own error path.
|
|
59
|
+
void getListener().catch(() => {
|
|
60
|
+
listenerPromise = undefined;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return async (req, res, next): Promise<void> => {
|
|
64
|
+
try {
|
|
65
|
+
const listener = await getListener();
|
|
66
|
+
await listener(req, res);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
next(error);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const createReactRouterDevServerSetup = ({
|
|
74
|
+
loadBuild,
|
|
75
|
+
}: {
|
|
76
|
+
loadBuild: BuildProvider;
|
|
77
|
+
}): ServerSetup =>
|
|
78
|
+
function reactRouterDevServerSetup(context) {
|
|
79
|
+
if (context.action !== 'dev') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// The returned callback runs after Rsbuild registers its built-in
|
|
83
|
+
// middlewares, so static assets are served before the React Router
|
|
84
|
+
// request handler.
|
|
85
|
+
return () => {
|
|
86
|
+
context.server.middlewares.use(createDevServerMiddleware({ loadBuild }));
|
|
87
|
+
};
|
|
88
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as Cause from 'effect/Cause';
|
|
2
|
+
import * as Duration from 'effect/Duration';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import * as Exit from 'effect/Exit';
|
|
5
|
+
import * as Fiber from 'effect/Fiber';
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
7
|
+
|
|
8
|
+
export const DEV_BACKGROUND_STARTUP_DELAY_MS = 3_000;
|
|
9
|
+
|
|
10
|
+
export const normalizeEffectError = (cause: unknown): Error =>
|
|
11
|
+
cause instanceof Error ? cause : new Error(String(cause));
|
|
12
|
+
|
|
13
|
+
const normalizeEffectCause = <E>(cause: Cause.Cause<E>): Error => {
|
|
14
|
+
const failure = Cause.failureOption(cause);
|
|
15
|
+
return normalizeEffectError(
|
|
16
|
+
Option.isSome(failure) ? failure.value : Cause.squash(cause)
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const runPluginEffect = async <A, E>(
|
|
21
|
+
effect: Effect.Effect<A, E, never>
|
|
22
|
+
): Promise<A> => {
|
|
23
|
+
const exit = await Effect.runPromiseExit(effect);
|
|
24
|
+
if (Exit.isSuccess(exit)) {
|
|
25
|
+
return exit.value;
|
|
26
|
+
}
|
|
27
|
+
throw normalizeEffectCause(exit.cause);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const tryPluginSync = <A>(
|
|
31
|
+
evaluate: () => A
|
|
32
|
+
): Effect.Effect<A, Error, never> =>
|
|
33
|
+
Effect.try({
|
|
34
|
+
try: evaluate,
|
|
35
|
+
catch: normalizeEffectError,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const tryPluginPromise = <A>(
|
|
39
|
+
evaluate: () => PromiseLike<A> | A
|
|
40
|
+
): Effect.Effect<A, Error, never> =>
|
|
41
|
+
Effect.tryPromise({
|
|
42
|
+
try: () => Promise.resolve(evaluate()),
|
|
43
|
+
catch: normalizeEffectError,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
type DelayedPluginTask = {
|
|
47
|
+
schedule(): void;
|
|
48
|
+
reschedule(): void;
|
|
49
|
+
cancelEffect(): Effect.Effect<void, Error, never>;
|
|
50
|
+
cancel(): Promise<void>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const createDelayedPluginTask = ({
|
|
54
|
+
delayMs,
|
|
55
|
+
run,
|
|
56
|
+
onError,
|
|
57
|
+
}: {
|
|
58
|
+
delayMs: number;
|
|
59
|
+
run: () => Effect.Effect<void, Error, never>;
|
|
60
|
+
onError: (error: Error) => void;
|
|
61
|
+
}): DelayedPluginTask => {
|
|
62
|
+
let activeFiber: ReturnType<typeof Effect.runFork> | undefined;
|
|
63
|
+
let version = 0;
|
|
64
|
+
|
|
65
|
+
const cancelActiveEffect = (): Effect.Effect<void, Error, never> =>
|
|
66
|
+
Effect.sync(() => {
|
|
67
|
+
const fiber = activeFiber;
|
|
68
|
+
activeFiber = undefined;
|
|
69
|
+
return fiber;
|
|
70
|
+
}).pipe(
|
|
71
|
+
Effect.flatMap(fiber =>
|
|
72
|
+
fiber ? Fiber.interrupt(fiber).pipe(Effect.asVoid) : Effect.void
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const cancelEffect = (): Effect.Effect<void, Error, never> =>
|
|
77
|
+
Effect.sync(() => {
|
|
78
|
+
version += 1;
|
|
79
|
+
}).pipe(Effect.zipRight(cancelActiveEffect()));
|
|
80
|
+
|
|
81
|
+
const start = (taskVersion: number): void => {
|
|
82
|
+
if (activeFiber || version !== taskVersion) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let fiber: ReturnType<typeof Effect.runFork>;
|
|
87
|
+
fiber = Effect.runFork(
|
|
88
|
+
Effect.sleep(Duration.millis(delayMs)).pipe(
|
|
89
|
+
Effect.zipRight(Effect.suspend(run)),
|
|
90
|
+
Effect.catchAll(error =>
|
|
91
|
+
Effect.sync(() => {
|
|
92
|
+
onError(error);
|
|
93
|
+
})
|
|
94
|
+
),
|
|
95
|
+
Effect.ensuring(
|
|
96
|
+
Effect.sync(() => {
|
|
97
|
+
if (activeFiber === fiber) {
|
|
98
|
+
activeFiber = undefined;
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
activeFiber = fiber;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
schedule(): void {
|
|
109
|
+
if (activeFiber) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
version += 1;
|
|
113
|
+
start(version);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
reschedule(): void {
|
|
117
|
+
version += 1;
|
|
118
|
+
const taskVersion = version;
|
|
119
|
+
void runPluginEffect(cancelActiveEffect())
|
|
120
|
+
.then(() => start(taskVersion))
|
|
121
|
+
.catch(onError);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
cancelEffect,
|
|
125
|
+
|
|
126
|
+
async cancel(): Promise<void> {
|
|
127
|
+
await runPluginEffect(cancelEffect());
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { rspack } from '@rsbuild/core';
|
|
3
|
+
import { langFromPath, parse, type ParseOptions } from 'yuku-parser';
|
|
4
|
+
import { setBoundedCacheEntry } from './bounded-cache.js';
|
|
5
|
+
import {
|
|
6
|
+
getExportedName,
|
|
7
|
+
getIdentifierNamesFromPattern,
|
|
8
|
+
getProgram,
|
|
9
|
+
type AnyNode,
|
|
10
|
+
type ProgramNode,
|
|
11
|
+
} from './route-ast.js';
|
|
12
|
+
|
|
13
|
+
type ExportInfo = {
|
|
14
|
+
readonly exportNames: readonly string[];
|
|
15
|
+
readonly exportAllModules: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type RouteModuleAnalysis = {
|
|
19
|
+
readonly code: string;
|
|
20
|
+
readonly exports: readonly string[];
|
|
21
|
+
readonly exportAllModules: readonly string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type RouteModuleAnalysisCacheEntry = {
|
|
25
|
+
mtimeMs: number;
|
|
26
|
+
size: number;
|
|
27
|
+
analysis: Promise<RouteModuleAnalysis>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const exportInfoCache = new Map<string, Promise<ExportInfo>>();
|
|
31
|
+
const routeModuleAnalysisCache = new Map<
|
|
32
|
+
string,
|
|
33
|
+
RouteModuleAnalysisCacheEntry
|
|
34
|
+
>();
|
|
35
|
+
|
|
36
|
+
const MAX_EXPORT_UTILS_CACHE_ENTRIES = 2048;
|
|
37
|
+
|
|
38
|
+
const stripResourcePathQuery = (resourcePath: string): string =>
|
|
39
|
+
resourcePath.replace(/[?#].*$/, '');
|
|
40
|
+
|
|
41
|
+
type TypeScriptParseLang = Extract<
|
|
42
|
+
NonNullable<ParseOptions['lang']>,
|
|
43
|
+
'ts' | 'tsx'
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
const getExportAnalysisCode = (
|
|
47
|
+
code: string,
|
|
48
|
+
resourcePath: string,
|
|
49
|
+
lang: TypeScriptParseLang
|
|
50
|
+
): string =>
|
|
51
|
+
rspack.experiments.swc.transformSync(code, {
|
|
52
|
+
filename: resourcePath,
|
|
53
|
+
jsc: {
|
|
54
|
+
parser: {
|
|
55
|
+
syntax: 'typescript',
|
|
56
|
+
tsx: lang === 'tsx',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}).code;
|
|
60
|
+
|
|
61
|
+
const getParseErrors = (result: ReturnType<typeof parse>) =>
|
|
62
|
+
result.diagnostics.filter(diagnostic => diagnostic.severity === 'error');
|
|
63
|
+
|
|
64
|
+
const getParseErrorMessage = (
|
|
65
|
+
errors: ReturnType<typeof getParseErrors>
|
|
66
|
+
): string => errors.map(error => error.message).join('\n');
|
|
67
|
+
|
|
68
|
+
const parseProgram = (code: string, resourcePath?: string): ProgramNode => {
|
|
69
|
+
const sourcePath = resourcePath
|
|
70
|
+
? stripResourcePathQuery(resourcePath)
|
|
71
|
+
: undefined;
|
|
72
|
+
const lang = sourcePath ? langFromPath(sourcePath) : 'tsx';
|
|
73
|
+
const result = parse(code, {
|
|
74
|
+
sourceType: 'module',
|
|
75
|
+
lang,
|
|
76
|
+
});
|
|
77
|
+
const errors = getParseErrors(result);
|
|
78
|
+
if (errors.length === 0) {
|
|
79
|
+
return getProgram(result);
|
|
80
|
+
}
|
|
81
|
+
if (!sourcePath || (lang !== 'ts' && lang !== 'tsx')) {
|
|
82
|
+
throw new Error(getParseErrorMessage(errors));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const normalizedCode = getExportAnalysisCode(code, sourcePath, lang);
|
|
86
|
+
const normalizedResult = parse(normalizedCode, {
|
|
87
|
+
sourceType: 'module',
|
|
88
|
+
lang: 'js',
|
|
89
|
+
});
|
|
90
|
+
const normalizedErrors = getParseErrors(normalizedResult);
|
|
91
|
+
if (normalizedErrors.length > 0) {
|
|
92
|
+
throw new Error(getParseErrorMessage(normalizedErrors));
|
|
93
|
+
}
|
|
94
|
+
return getProgram(normalizedResult);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getExportInfoCacheKey = (
|
|
98
|
+
code: string,
|
|
99
|
+
resourcePath?: string
|
|
100
|
+
): string => {
|
|
101
|
+
const lang = resourcePath
|
|
102
|
+
? langFromPath(stripResourcePathQuery(resourcePath))
|
|
103
|
+
: 'inline';
|
|
104
|
+
return `${lang}\0${code}`;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const cachePromiseOnReject = <T>(
|
|
108
|
+
promise: Promise<T>,
|
|
109
|
+
invalidate: () => void
|
|
110
|
+
): Promise<T> =>
|
|
111
|
+
promise.catch(error => {
|
|
112
|
+
invalidate();
|
|
113
|
+
throw error;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const isTypeOnlyExport = (node: AnyNode): boolean =>
|
|
117
|
+
node.exportKind === 'type' ||
|
|
118
|
+
node.type === 'TSExportAssignment' ||
|
|
119
|
+
node.declaration?.declare === true ||
|
|
120
|
+
(node.type === 'ExportDefaultDeclaration' &&
|
|
121
|
+
node.declaration?.type === 'TSInterfaceDeclaration');
|
|
122
|
+
|
|
123
|
+
export const collectProgramExportNames = (program: ProgramNode): string[] => {
|
|
124
|
+
const exportNames = new Set<string>();
|
|
125
|
+
for (const statement of program.body ?? []) {
|
|
126
|
+
if (isTypeOnlyExport(statement)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (statement.type === 'ExportAllDeclaration') {
|
|
131
|
+
const exported = getExportedName(statement.exported);
|
|
132
|
+
if (exported) {
|
|
133
|
+
exportNames.add(exported);
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (statement.type === 'ExportDefaultDeclaration') {
|
|
139
|
+
exportNames.add('default');
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (statement.type !== 'ExportNamedDeclaration') {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const declaration = statement.declaration;
|
|
147
|
+
if (declaration) {
|
|
148
|
+
if (declaration.type === 'VariableDeclaration') {
|
|
149
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
150
|
+
for (const name of getIdentifierNamesFromPattern(declarator.id)) {
|
|
151
|
+
exportNames.add(name);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else if (
|
|
155
|
+
(declaration.type === 'FunctionDeclaration' ||
|
|
156
|
+
declaration.type === 'ClassDeclaration' ||
|
|
157
|
+
declaration.type === 'TSEnumDeclaration') &&
|
|
158
|
+
declaration.id?.name
|
|
159
|
+
) {
|
|
160
|
+
exportNames.add(declaration.id.name);
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
166
|
+
if (specifier.exportKind === 'type') {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const exported = getExportedName(specifier.exported);
|
|
170
|
+
if (exported) {
|
|
171
|
+
exportNames.add(exported);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return Array.from(exportNames);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const collectExportAllModules = (program: AnyNode): string[] => {
|
|
179
|
+
const modules: string[] = [];
|
|
180
|
+
for (const statement of program.body ?? []) {
|
|
181
|
+
if (
|
|
182
|
+
statement.type !== 'ExportAllDeclaration' ||
|
|
183
|
+
isTypeOnlyExport(statement)
|
|
184
|
+
) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (statement.exported) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const source = statement.source?.value;
|
|
191
|
+
if (typeof source === 'string') {
|
|
192
|
+
modules.push(source);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return modules;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const getExportNames = async (
|
|
199
|
+
code: string,
|
|
200
|
+
resourcePath?: string
|
|
201
|
+
): Promise<readonly string[]> => {
|
|
202
|
+
return (await getExportNamesAndExportAll(code, resourcePath)).exportNames;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const getExportNamesAndExportAll = async (
|
|
206
|
+
code: string,
|
|
207
|
+
resourcePath?: string
|
|
208
|
+
): Promise<ExportInfo> => {
|
|
209
|
+
const cacheKey = getExportInfoCacheKey(code, resourcePath);
|
|
210
|
+
const cached = exportInfoCache.get(cacheKey);
|
|
211
|
+
if (cached) {
|
|
212
|
+
return cached;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const exportInfo = (async () => {
|
|
216
|
+
const program = parseProgram(code, resourcePath);
|
|
217
|
+
return {
|
|
218
|
+
exportNames: collectProgramExportNames(program),
|
|
219
|
+
exportAllModules: collectExportAllModules(program),
|
|
220
|
+
};
|
|
221
|
+
})();
|
|
222
|
+
|
|
223
|
+
let trackedExportInfo: Promise<ExportInfo>;
|
|
224
|
+
trackedExportInfo = cachePromiseOnReject(exportInfo, () => {
|
|
225
|
+
if (exportInfoCache.get(cacheKey) === trackedExportInfo) {
|
|
226
|
+
exportInfoCache.delete(cacheKey);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
setBoundedCacheEntry(
|
|
231
|
+
exportInfoCache,
|
|
232
|
+
cacheKey,
|
|
233
|
+
trackedExportInfo,
|
|
234
|
+
MAX_EXPORT_UTILS_CACHE_ENTRIES
|
|
235
|
+
);
|
|
236
|
+
return trackedExportInfo;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export const getRouteModuleAnalysis = async (
|
|
240
|
+
resourcePath: string
|
|
241
|
+
): Promise<RouteModuleAnalysis> => {
|
|
242
|
+
const stats = await stat(resourcePath);
|
|
243
|
+
const cached = routeModuleAnalysisCache.get(resourcePath);
|
|
244
|
+
if (cached?.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
245
|
+
return cached.analysis;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const analysis = (async () => {
|
|
249
|
+
const source = await readFile(resourcePath, 'utf8');
|
|
250
|
+
const program = parseProgram(source, resourcePath);
|
|
251
|
+
return {
|
|
252
|
+
code: source,
|
|
253
|
+
exports: collectProgramExportNames(program),
|
|
254
|
+
exportAllModules: collectExportAllModules(program),
|
|
255
|
+
};
|
|
256
|
+
})();
|
|
257
|
+
|
|
258
|
+
let trackedAnalysis: Promise<RouteModuleAnalysis>;
|
|
259
|
+
trackedAnalysis = cachePromiseOnReject(analysis, () => {
|
|
260
|
+
if (
|
|
261
|
+
routeModuleAnalysisCache.get(resourcePath)?.analysis === trackedAnalysis
|
|
262
|
+
) {
|
|
263
|
+
routeModuleAnalysisCache.delete(resourcePath);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
setBoundedCacheEntry(
|
|
268
|
+
routeModuleAnalysisCache,
|
|
269
|
+
resourcePath,
|
|
270
|
+
{
|
|
271
|
+
mtimeMs: stats.mtimeMs,
|
|
272
|
+
size: stats.size,
|
|
273
|
+
analysis: trackedAnalysis,
|
|
274
|
+
},
|
|
275
|
+
MAX_EXPORT_UTILS_CACHE_ENTRIES
|
|
276
|
+
);
|
|
277
|
+
return trackedAnalysis;
|
|
278
|
+
};
|