@reckona/mreact-router 0.0.126 → 0.0.128

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reckona/mreact-router",
3
- "version": "0.0.126",
3
+ "version": "0.0.128",
4
4
  "description": "File-system app router, SSR, actions, and deployment adapters for mreact.",
5
5
  "keywords": [
6
6
  "jsx",
@@ -105,20 +105,20 @@
105
105
  },
106
106
  "dependencies": {
107
107
  "typescript": "^6.0.3",
108
- "@reckona/mreact": "0.0.126",
109
- "@reckona/mreact-compat": "0.0.126",
110
- "@reckona/mreact-reactive-core": "0.0.126",
111
- "@reckona/mreact-query": "0.0.126",
112
- "@reckona/mreact-compiler": "0.0.126",
113
- "@reckona/mreact-devtools": "0.0.126",
114
- "@reckona/mreact-reactive-dom": "0.0.126",
115
- "@reckona/mreact-shared": "0.0.126",
116
- "@reckona/mreact-server": "0.0.126"
108
+ "@reckona/mreact": "0.0.128",
109
+ "@reckona/mreact-compat": "0.0.128",
110
+ "@reckona/mreact-compiler": "0.0.128",
111
+ "@reckona/mreact-query": "0.0.128",
112
+ "@reckona/mreact-reactive-core": "0.0.128",
113
+ "@reckona/mreact-devtools": "0.0.128",
114
+ "@reckona/mreact-reactive-dom": "0.0.128",
115
+ "@reckona/mreact-server": "0.0.128",
116
+ "@reckona/mreact-shared": "0.0.128"
117
117
  },
118
118
  "peerDependencies": {
119
119
  "vite": ">=8 <9"
120
120
  },
121
121
  "optionalDependencies": {
122
- "@reckona/mreact-router-native": "0.0.126"
122
+ "@reckona/mreact-router-native": "0.0.128"
123
123
  }
124
124
  }
@@ -1,6 +1,14 @@
1
1
  import type { BuiltPrerenderedRoute, BuiltServerManifest } from "../build.js";
2
2
  import type { ClientRouteManifestEntry } from "../client.js";
3
3
  import type { AppRouterResponseHook } from "../render.js";
4
+ import {
5
+ __MREACT_QUERY_STATE_SCRIPT_ID,
6
+ createQueryClient,
7
+ dehydrate,
8
+ runWithQueryClient,
9
+ type DehydratedQueryClient,
10
+ type QueryClient,
11
+ } from "@reckona/mreact-query";
4
12
  import type {
5
13
  GenerateMetadataContext,
6
14
  ManifestDescriptor,
@@ -80,6 +88,7 @@ export interface CloudflareBuiltRouteRenderContext<
80
88
  export interface CloudflareRouteModuleLoaderContext<
81
89
  Env = unknown,
82
90
  > extends CloudflareBuiltRouteRenderContext<Env> {
91
+ queryClient: QueryClient;
83
92
  request: Request;
84
93
  }
85
94
 
@@ -94,6 +103,7 @@ export interface CloudflareRouteModuleComponentProps<
94
103
  Env = unknown,
95
104
  > extends CloudflareBuiltRouteRenderContext<Env> {
96
105
  data: Data;
106
+ queryClient: QueryClient;
97
107
  request: Request;
98
108
  }
99
109
 
@@ -328,6 +338,7 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
328
338
 
329
339
  const pageModule = module as CloudflareRouteModule<unknown, Env>;
330
340
  const component = selectCloudflarePageComponent(pageModule);
341
+ const queryClient = createQueryClient();
331
342
 
332
343
  if (component === undefined) {
333
344
  return new Response(
@@ -341,12 +352,16 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
341
352
 
342
353
  const loaderContext = {
343
354
  ...context,
355
+ queryClient,
344
356
  request,
345
357
  };
346
358
  let data: unknown;
347
359
 
348
360
  try {
349
- data = pageModule.loader === undefined ? undefined : await pageModule.loader(loaderContext);
361
+ data =
362
+ pageModule.loader === undefined
363
+ ? undefined
364
+ : await runWithQueryClient(queryClient, () => pageModule.loader!(loaderContext));
350
365
  } catch (error) {
351
366
  if (error instanceof Response) {
352
367
  return error;
@@ -361,12 +376,13 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
361
376
  const props = {
362
377
  ...context,
363
378
  data,
379
+ queryClient,
364
380
  request,
365
381
  };
366
382
  let rendered: Awaited<ReturnType<typeof component>>;
367
383
 
368
384
  try {
369
- rendered = await component(props);
385
+ rendered = await runWithQueryClient(queryClient, () => component(props));
370
386
  } catch (error) {
371
387
  if (isNotFoundError(error)) {
372
388
  return cloudflareNotFoundResponse(request);
@@ -380,7 +396,9 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
380
396
  }
381
397
 
382
398
  const modulePreload = cloudflareModulePreloadTag(context.clientManifest, context.route.path);
383
- const metadata = await resolveCloudflareRouteMetadata([pageModule], props);
399
+ const metadata = await runWithQueryClient(queryClient, () =>
400
+ resolveCloudflareRouteMetadata([pageModule], props),
401
+ );
384
402
  const body = withCloudflareHydrationMarkers({
385
403
  data,
386
404
  html: rendered,
@@ -392,16 +410,22 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
392
410
  const documented =
393
411
  options.document === undefined
394
412
  ? defaultCloudflareDocument(body, modulePreload, metadata)
395
- : await options.document({
396
- ...props,
397
- body,
398
- modulePreload,
399
- });
413
+ : await runWithQueryClient(queryClient, () =>
414
+ options.document!({
415
+ ...props,
416
+ body,
417
+ modulePreload,
418
+ }),
419
+ );
420
+ const documentedWithQueryState = await injectCloudflareQueryState(
421
+ documented,
422
+ dehydrate(queryClient),
423
+ );
400
424
 
401
425
  return withDefaultSecurityHeaders(
402
- documented instanceof Response
403
- ? documented
404
- : new Response(documented, {
426
+ documentedWithQueryState instanceof Response
427
+ ? documentedWithQueryState
428
+ : new Response(documentedWithQueryState, {
405
429
  headers: { "content-type": "text/html; charset=utf-8" },
406
430
  }),
407
431
  request,
@@ -409,6 +433,42 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
409
433
  };
410
434
  }
411
435
 
436
+ async function injectCloudflareQueryState<T extends Response | string>(
437
+ document: T,
438
+ state: DehydratedQueryClient,
439
+ ): Promise<T | Response | string> {
440
+ if (state.queries.length === 0) {
441
+ return document;
442
+ }
443
+
444
+ if (typeof document === "string") {
445
+ return injectCloudflareQueryStateScript(document, state);
446
+ }
447
+
448
+ const contentType = document.headers.get("content-type") ?? "";
449
+ if (!/^text\/html\b/i.test(contentType)) {
450
+ return document;
451
+ }
452
+
453
+ const headers = new Headers(document.headers);
454
+ headers.delete("content-length");
455
+ return new Response(injectCloudflareQueryStateScript(await document.text(), state), {
456
+ headers,
457
+ status: document.status,
458
+ statusText: document.statusText,
459
+ });
460
+ }
461
+
462
+ function injectCloudflareQueryStateScript(html: string, state: DehydratedQueryClient): string {
463
+ const script = `<script type="application/json" id="${__MREACT_QUERY_STATE_SCRIPT_ID}">${escapeJsonForHtml(
464
+ JSON.stringify(state),
465
+ )}</script>`;
466
+
467
+ return /<\/body>/i.test(html)
468
+ ? html.replace(/<\/body>/i, () => `${script}</body>`)
469
+ : `${html}${script}`;
470
+ }
471
+
412
472
  async function dispatchCloudflareServerRoute<Env>(
413
473
  module: CloudflareServerRouteModule<Env>,
414
474
  request: Request,
@@ -1225,6 +1285,15 @@ function escapeScriptJson(json: string): string {
1225
1285
  return json.replaceAll("<", "\\u003c");
1226
1286
  }
1227
1287
 
1288
+ function escapeJsonForHtml(value: string): string {
1289
+ return value
1290
+ .replaceAll("&", "\\u0026")
1291
+ .replaceAll("<", "\\u003c")
1292
+ .replaceAll(">", "\\u003e")
1293
+ .replaceAll("\u2028", "\\u2028")
1294
+ .replaceAll("\u2029", "\\u2029");
1295
+ }
1296
+
1228
1297
  function cloudflareRouteIdForPath(path: string): string {
1229
1298
  if (path === "/") {
1230
1299
  return "index";
package/src/client.ts CHANGED
@@ -1110,7 +1110,15 @@ function isStyleModuleSpecifier(source: string): boolean {
1110
1110
  const styleModuleExtensions = new Set([".css", ".less", ".sass", ".scss", ".styl", ".stylus"]);
1111
1111
 
1112
1112
  function isClientBoundaryFallbackEligibleSource(source: string): boolean {
1113
- return !/\bon[A-Z][A-Za-z0-9_$]*\s*=/u.test(source) && !/\bglobalThis\b/u.test(source);
1113
+ const sourceWithoutGuardedUndefinedCallbacks = source.replaceAll(
1114
+ /\bon[A-Z][A-Za-z0-9_$]*\s*=\s*\{\s*props\.[A-Za-z_$][\w$]*\s*===\s*undefined\s*\?\s*undefined\s*:/gu,
1115
+ "",
1116
+ );
1117
+
1118
+ return (
1119
+ !/\bon[A-Z][A-Za-z0-9_$]*\s*=/u.test(sourceWithoutGuardedUndefinedCallbacks) &&
1120
+ !/\bglobalThis\b/u.test(source)
1121
+ );
1114
1122
  }
1115
1123
 
1116
1124
  function renderedImportedExportNames(