react-bun-ssr 0.1.1 → 0.2.0
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/framework/cli/commands.ts +59 -223
- package/framework/cli/dev-client-watch.ts +281 -0
- package/framework/cli/dev-route-table.ts +71 -0
- package/framework/cli/dev-runtime.ts +382 -0
- package/framework/cli/internal.ts +138 -0
- package/framework/cli/main.ts +27 -31
- package/framework/runtime/build-tools.ts +131 -12
- package/framework/runtime/bun-route-adapter.ts +20 -7
- package/framework/runtime/client-runtime.tsx +73 -132
- package/framework/runtime/client-transition-core.ts +159 -0
- package/framework/runtime/markdown-routes.ts +1 -14
- package/framework/runtime/matcher.ts +11 -11
- package/framework/runtime/module-loader.ts +62 -24
- package/framework/runtime/render.tsx +56 -20
- package/framework/runtime/server.ts +49 -106
- package/framework/runtime/types.ts +12 -1
- package/framework/runtime/utils.ts +3 -0
- package/package.json +2 -1
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
ServerRuntimeOptions,
|
|
29
29
|
TransitionChunk,
|
|
30
30
|
TransitionDeferredChunk,
|
|
31
|
+
TransitionDocumentChunk,
|
|
31
32
|
TransitionInitialChunk,
|
|
32
33
|
TransitionRedirectChunk,
|
|
33
34
|
} from "./types";
|
|
@@ -330,6 +331,7 @@ async function loadRootOnlyModule(
|
|
|
330
331
|
options: {
|
|
331
332
|
cacheBustKey?: string;
|
|
332
333
|
serverBytecode: boolean;
|
|
334
|
+
devSourceImports?: boolean;
|
|
333
335
|
},
|
|
334
336
|
): Promise<RouteModule> {
|
|
335
337
|
return loadRouteModule(rootModulePath, options);
|
|
@@ -436,17 +438,25 @@ function toRedirectChunk(location: string, status: number): TransitionRedirectCh
|
|
|
436
438
|
};
|
|
437
439
|
}
|
|
438
440
|
|
|
441
|
+
function toDocumentChunk(location: string, status: number): TransitionDocumentChunk {
|
|
442
|
+
return {
|
|
443
|
+
type: "document",
|
|
444
|
+
location,
|
|
445
|
+
status,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
439
449
|
function createTransitionStream(options: {
|
|
440
450
|
initialChunk?: TransitionInitialChunk;
|
|
441
|
-
|
|
451
|
+
controlChunk?: TransitionRedirectChunk | TransitionDocumentChunk;
|
|
442
452
|
deferredSettleEntries?: DeferredSettleEntry[];
|
|
443
453
|
sanitizeDeferredError: (message: string) => string;
|
|
444
454
|
}): ReadableStream<Uint8Array> {
|
|
445
455
|
return new ReadableStream<Uint8Array>({
|
|
446
456
|
async start(controller) {
|
|
447
457
|
try {
|
|
448
|
-
if (options.
|
|
449
|
-
controller.enqueue(toTransitionChunkLine(options.
|
|
458
|
+
if (options.controlChunk) {
|
|
459
|
+
controller.enqueue(toTransitionChunkLine(options.controlChunk));
|
|
450
460
|
controller.close();
|
|
451
461
|
return;
|
|
452
462
|
}
|
|
@@ -489,74 +499,6 @@ function createTransitionStream(options: {
|
|
|
489
499
|
});
|
|
490
500
|
}
|
|
491
501
|
|
|
492
|
-
function createDevReloadEventStream(options: {
|
|
493
|
-
getVersion: () => number;
|
|
494
|
-
subscribe?: (listener: (version: number) => void) => (() => void) | void;
|
|
495
|
-
}): Response {
|
|
496
|
-
const encoder = new TextEncoder();
|
|
497
|
-
let interval: ReturnType<typeof setInterval> | undefined;
|
|
498
|
-
let unsubscribe: (() => void) | void;
|
|
499
|
-
let cleanup: (() => void) | undefined;
|
|
500
|
-
|
|
501
|
-
const stream = new ReadableStream<Uint8Array>({
|
|
502
|
-
start(controller) {
|
|
503
|
-
let closed = false;
|
|
504
|
-
|
|
505
|
-
cleanup = (): void => {
|
|
506
|
-
if (closed) {
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
closed = true;
|
|
510
|
-
if (interval) {
|
|
511
|
-
clearInterval(interval);
|
|
512
|
-
interval = undefined;
|
|
513
|
-
}
|
|
514
|
-
if (typeof unsubscribe === "function") {
|
|
515
|
-
unsubscribe();
|
|
516
|
-
unsubscribe = undefined;
|
|
517
|
-
}
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
const sendChunk = (chunk: string): void => {
|
|
521
|
-
if (closed) {
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
try {
|
|
525
|
-
controller.enqueue(encoder.encode(chunk));
|
|
526
|
-
} catch {
|
|
527
|
-
cleanup?.();
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
const sendReload = (version: number): void => {
|
|
532
|
-
sendChunk(`event: reload\ndata: ${version}\n\n`);
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
sendChunk(": connected\n\n");
|
|
536
|
-
sendReload(options.getVersion());
|
|
537
|
-
|
|
538
|
-
unsubscribe = options.subscribe?.(nextVersion => {
|
|
539
|
-
sendReload(nextVersion);
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
interval = setInterval(() => {
|
|
543
|
-
sendChunk(": ping\n\n");
|
|
544
|
-
}, 15_000);
|
|
545
|
-
},
|
|
546
|
-
cancel() {
|
|
547
|
-
cleanup?.();
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
return new Response(stream, {
|
|
552
|
-
headers: {
|
|
553
|
-
"content-type": "text/event-stream; charset=utf-8",
|
|
554
|
-
"cache-control": "no-cache, no-transform",
|
|
555
|
-
connection: "keep-alive",
|
|
556
|
-
},
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
|
|
560
502
|
export function createServer(
|
|
561
503
|
config: FrameworkConfig = {},
|
|
562
504
|
runtimeOptions: ServerRuntimeOptions = {},
|
|
@@ -569,8 +511,10 @@ export function createServer(
|
|
|
569
511
|
const pendingAdapterCache = new Map<string, Promise<BunRouteAdapter>>();
|
|
570
512
|
|
|
571
513
|
const getAdapterKey = (activeConfig: ResolvedConfig): string => {
|
|
572
|
-
const
|
|
573
|
-
|
|
514
|
+
const routeVersion = dev
|
|
515
|
+
? runtimeOptions.routeManifestVersion?.() ?? runtimeOptions.reloadVersion?.() ?? 0
|
|
516
|
+
: 0;
|
|
517
|
+
return `${normalizeSlashes(activeConfig.routesDir)}|${dev ? "dev" : "prod"}|${routeVersion}`;
|
|
574
518
|
};
|
|
575
519
|
|
|
576
520
|
const trimAdapterCache = (): void => {
|
|
@@ -601,10 +545,9 @@ export function createServer(
|
|
|
601
545
|
return pending;
|
|
602
546
|
}
|
|
603
547
|
|
|
604
|
-
const reloadVersion = dev ? runtimeOptions.reloadVersion?.() ?? 0 : 0;
|
|
605
548
|
const routesHash = stableHash(normalizeSlashes(activeConfig.routesDir));
|
|
606
549
|
const projectionRootDir = dev
|
|
607
|
-
? path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", `dev-${routesHash}
|
|
550
|
+
? path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", `dev-${routesHash}`)
|
|
608
551
|
: path.resolve(activeConfig.cwd, ".rbssr/generated/router-projection", "prod", routesHash);
|
|
609
552
|
|
|
610
553
|
const buildAdapterPromise = createBunRouteAdapter({
|
|
@@ -626,8 +569,6 @@ export function createServer(
|
|
|
626
569
|
};
|
|
627
570
|
|
|
628
571
|
const fetchHandler = async (request: Request): Promise<Response> => {
|
|
629
|
-
await runtimeOptions.onBeforeRequest?.();
|
|
630
|
-
|
|
631
572
|
const runtimePaths = runtimeOptions.resolvePaths?.() ?? {};
|
|
632
573
|
const activeConfig: ResolvedConfig = {
|
|
633
574
|
...resolvedConfig,
|
|
@@ -647,13 +588,6 @@ export function createServer(
|
|
|
647
588
|
});
|
|
648
589
|
};
|
|
649
590
|
|
|
650
|
-
if (dev && url.pathname === "/__rbssr/events") {
|
|
651
|
-
return finalize(createDevReloadEventStream({
|
|
652
|
-
getVersion: () => runtimeOptions.reloadVersion?.() ?? 0,
|
|
653
|
-
subscribe: runtimeOptions.subscribeReload,
|
|
654
|
-
}), "internal-dev");
|
|
655
|
-
}
|
|
656
|
-
|
|
657
591
|
if (dev && url.pathname === "/__rbssr/version") {
|
|
658
592
|
const version = runtimeOptions.reloadVersion?.() ?? 0;
|
|
659
593
|
return finalize(new Response(String(version), {
|
|
@@ -692,8 +626,20 @@ export function createServer(
|
|
|
692
626
|
return finalize(publicResponse, "static");
|
|
693
627
|
}
|
|
694
628
|
|
|
629
|
+
await runtimeOptions.onBeforeRequest?.();
|
|
630
|
+
|
|
695
631
|
const routeAdapter = await getRouteAdapter(activeConfig);
|
|
696
|
-
const
|
|
632
|
+
const devCacheBustKey = dev ? String(runtimeOptions.reloadVersion?.() ?? 0) : undefined;
|
|
633
|
+
const routeModuleLoadOptions = {
|
|
634
|
+
cacheBustKey: devCacheBustKey,
|
|
635
|
+
serverBytecode: activeConfig.serverBytecode,
|
|
636
|
+
devSourceImports: false,
|
|
637
|
+
};
|
|
638
|
+
const requestModuleLoadOptions = {
|
|
639
|
+
cacheBustKey: undefined,
|
|
640
|
+
serverBytecode: activeConfig.serverBytecode,
|
|
641
|
+
devSourceImports: dev,
|
|
642
|
+
};
|
|
697
643
|
const routeAssetsById = resolveAllRouteAssets({
|
|
698
644
|
dev,
|
|
699
645
|
runtimeOptions,
|
|
@@ -728,8 +674,7 @@ export function createServer(
|
|
|
728
674
|
const transitionPageMatch = routeAdapter.matchPage(targetUrl.pathname);
|
|
729
675
|
if (!transitionPageMatch) {
|
|
730
676
|
const rootModule = await loadRootOnlyModule(activeConfig.rootModule, {
|
|
731
|
-
|
|
732
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
677
|
+
...routeModuleLoadOptions,
|
|
733
678
|
});
|
|
734
679
|
const fallbackRoute: RouteModule = {
|
|
735
680
|
default: () => null,
|
|
@@ -769,11 +714,10 @@ export function createServer(
|
|
|
769
714
|
rootFilePath: activeConfig.rootModule,
|
|
770
715
|
layoutFiles: transitionPageMatch.route.layoutFiles,
|
|
771
716
|
routeFilePath: transitionPageMatch.route.filePath,
|
|
772
|
-
|
|
773
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
717
|
+
...routeModuleLoadOptions,
|
|
774
718
|
}),
|
|
775
|
-
loadGlobalMiddleware(activeConfig.middlewareFile,
|
|
776
|
-
loadNestedMiddleware(transitionPageMatch.route.middlewareFiles,
|
|
719
|
+
loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
|
|
720
|
+
loadNestedMiddleware(transitionPageMatch.route.middlewareFiles, requestModuleLoadOptions),
|
|
777
721
|
]);
|
|
778
722
|
const moduleMiddleware = extractRouteMiddleware(routeModules.route);
|
|
779
723
|
const routeAssets = routeAssetsById[transitionPageMatch.route.id] ?? null;
|
|
@@ -862,7 +806,7 @@ export function createServer(
|
|
|
862
806
|
const location = redirectResponse.headers.get("location");
|
|
863
807
|
if (location) {
|
|
864
808
|
const stream = createTransitionStream({
|
|
865
|
-
|
|
809
|
+
controlChunk: toRedirectChunk(location, redirectResponse.status),
|
|
866
810
|
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
867
811
|
});
|
|
868
812
|
return finalize(toTransitionStreamResponse(stream, redirectResponse.headers), "internal-transition");
|
|
@@ -952,17 +896,18 @@ export function createServer(
|
|
|
952
896
|
const redirectLocation = middlewareResponse.headers.get("location");
|
|
953
897
|
if (redirectLocation && isRedirectStatus(middlewareResponse.status)) {
|
|
954
898
|
const stream = createTransitionStream({
|
|
955
|
-
|
|
899
|
+
controlChunk: toRedirectChunk(redirectLocation, middlewareResponse.status),
|
|
956
900
|
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
957
901
|
});
|
|
958
902
|
return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
|
|
959
903
|
}
|
|
960
904
|
|
|
961
905
|
if (!transitionInitialChunk) {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
);
|
|
906
|
+
const stream = createTransitionStream({
|
|
907
|
+
controlChunk: toDocumentChunk(targetUrl.toString(), middlewareResponse.status),
|
|
908
|
+
sanitizeDeferredError: message => sanitizeErrorMessage(message, !dev),
|
|
909
|
+
});
|
|
910
|
+
return finalize(toTransitionStreamResponse(stream, middlewareResponse.headers), "internal-transition");
|
|
966
911
|
}
|
|
967
912
|
|
|
968
913
|
const stream = createTransitionStream({
|
|
@@ -975,7 +920,7 @@ export function createServer(
|
|
|
975
920
|
|
|
976
921
|
const apiMatch = routeAdapter.matchApi(url.pathname);
|
|
977
922
|
if (apiMatch) {
|
|
978
|
-
const apiModule = await loadApiRouteModule(apiMatch.route.filePath,
|
|
923
|
+
const apiModule = await loadApiRouteModule(apiMatch.route.filePath, requestModuleLoadOptions);
|
|
979
924
|
const methodHandler = getMethodHandler(apiModule as Record<string, unknown>, request.method);
|
|
980
925
|
|
|
981
926
|
if (typeof methodHandler !== "function") {
|
|
@@ -997,8 +942,8 @@ export function createServer(
|
|
|
997
942
|
};
|
|
998
943
|
|
|
999
944
|
const [globalMiddleware, routeMiddleware] = await Promise.all([
|
|
1000
|
-
loadGlobalMiddleware(activeConfig.middlewareFile,
|
|
1001
|
-
loadNestedMiddleware(apiMatch.route.middlewareFiles,
|
|
945
|
+
loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
|
|
946
|
+
loadNestedMiddleware(apiMatch.route.middlewareFiles, requestModuleLoadOptions),
|
|
1002
947
|
]);
|
|
1003
948
|
const allMiddleware = [...globalMiddleware, ...routeMiddleware];
|
|
1004
949
|
let apiPhase: RouteErrorPhase = "middleware";
|
|
@@ -1062,8 +1007,7 @@ export function createServer(
|
|
|
1062
1007
|
|
|
1063
1008
|
if (!pageMatch) {
|
|
1064
1009
|
const rootModule = await loadRootOnlyModule(activeConfig.rootModule, {
|
|
1065
|
-
|
|
1066
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
1010
|
+
...routeModuleLoadOptions,
|
|
1067
1011
|
});
|
|
1068
1012
|
const fallbackRoute: RouteModule = {
|
|
1069
1013
|
default: () => null,
|
|
@@ -1105,11 +1049,10 @@ export function createServer(
|
|
|
1105
1049
|
rootFilePath: activeConfig.rootModule,
|
|
1106
1050
|
layoutFiles: pageMatch.route.layoutFiles,
|
|
1107
1051
|
routeFilePath: pageMatch.route.filePath,
|
|
1108
|
-
|
|
1109
|
-
serverBytecode: activeConfig.serverBytecode,
|
|
1052
|
+
...routeModuleLoadOptions,
|
|
1110
1053
|
}),
|
|
1111
|
-
loadGlobalMiddleware(activeConfig.middlewareFile,
|
|
1112
|
-
loadNestedMiddleware(pageMatch.route.middlewareFiles,
|
|
1054
|
+
loadGlobalMiddleware(activeConfig.middlewareFile, requestModuleLoadOptions),
|
|
1055
|
+
loadNestedMiddleware(pageMatch.route.middlewareFiles, requestModuleLoadOptions),
|
|
1113
1056
|
]);
|
|
1114
1057
|
const moduleMiddleware = extractRouteMiddleware(routeModules.route);
|
|
1115
1058
|
|
|
@@ -254,7 +254,17 @@ export interface TransitionRedirectChunk {
|
|
|
254
254
|
status: number;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
export
|
|
257
|
+
export interface TransitionDocumentChunk {
|
|
258
|
+
type: "document";
|
|
259
|
+
location: string;
|
|
260
|
+
status: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type TransitionChunk =
|
|
264
|
+
| TransitionInitialChunk
|
|
265
|
+
| TransitionDeferredChunk
|
|
266
|
+
| TransitionRedirectChunk
|
|
267
|
+
| TransitionDocumentChunk;
|
|
258
268
|
|
|
259
269
|
export interface RouteModuleBundle {
|
|
260
270
|
root: RouteModule;
|
|
@@ -274,6 +284,7 @@ export interface ServerRuntimeOptions {
|
|
|
274
284
|
devAssets?: Record<string, BuildRouteAsset>;
|
|
275
285
|
getDevAssets?: () => Record<string, BuildRouteAsset>;
|
|
276
286
|
reloadVersion?: () => number;
|
|
287
|
+
routeManifestVersion?: () => number;
|
|
277
288
|
subscribeReload?: (listener: (version: number) => void) => (() => void) | void;
|
|
278
289
|
resolvePaths?: () => Partial<ResolvedConfig>;
|
|
279
290
|
onBeforeRequest?: () => Promise<void>;
|
|
@@ -85,6 +85,9 @@ export function sanitizeErrorMessage(error: unknown, production: boolean): strin
|
|
|
85
85
|
if (error instanceof Error) {
|
|
86
86
|
return error.message;
|
|
87
87
|
}
|
|
88
|
+
if (typeof error === "string") {
|
|
89
|
+
return error;
|
|
90
|
+
}
|
|
88
91
|
return Bun.inspect(error);
|
|
89
92
|
}
|
|
90
93
|
return "Internal Server Error";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-bun-ssr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"test": "bun bin/rbssr.ts test",
|
|
47
47
|
"test:unit": "bun test ./tests/unit",
|
|
48
48
|
"test:integration": "bun test ./tests/integration",
|
|
49
|
+
"test:consumer": "RBSSR_RUN_CONSUMER_SMOKE=1 bun test ./tests/integration/package-smoke.test.ts",
|
|
49
50
|
"test:e2e": "bunx playwright test",
|
|
50
51
|
"docs:dev": "bun run scripts/generate-api-docs.ts && bun run scripts/build-docs-manifest.ts && bun run scripts/build-search-index.ts && bun run scripts/build-blog-manifest.ts && bun run scripts/build-sitemap.ts && bun bin/rbssr.ts dev",
|
|
51
52
|
"docs:build": "bun run scripts/docs-build.ts",
|