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.
@@ -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
- redirectChunk?: TransitionRedirectChunk;
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.redirectChunk) {
449
- controller.enqueue(toTransitionChunkLine(options.redirectChunk));
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 reloadVersion = dev ? runtimeOptions.reloadVersion?.() ?? 0 : 0;
573
- return `${normalizeSlashes(activeConfig.routesDir)}|${dev ? "dev" : "prod"}|${reloadVersion}`;
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}-v${reloadVersion}`)
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 cacheBustKey = dev ? String(runtimeOptions.reloadVersion?.() ?? Date.now()) : undefined;
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
- cacheBustKey,
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
- cacheBustKey,
773
- serverBytecode: activeConfig.serverBytecode,
717
+ ...routeModuleLoadOptions,
774
718
  }),
775
- loadGlobalMiddleware(activeConfig.middlewareFile, cacheBustKey),
776
- loadNestedMiddleware(transitionPageMatch.route.middlewareFiles, cacheBustKey),
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
- redirectChunk: toRedirectChunk(location, redirectResponse.status),
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
- redirectChunk: toRedirectChunk(redirectLocation, middlewareResponse.status),
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
- return finalize(
963
- new Response("Transition fallback required for non-streamable response.", { status: 409 }),
964
- "internal-transition",
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, cacheBustKey);
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, cacheBustKey),
1001
- loadNestedMiddleware(apiMatch.route.middlewareFiles, cacheBustKey),
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
- cacheBustKey,
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
- cacheBustKey,
1109
- serverBytecode: activeConfig.serverBytecode,
1052
+ ...routeModuleLoadOptions,
1110
1053
  }),
1111
- loadGlobalMiddleware(activeConfig.middlewareFile, cacheBustKey),
1112
- loadNestedMiddleware(pageMatch.route.middlewareFiles, cacheBustKey),
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 type TransitionChunk = TransitionInitialChunk | TransitionDeferredChunk | TransitionRedirectChunk;
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.1.1",
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",