react-bun-ssr 0.3.2 → 0.4.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 +39 -5
- package/framework/cli/dev-runtime.ts +18 -1
- package/framework/runtime/action-stub.ts +26 -0
- package/framework/runtime/client-runtime.tsx +66 -182
- package/framework/runtime/helpers.ts +75 -1
- package/framework/runtime/index.ts +53 -23
- package/framework/runtime/module-loader.ts +197 -35
- package/framework/runtime/render.tsx +1 -1
- package/framework/runtime/request-executor.ts +1705 -0
- package/framework/runtime/response-context.ts +206 -0
- package/framework/runtime/route-api.ts +51 -18
- package/framework/runtime/route-scanner.ts +104 -12
- package/framework/runtime/route-wire-protocol.ts +486 -0
- package/framework/runtime/server.ts +8 -1295
- package/framework/runtime/tree.tsx +45 -4
- package/framework/runtime/types.ts +71 -10
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { isRouteActionStub } from './action-stub';
|
|
2
3
|
import { ensureDir, existsPath } from './io';
|
|
3
4
|
import type {
|
|
4
5
|
ApiRouteModule,
|
|
@@ -30,8 +31,19 @@ export interface RouteModuleLoadOptions {
|
|
|
30
31
|
serverBytecode?: boolean;
|
|
31
32
|
devSourceImports?: boolean;
|
|
32
33
|
nodeEnv?: "development" | "production";
|
|
34
|
+
companionFilePath?: string | null;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
const ROUTE_SERVER_EXPORT_KEYS = new Set([
|
|
38
|
+
"loader",
|
|
39
|
+
"action",
|
|
40
|
+
"middleware",
|
|
41
|
+
"head",
|
|
42
|
+
"meta",
|
|
43
|
+
"onError",
|
|
44
|
+
"onCatch",
|
|
45
|
+
]);
|
|
46
|
+
|
|
35
47
|
export function createServerModuleCacheKey(options: {
|
|
36
48
|
absoluteFilePath: string;
|
|
37
49
|
cacheBustKey?: string;
|
|
@@ -88,6 +100,46 @@ function normalizeLoadOptions(
|
|
|
88
100
|
return options ?? {};
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
export function toServerCompanionPath(filePath: string): string {
|
|
104
|
+
const extension = path.extname(filePath);
|
|
105
|
+
if (!extension) {
|
|
106
|
+
return `${filePath}.server`;
|
|
107
|
+
}
|
|
108
|
+
return `${filePath.slice(0, -extension.length)}.server${extension}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function resolveServerCompanionFilePath(
|
|
112
|
+
filePath: string,
|
|
113
|
+
options: RouteModuleLoadOptions,
|
|
114
|
+
): Promise<string | null> {
|
|
115
|
+
if (options.companionFilePath === null) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof options.companionFilePath === "string") {
|
|
120
|
+
return options.companionFilePath;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const companionFilePath = toServerCompanionPath(filePath);
|
|
124
|
+
if (await existsPath(companionFilePath)) {
|
|
125
|
+
return companionFilePath;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function loadRawModuleRecord(
|
|
131
|
+
filePath: string,
|
|
132
|
+
options: RouteModuleLoadOptions,
|
|
133
|
+
): Promise<Record<string, unknown>> {
|
|
134
|
+
const modulePath = options.devSourceImports
|
|
135
|
+
? path.resolve(filePath)
|
|
136
|
+
: await buildServerModule(filePath, options);
|
|
137
|
+
return unwrapModuleNamespace(await importModule<Record<string, unknown>>(
|
|
138
|
+
modulePath,
|
|
139
|
+
options.cacheBustKey,
|
|
140
|
+
));
|
|
141
|
+
}
|
|
142
|
+
|
|
91
143
|
function toRouteModule(filePath: string, moduleValue: unknown): RouteModule {
|
|
92
144
|
const rawValue = moduleValue as Record<string, unknown>;
|
|
93
145
|
const value = unwrapModuleNamespace(rawValue) as Partial<RouteModule>;
|
|
@@ -108,14 +160,103 @@ function toRouteModule(filePath: string, moduleValue: unknown): RouteModule {
|
|
|
108
160
|
} as RouteModule;
|
|
109
161
|
}
|
|
110
162
|
|
|
163
|
+
function toRouteServerCompanionExports(
|
|
164
|
+
filePath: string,
|
|
165
|
+
companionFilePath: string,
|
|
166
|
+
moduleValue: Record<string, unknown>,
|
|
167
|
+
): Partial<RouteModule> {
|
|
168
|
+
const exportKeys = Object.keys(moduleValue).filter(key => key !== "__esModule");
|
|
169
|
+
|
|
170
|
+
if (exportKeys.includes("default")) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Route companion module ${companionFilePath} cannot export default. ` +
|
|
173
|
+
`Move UI exports back to ${filePath}.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const unsupportedExports = exportKeys.filter(key => !ROUTE_SERVER_EXPORT_KEYS.has(key));
|
|
178
|
+
if (unsupportedExports.length > 0) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Route companion module ${companionFilePath} has unsupported exports: ${unsupportedExports.join(", ")}. ` +
|
|
181
|
+
`Allowed exports: ${[...ROUTE_SERVER_EXPORT_KEYS].join(", ")}.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const companionExports: Partial<RouteModule> = {};
|
|
186
|
+
for (const key of exportKeys) {
|
|
187
|
+
(companionExports as Record<string, unknown>)[key] = moduleValue[key];
|
|
188
|
+
}
|
|
189
|
+
return companionExports;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function mergeRouteModuleWithCompanion(options: {
|
|
193
|
+
filePath: string;
|
|
194
|
+
companionFilePath: string;
|
|
195
|
+
routeModule: RouteModule;
|
|
196
|
+
companionExports: Partial<RouteModule>;
|
|
197
|
+
}): RouteModule {
|
|
198
|
+
for (const key of ROUTE_SERVER_EXPORT_KEYS) {
|
|
199
|
+
if (options.companionExports[key as keyof RouteModule] === undefined) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
key === "action"
|
|
205
|
+
&& isRouteActionStub(options.routeModule.action)
|
|
206
|
+
) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (options.routeModule[key as keyof RouteModule] !== undefined) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Duplicate server export "${key}" found in both ${options.filePath} and ${options.companionFilePath}. ` +
|
|
213
|
+
`Keep "${key}" in only one file.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
...options.routeModule,
|
|
220
|
+
...options.companionExports,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function stripRouteActionStub(routeModule: RouteModule): RouteModule {
|
|
225
|
+
if (!isRouteActionStub(routeModule.action)) {
|
|
226
|
+
return routeModule;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { action: _stubAction, ...rest } = routeModule;
|
|
230
|
+
return rest as RouteModule;
|
|
231
|
+
}
|
|
232
|
+
|
|
111
233
|
function unwrapModuleNamespace(moduleValue: Record<string, unknown>): Record<string, unknown> {
|
|
112
234
|
if (
|
|
113
|
-
moduleValue
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
235
|
+
!moduleValue
|
|
236
|
+
|| typeof moduleValue.default !== "object"
|
|
237
|
+
|| moduleValue.default === null
|
|
238
|
+
) {
|
|
239
|
+
return moduleValue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const defaultNamespace = moduleValue.default as Record<string, unknown>;
|
|
243
|
+
|
|
244
|
+
if ("default" in defaultNamespace) {
|
|
245
|
+
return defaultNamespace;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const namedExportKeys = Object.keys(moduleValue).filter(key => {
|
|
249
|
+
return key !== "default" && key !== "__esModule";
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
namedExportKeys.length > 0
|
|
254
|
+
&& namedExportKeys.every(key => {
|
|
255
|
+
return Object.prototype.hasOwnProperty.call(defaultNamespace, key)
|
|
256
|
+
&& defaultNamespace[key] === moduleValue[key];
|
|
257
|
+
})
|
|
117
258
|
) {
|
|
118
|
-
return
|
|
259
|
+
return defaultNamespace;
|
|
119
260
|
}
|
|
120
261
|
|
|
121
262
|
return moduleValue;
|
|
@@ -243,20 +384,29 @@ export async function loadRouteModule(
|
|
|
243
384
|
options: RouteModuleLoadOptions = {},
|
|
244
385
|
): Promise<RouteModule> {
|
|
245
386
|
const normalizedOptions = normalizeLoadOptions(options);
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
387
|
+
const baseModuleValue = await loadRawModuleRecord(filePath, normalizedOptions);
|
|
388
|
+
const routeModule = toRouteModule(filePath, baseModuleValue);
|
|
389
|
+
const companionFilePath = await resolveServerCompanionFilePath(filePath, normalizedOptions);
|
|
390
|
+
if (!companionFilePath) {
|
|
391
|
+
return stripRouteActionStub(routeModule);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const companionModuleValue = await loadRawModuleRecord(companionFilePath, normalizedOptions);
|
|
395
|
+
const companionExports = toRouteServerCompanionExports(filePath, companionFilePath, companionModuleValue);
|
|
396
|
+
const mergedRouteModule = mergeRouteModuleWithCompanion({
|
|
397
|
+
filePath,
|
|
398
|
+
companionFilePath,
|
|
399
|
+
routeModule,
|
|
400
|
+
companionExports,
|
|
401
|
+
});
|
|
402
|
+
return stripRouteActionStub(mergedRouteModule);
|
|
254
403
|
}
|
|
255
404
|
|
|
256
405
|
export async function loadRouteModules(options: {
|
|
257
406
|
rootFilePath: string;
|
|
258
407
|
layoutFiles: string[];
|
|
259
408
|
routeFilePath: string;
|
|
409
|
+
routeServerFilePath?: string;
|
|
260
410
|
cacheBustKey?: string;
|
|
261
411
|
serverBytecode?: boolean;
|
|
262
412
|
devSourceImports?: boolean;
|
|
@@ -273,7 +423,10 @@ export async function loadRouteModules(options: {
|
|
|
273
423
|
loadRouteModule(layoutFilePath, moduleOptions),
|
|
274
424
|
),
|
|
275
425
|
),
|
|
276
|
-
loadRouteModule(options.routeFilePath,
|
|
426
|
+
loadRouteModule(options.routeFilePath, {
|
|
427
|
+
...moduleOptions,
|
|
428
|
+
companionFilePath: options.routeServerFilePath,
|
|
429
|
+
}),
|
|
277
430
|
]);
|
|
278
431
|
|
|
279
432
|
return {
|
|
@@ -301,22 +454,42 @@ function normalizeMiddlewareExport(value: unknown): Middleware[] {
|
|
|
301
454
|
return [];
|
|
302
455
|
}
|
|
303
456
|
|
|
457
|
+
async function resolveGlobalMiddlewarePath(middlewareFilePath: string): Promise<string | null> {
|
|
458
|
+
const basePath = path.resolve(middlewareFilePath);
|
|
459
|
+
const serverPath = toServerCompanionPath(basePath);
|
|
460
|
+
const [baseExists, serverExists] = await Promise.all([
|
|
461
|
+
existsPath(basePath),
|
|
462
|
+
existsPath(serverPath),
|
|
463
|
+
]);
|
|
464
|
+
|
|
465
|
+
if (baseExists && serverExists) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Global middleware file collision: both ${basePath} and ${serverPath} exist. ` +
|
|
468
|
+
"Use only one of these files.",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (serverExists) {
|
|
473
|
+
return serverPath;
|
|
474
|
+
}
|
|
475
|
+
if (baseExists) {
|
|
476
|
+
return basePath;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
304
482
|
export async function loadGlobalMiddleware(
|
|
305
483
|
middlewareFilePath: string,
|
|
306
484
|
options: string | RouteModuleLoadOptions = {},
|
|
307
485
|
): Promise<Middleware[]> {
|
|
308
|
-
|
|
486
|
+
const resolvedMiddlewarePath = await resolveGlobalMiddlewarePath(middlewareFilePath);
|
|
487
|
+
if (!resolvedMiddlewarePath) {
|
|
309
488
|
return [];
|
|
310
489
|
}
|
|
311
490
|
|
|
312
491
|
const normalizedOptions = normalizeLoadOptions(options);
|
|
313
|
-
const
|
|
314
|
-
? path.resolve(middlewareFilePath)
|
|
315
|
-
: await buildServerModule(middlewareFilePath, normalizedOptions);
|
|
316
|
-
const raw = unwrapModuleNamespace(await importModule<Record<string, unknown>>(
|
|
317
|
-
modulePath,
|
|
318
|
-
normalizedOptions.cacheBustKey,
|
|
319
|
-
));
|
|
492
|
+
const raw = await loadRawModuleRecord(resolvedMiddlewarePath, normalizedOptions);
|
|
320
493
|
|
|
321
494
|
return [
|
|
322
495
|
...normalizeMiddlewareExport(raw.default),
|
|
@@ -331,13 +504,7 @@ export async function loadNestedMiddleware(
|
|
|
331
504
|
const normalizedOptions = normalizeLoadOptions(options);
|
|
332
505
|
const rawModules = await Promise.all(
|
|
333
506
|
middlewareFilePaths.map(async (middlewareFilePath) => {
|
|
334
|
-
|
|
335
|
-
? path.resolve(middlewareFilePath)
|
|
336
|
-
: await buildServerModule(middlewareFilePath, normalizedOptions);
|
|
337
|
-
return unwrapModuleNamespace(await importModule<Record<string, unknown>>(
|
|
338
|
-
modulePath,
|
|
339
|
-
normalizedOptions.cacheBustKey,
|
|
340
|
-
));
|
|
507
|
+
return loadRawModuleRecord(middlewareFilePath, normalizedOptions);
|
|
341
508
|
}),
|
|
342
509
|
);
|
|
343
510
|
|
|
@@ -358,10 +525,5 @@ export async function loadApiRouteModule(
|
|
|
358
525
|
options: string | RouteModuleLoadOptions = {},
|
|
359
526
|
): Promise<ApiRouteModule> {
|
|
360
527
|
const normalizedOptions = normalizeLoadOptions(options);
|
|
361
|
-
|
|
362
|
-
? path.resolve(filePath)
|
|
363
|
-
: await buildServerModule(filePath, normalizedOptions);
|
|
364
|
-
return unwrapModuleNamespace(
|
|
365
|
-
await importModule<Record<string, unknown>>(modulePath, normalizedOptions.cacheBustKey),
|
|
366
|
-
) as ApiRouteModule;
|
|
528
|
+
return await loadRawModuleRecord(filePath, normalizedOptions) as ApiRouteModule;
|
|
367
529
|
}
|
|
@@ -155,7 +155,7 @@ function moduleHeadToElements(moduleValue: RouteModule, payload: RenderPayload,
|
|
|
155
155
|
const tags: ReactNode[] = [];
|
|
156
156
|
|
|
157
157
|
const context = {
|
|
158
|
-
data: payload.
|
|
158
|
+
data: payload.loaderData,
|
|
159
159
|
params: payload.params,
|
|
160
160
|
url: new URL(payload.url),
|
|
161
161
|
error: payload.error,
|