@reckona/mreact-router 0.0.115 → 0.0.117

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.115",
3
+ "version": "0.0.117",
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.115",
109
- "@reckona/mreact-compat": "0.0.115",
110
- "@reckona/mreact-devtools": "0.0.115",
111
- "@reckona/mreact-query": "0.0.115",
112
- "@reckona/mreact-reactive-core": "0.0.115",
113
- "@reckona/mreact-compiler": "0.0.115",
114
- "@reckona/mreact-reactive-dom": "0.0.115",
115
- "@reckona/mreact-server": "0.0.115",
116
- "@reckona/mreact-shared": "0.0.115"
108
+ "@reckona/mreact": "0.0.117",
109
+ "@reckona/mreact-devtools": "0.0.117",
110
+ "@reckona/mreact-compiler": "0.0.117",
111
+ "@reckona/mreact-query": "0.0.117",
112
+ "@reckona/mreact-reactive-core": "0.0.117",
113
+ "@reckona/mreact-reactive-dom": "0.0.117",
114
+ "@reckona/mreact-compat": "0.0.117",
115
+ "@reckona/mreact-server": "0.0.117",
116
+ "@reckona/mreact-shared": "0.0.117"
117
117
  },
118
118
  "peerDependencies": {
119
119
  "vite": ">=8 <9"
120
120
  },
121
121
  "optionalDependencies": {
122
- "@reckona/mreact-router-native": "0.0.115"
122
+ "@reckona/mreact-router-native": "0.0.117"
123
123
  }
124
124
  }
@@ -190,6 +190,7 @@ export interface CloudflareBuiltRequestHandlerOptions<Env = unknown> extends Omi
190
190
  }
191
191
 
192
192
  export interface CloudflareClientManifest {
193
+ assets?: readonly string[] | undefined;
193
194
  publicAssets?: readonly string[] | undefined;
194
195
  routes: ClientRouteManifestEntry[];
195
196
  }
@@ -380,12 +381,20 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
380
381
 
381
382
  const modulePreload = cloudflareModulePreloadTag(context.clientManifest, context.route.path);
382
383
  const metadata = await resolveCloudflareRouteMetadata([pageModule], props);
384
+ const body = withCloudflareHydrationMarkers({
385
+ data,
386
+ html: rendered,
387
+ manifest: context.clientManifest,
388
+ params: context.params,
389
+ request,
390
+ routePath: context.route.path,
391
+ });
383
392
  const documented =
384
393
  options.document === undefined
385
- ? defaultCloudflareDocument(rendered, modulePreload, metadata)
394
+ ? defaultCloudflareDocument(body, modulePreload, metadata)
386
395
  : await options.document({
387
396
  ...props,
388
- body: rendered,
397
+ body,
389
398
  modulePreload,
390
399
  });
391
400
 
@@ -709,6 +718,14 @@ export function cloudflareClientAssetPaths(
709
718
  }
710
719
  }
711
720
 
721
+ for (const asset of manifest.assets ?? []) {
722
+ const path = safeClientAssetPath(prefix, asset);
723
+
724
+ if (path !== undefined) {
725
+ paths.add(path);
726
+ }
727
+ }
728
+
712
729
  for (const extraPath of options.extraPaths ?? []) {
713
730
  const path = safeClientAssetPath(prefix, extraPath);
714
731
 
@@ -1145,6 +1162,81 @@ function safeClientAssetPath(prefix: string, asset: string | undefined): string
1145
1162
  return `${prefix}${segments.join("/")}`;
1146
1163
  }
1147
1164
 
1165
+ function withCloudflareHydrationMarkers(options: {
1166
+ data: unknown;
1167
+ html: string;
1168
+ manifest: CloudflareClientManifest;
1169
+ params: Record<string, readonly string[] | string>;
1170
+ request: Request;
1171
+ routePath: string;
1172
+ }): string {
1173
+ const marker = cloudflareHydrationMarkerParts(options);
1174
+
1175
+ return marker.prefix === "" && marker.suffix === ""
1176
+ ? options.html
1177
+ : `${marker.prefix}${options.html}${marker.suffix}`;
1178
+ }
1179
+
1180
+ function cloudflareHydrationMarkerParts(options: {
1181
+ data: unknown;
1182
+ manifest: CloudflareClientManifest;
1183
+ params: Record<string, readonly string[] | string>;
1184
+ request: Request;
1185
+ routePath: string;
1186
+ }): { prefix: string; suffix: string } {
1187
+ const route = options.manifest.routes.find(
1188
+ (route) => route.path === options.routePath && route.client === true,
1189
+ );
1190
+
1191
+ if (route?.script === undefined) {
1192
+ return { prefix: "", suffix: "" };
1193
+ }
1194
+
1195
+ const routeId = route.routeId ?? cloudflareRouteIdForPath(options.routePath);
1196
+ const escapedRouteId = escapeHtmlAttribute(routeId);
1197
+ const propsJson = escapeScriptJson(
1198
+ JSON.stringify({
1199
+ params: options.params,
1200
+ request: { url: options.request.url },
1201
+ data: options.data,
1202
+ }),
1203
+ );
1204
+ const clientReferencesJson =
1205
+ route.clientReferenceManifest === undefined || route.clientReferenceManifest.length === 0
1206
+ ? undefined
1207
+ : escapeScriptJson(JSON.stringify(route.clientReferenceManifest));
1208
+
1209
+ return {
1210
+ prefix: `<div data-mreact-route-id="${escapedRouteId}">`,
1211
+ suffix: [
1212
+ "</div>",
1213
+ `<script type="application/json" id="mreact-props-${escapedRouteId}">${propsJson}</script>`,
1214
+ clientReferencesJson === undefined
1215
+ ? undefined
1216
+ : `<script type="application/json" id="mreact-client-references-${escapedRouteId}">${clientReferencesJson}</script>`,
1217
+ `<script type="module" src="/_mreact/client/${escapeHtmlAttribute(route.script)}"></script>`,
1218
+ ]
1219
+ .filter((part): part is string => part !== undefined)
1220
+ .join(""),
1221
+ };
1222
+ }
1223
+
1224
+ function escapeScriptJson(json: string): string {
1225
+ return json.replaceAll("<", "\\u003c");
1226
+ }
1227
+
1228
+ function cloudflareRouteIdForPath(path: string): string {
1229
+ if (path === "/") {
1230
+ return "index";
1231
+ }
1232
+
1233
+ return path
1234
+ .slice(1)
1235
+ .replaceAll("/", "_")
1236
+ .replaceAll(":", "_")
1237
+ .replace(/[^A-Za-z0-9_$-]/g, "_");
1238
+ }
1239
+
1148
1240
  function unsafeAssetSegment(segment: string): boolean {
1149
1241
  if (segment === "" || segment === "." || segment === "..") {
1150
1242
  return true;
package/src/build.ts CHANGED
@@ -425,7 +425,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
425
425
  ...route,
426
426
  file: relative(project.projectRoot, route.file),
427
427
  }));
428
- const [serverModuleArtifacts, clientRoutes] = await Promise.all([
428
+ const [serverModuleArtifacts, clientBundle] = await Promise.all([
429
429
  timingSink === undefined
430
430
  ? writeServerModuleArtifactFiles(serverDir, serverModules, generatedImportPolicy.runtimePackages)
431
431
  : timeBuildPhase(timingSink, "serverModuleArtifacts", () =>
@@ -465,6 +465,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
465
465
  }),
466
466
  ),
467
467
  ]);
468
+ const clientRoutes = clientBundle.routes;
468
469
  const navigationRuntimeScript = clientRoutes.some(
469
470
  (route) => route.navigation === true && !route.client,
470
471
  )
@@ -482,6 +483,12 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
482
483
  ? { ...route, navigationScript: navigationRuntimeScript }
483
484
  : route,
484
485
  );
486
+ const clientManifestAssets = Array.from(
487
+ new Set([
488
+ ...clientBundle.assets,
489
+ ...(navigationRuntimeScript === undefined ? [] : [navigationRuntimeScript]),
490
+ ]),
491
+ ).sort();
485
492
  const prerenderedRoutes =
486
493
  timingSink === undefined
487
494
  ? await prerenderStaticRoutes({
@@ -571,6 +578,7 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
571
578
  : { serverModuleRenderFiles: serverModuleArtifacts.renderFiles }),
572
579
  } satisfies BuiltServerManifest;
573
580
  const clientManifest = {
581
+ ...(clientManifestAssets.length === 0 ? {} : { assets: clientManifestAssets }),
574
582
  ...(publicAssets.length === 0 ? {} : { publicAssets }),
575
583
  routes: clientManifestRoutes,
576
584
  };
@@ -2999,7 +3007,7 @@ async function renderCloudflareStringRoute(props) {
2999
3007
  for (const shell of layoutShells) {
3000
3008
  html += shell.prefix;
3001
3009
  }
3002
- html += String(await pageComponent(props) ?? "");
3010
+ html += withCloudflareHydrationMarkers(props, String(await pageComponent(props) ?? ""));
3003
3011
  for (const shell of [...layoutShells].reverse()) {
3004
3012
  html += shell.suffix;
3005
3013
  }
@@ -3171,7 +3179,7 @@ async function renderCloudflareStringRoute(props) {
3171
3179
  for (const shell of layoutShells) {
3172
3180
  html += shell.prefix;
3173
3181
  }
3174
- html += String(await pageComponent(props) ?? "");
3182
+ html += withCloudflareHydrationMarkers(props, String(await pageComponent(props) ?? ""));
3175
3183
  for (const shell of [...layoutShells].reverse()) {
3176
3184
  html += shell.suffix;
3177
3185
  }
@@ -3280,7 +3288,10 @@ function renderCloudflareStreamRoute(props) {
3280
3288
  const shell = layoutShells[index];
3281
3289
  $sink.append(index === 0 ? injectCloudflareHead(shell.prefix, metadata, routeHeadTags) : shell.prefix);
3282
3290
  }
3291
+ const hydrationMarker = cloudflareHydrationMarkerParts(props);
3292
+ $sink.append(hydrationMarker.prefix);
3283
3293
  await pageComponent($sink, props);
3294
+ $sink.append(hydrationMarker.suffix);
3284
3295
  renderOutOfOrderReorderScript($sink);
3285
3296
  for (const shell of [...layoutShells].reverse()) {
3286
3297
  $sink.append(shell.suffix);
@@ -3403,6 +3414,54 @@ function readSlotName(attributes) {
3403
3414
  return match?.[1] ?? match?.[2];
3404
3415
  }
3405
3416
 
3417
+ function withCloudflareHydrationMarkers(props, html) {
3418
+ const marker = cloudflareHydrationMarkerParts(props);
3419
+ return marker.prefix === "" && marker.suffix === "" ? html : \`\${marker.prefix}\${html}\${marker.suffix}\`;
3420
+ }
3421
+
3422
+ function cloudflareHydrationMarkerParts(props) {
3423
+ const route = props.clientManifest.routes.find((route) => route.path === props.route.path && route.client === true);
3424
+ if (route?.script === undefined) {
3425
+ return { prefix: "", suffix: "" };
3426
+ }
3427
+ const routeId = route.routeId ?? cloudflareRouteIdForPath(props.route.path);
3428
+ const escapedRouteId = escapeHtmlAttribute(routeId);
3429
+ const propsJson = escapeScriptJson(JSON.stringify({
3430
+ params: props.params,
3431
+ request: { url: props.request.url },
3432
+ data: props.data,
3433
+ }));
3434
+ const clientReferencesJson = route.clientReferenceManifest === undefined || route.clientReferenceManifest.length === 0
3435
+ ? undefined
3436
+ : escapeScriptJson(JSON.stringify(route.clientReferenceManifest));
3437
+ return {
3438
+ prefix: \`<div data-mreact-route-id="\${escapedRouteId}">\`,
3439
+ suffix: [
3440
+ "</div>",
3441
+ \`<script type="application/json" id="mreact-props-\${escapedRouteId}">\${propsJson}</script>\`,
3442
+ clientReferencesJson === undefined
3443
+ ? undefined
3444
+ : \`<script type="application/json" id="mreact-client-references-\${escapedRouteId}">\${clientReferencesJson}</script>\`,
3445
+ \`<script type="module" src="/_mreact/client/\${escapeHtmlAttribute(route.script)}"></script>\`,
3446
+ ].filter((part) => part !== undefined).join(""),
3447
+ };
3448
+ }
3449
+
3450
+ function escapeScriptJson(json) {
3451
+ return json.replaceAll("<", "\\\\u003c");
3452
+ }
3453
+
3454
+ function cloudflareRouteIdForPath(path) {
3455
+ if (path === "/") {
3456
+ return "index";
3457
+ }
3458
+ return path
3459
+ .slice(1)
3460
+ .replaceAll("/", "_")
3461
+ .replaceAll(":", "_")
3462
+ .replace(/[^A-Za-z0-9_$-]/g, "_");
3463
+ }
3464
+
3406
3465
  async function resolveRouteMetadata(modules, props) {
3407
3466
  const metadata = [];
3408
3467
  const context = {
@@ -4178,6 +4237,11 @@ function viteManifestFromClientRoutes(routes: ClientRouteManifestEntry[]): Recor
4178
4237
  return manifest;
4179
4238
  }
4180
4239
 
4240
+ interface ClientRouteBundleManifest {
4241
+ assets: string[];
4242
+ routes: ClientRouteManifestEntry[];
4243
+ }
4244
+
4181
4245
  async function writeClientRouteBundles(options: {
4182
4246
  appDir: string;
4183
4247
  assetBaseUrl?: string | undefined;
@@ -4191,7 +4255,7 @@ async function writeClientRouteBundles(options: {
4191
4255
  sourceMapDir: string;
4192
4256
  sourceMaps: AppRouterClientSourceMapMode;
4193
4257
  vitePlugins?: readonly PluginOption[] | undefined;
4194
- }): Promise<ClientRouteManifestEntry[]> {
4258
+ }): Promise<ClientRouteBundleManifest> {
4195
4259
  type PreparedClientRouteEntry = {
4196
4260
  build: BuildClientRouteOutputOptions;
4197
4261
  css: string[];
@@ -4292,7 +4356,10 @@ async function writeClientRouteBundles(options: {
4292
4356
  );
4293
4357
 
4294
4358
  if (clientEntries.length === 0) {
4295
- return entries.flatMap((entry) => ("manifest" in entry ? [entry.manifest] : []));
4359
+ return {
4360
+ assets: [],
4361
+ routes: entries.flatMap((entry) => ("manifest" in entry ? [entry.manifest] : [])),
4362
+ };
4296
4363
  }
4297
4364
 
4298
4365
  let output: Awaited<ReturnType<typeof buildClientRouteBatchOutput>>;
@@ -4354,7 +4421,20 @@ async function writeClientRouteBundles(options: {
4354
4421
  await writeFile(join(options.clientDir, asset.fileName), source);
4355
4422
  }
4356
4423
 
4357
- return entries.map((entry) => {
4424
+ const generatedAssets = new Set<string>();
4425
+
4426
+ for (const chunk of output.chunks) {
4427
+ generatedAssets.add(chunk.fileName);
4428
+ if (options.sourceMaps === "linked" && chunk.map !== undefined) {
4429
+ generatedAssets.add(`${chunk.fileName}.map`);
4430
+ }
4431
+ }
4432
+
4433
+ for (const asset of output.assets ?? []) {
4434
+ generatedAssets.add(asset.fileName);
4435
+ }
4436
+
4437
+ const routes = entries.map((entry) => {
4358
4438
  if (!("build" in entry)) {
4359
4439
  return entry.manifest;
4360
4440
  }
@@ -4378,6 +4458,10 @@ async function writeClientRouteBundles(options: {
4378
4458
  path: entry.route.path,
4379
4459
  kind: entry.route.kind,
4380
4460
  client: true,
4461
+ ...(entry.build.clientReferenceManifest === undefined ||
4462
+ entry.build.clientReferenceManifest.length === 0
4463
+ ? {}
4464
+ : { clientReferenceManifest: entry.build.clientReferenceManifest }),
4381
4465
  ...(entry.css.length === 0 ? {} : { css: entry.css }),
4382
4466
  ...(routeOutput.chunk.imports.length === 0 ? {} : { imports: routeOutput.chunk.imports }),
4383
4467
  ...(entry.navigation ? { navigation: entry.navigation } : {}),
@@ -4387,6 +4471,11 @@ async function writeClientRouteBundles(options: {
4387
4471
  devScript: clientScriptForPath(entry.route.path),
4388
4472
  };
4389
4473
  });
4474
+
4475
+ return {
4476
+ assets: Array.from(generatedAssets).sort(),
4477
+ routes,
4478
+ };
4390
4479
  }
4391
4480
 
4392
4481
  async function writeRouteCssAssetBatches(options: {
package/src/client.ts CHANGED
@@ -57,6 +57,7 @@ export interface ClientRouteManifestEntry {
57
57
  path: string;
58
58
  kind: AppRoute["kind"];
59
59
  client: boolean;
60
+ clientReferenceManifest?: readonly ClientReferenceMetadata[] | undefined;
60
61
  devScript?: string;
61
62
  imports?: readonly string[];
62
63
  navigation?: boolean;