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.
Files changed (105) hide show
  1. package/README.md +213 -201
  2. package/dist/451.js +1103 -0
  3. package/dist/bounded-cache.d.ts +1 -0
  4. package/dist/build-manifest.d.ts +7 -4
  5. package/dist/build-output-transforms.d.ts +30 -0
  6. package/dist/concurrency.d.ts +3 -0
  7. package/dist/config-imports.d.ts +10 -0
  8. package/dist/constants.d.ts +3 -0
  9. package/dist/dev-background-resources.d.ts +38 -0
  10. package/dist/dev-generation.d.ts +25 -0
  11. package/dist/dev-runtime-artifacts.d.ts +47 -0
  12. package/dist/dev-runtime-compilation.d.ts +47 -0
  13. package/dist/dev-runtime-controller.d.ts +14 -0
  14. package/dist/dev-runtime-session.d.ts +34 -0
  15. package/dist/dev-server.d.ts +16 -2
  16. package/dist/effect-runtime.d.ts +18 -0
  17. package/dist/export-utils.d.ts +15 -7
  18. package/dist/index.cjs +13775 -1412
  19. package/dist/index.d.ts +8 -1
  20. package/dist/index.js +9429 -1501
  21. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  22. package/dist/lazy-compilation.d.ts +6 -0
  23. package/dist/manifest.d.ts +48 -10
  24. package/dist/modify-browser-manifest.d.ts +30 -13
  25. package/dist/parallel-route-transform-protocol.d.ts +25 -0
  26. package/dist/parallel-route-transform-worker.d.ts +1 -0
  27. package/dist/parallel-route-transform-worker.js +38 -0
  28. package/dist/parallel-route-transforms.d.ts +29 -0
  29. package/dist/performance.d.ts +26 -0
  30. package/dist/plugin-utils.d.ts +2 -12
  31. package/dist/prerender-build.d.ts +36 -0
  32. package/dist/prerender.d.ts +27 -2
  33. package/dist/react-router-config.d.ts +15 -5
  34. package/dist/route-artifacts.d.ts +33 -0
  35. package/dist/route-ast.d.ts +21 -0
  36. package/dist/route-chunks.d.ts +9 -1
  37. package/dist/route-component-transform.d.ts +5 -0
  38. package/dist/route-export-pruning.d.ts +6 -0
  39. package/dist/route-export-resolution.d.ts +5 -0
  40. package/dist/route-transform-tasks.d.ts +47 -0
  41. package/dist/route-watch.d.ts +27 -0
  42. package/dist/server-build-plan.d.ts +22 -0
  43. package/dist/server-build-resolution.d.ts +3 -0
  44. package/dist/server-utils.d.ts +3 -2
  45. package/dist/ssr-externals.d.ts +1 -0
  46. package/dist/templates/entry.server.cjs +3 -3
  47. package/dist/templates/entry.server.js +3 -3
  48. package/dist/typegen.d.ts +15 -0
  49. package/dist/types.d.ts +41 -14
  50. package/dist/virtual-modules.d.ts +2 -0
  51. package/dist/warnings/warn-on-client-source-maps.d.ts +1 -0
  52. package/dist/yuku.d.ts +15 -0
  53. package/package.json +24 -19
  54. package/src/bounded-cache.ts +18 -0
  55. package/src/build-manifest.ts +205 -0
  56. package/src/build-output-transforms.ts +273 -0
  57. package/src/concurrency.ts +15 -0
  58. package/src/config-imports.ts +83 -0
  59. package/src/constants.ts +91 -0
  60. package/src/dev-background-resources.ts +255 -0
  61. package/src/dev-generation.ts +690 -0
  62. package/src/dev-runtime-artifacts.ts +239 -0
  63. package/src/dev-runtime-compilation.ts +171 -0
  64. package/src/dev-runtime-controller.ts +542 -0
  65. package/src/dev-runtime-session.ts +191 -0
  66. package/src/dev-server.ts +88 -0
  67. package/src/effect-runtime.ts +130 -0
  68. package/src/export-utils.ts +278 -0
  69. package/src/index.ts +990 -0
  70. package/src/lazy-compilation-prewarm.ts +279 -0
  71. package/src/lazy-compilation.ts +101 -0
  72. package/src/manifest.ts +591 -0
  73. package/src/modify-browser-manifest.ts +246 -0
  74. package/src/parallel-route-transform-protocol.ts +35 -0
  75. package/src/parallel-route-transform-worker.ts +82 -0
  76. package/src/parallel-route-transforms.ts +458 -0
  77. package/src/performance.ts +254 -0
  78. package/src/plugin-utils.ts +82 -0
  79. package/src/prerender-build.ts +687 -0
  80. package/src/prerender.ts +349 -0
  81. package/src/react-router-config.ts +245 -0
  82. package/src/route-artifacts.ts +155 -0
  83. package/src/route-ast.ts +163 -0
  84. package/src/route-chunks.ts +857 -0
  85. package/src/route-component-transform.ts +314 -0
  86. package/src/route-config.ts +106 -0
  87. package/src/route-export-pruning.ts +668 -0
  88. package/src/route-export-resolution.ts +329 -0
  89. package/src/route-transform-tasks.ts +249 -0
  90. package/src/route-watch.ts +357 -0
  91. package/src/server-build-plan.ts +91 -0
  92. package/src/server-build-resolution.ts +131 -0
  93. package/src/server-utils.ts +111 -0
  94. package/src/ssr-externals.ts +59 -0
  95. package/src/templates/context.ts +12 -0
  96. package/src/templates/entry.client.tsx +12 -0
  97. package/src/templates/entry.server.tsx +76 -0
  98. package/src/typegen.ts +178 -0
  99. package/src/types.ts +92 -0
  100. package/src/validation/validate-plugin-order.ts +76 -0
  101. package/src/virtual-modules.ts +30 -0
  102. package/src/warnings/warn-on-client-source-maps.ts +96 -0
  103. package/src/yuku.ts +67 -0
  104. package/dist/0~rslib-runtime.js +0 -16
  105. 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
+ };