@tyndall/react 0.0.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.
@@ -0,0 +1,750 @@
1
+ import React from "react";
2
+ import { createRequire } from "node:module";
3
+ import { join, relative, sep } from "node:path";
4
+ import { mergeHeadDescriptors, serializeProps } from "@tyndall/core";
5
+ import { collectHeadFromTree } from "./head.js";
6
+ const REACT_ELEMENT = Symbol.for("react.element");
7
+ const REACT_TRANSITIONAL_ELEMENT = Symbol.for("react.transitional.element");
8
+ const HEAD_ELEMENT = Symbol.for("hyper.head");
9
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10
+ const isElementLike = (value) => isRecord(value) &&
11
+ (value.$$typeof === REACT_ELEMENT || value.$$typeof === REACT_TRANSITIONAL_ELEMENT) &&
12
+ "type" in value &&
13
+ "props" in value;
14
+ const isHeadMarker = (element) => {
15
+ if (element.type === HEAD_ELEMENT) {
16
+ return true;
17
+ }
18
+ if (typeof element.type === "function") {
19
+ return Boolean(element.type.__hyperHead);
20
+ }
21
+ return false;
22
+ };
23
+ const escapeHtml = (value) => value
24
+ .replace(/&/g, "&")
25
+ .replace(/</g, "&lt;")
26
+ .replace(/>/g, "&gt;")
27
+ .replace(/\"/g, "&quot;");
28
+ const renderAttributes = (props) => {
29
+ const attrs = [];
30
+ for (const [key, value] of Object.entries(props)) {
31
+ if (key === "children" || value === undefined || value === null) {
32
+ continue;
33
+ }
34
+ if (typeof value === "function") {
35
+ continue;
36
+ }
37
+ attrs.push(`${key}=\"${escapeHtml(String(value))}\"`);
38
+ }
39
+ return attrs.length ? ` ${attrs.join(" ")}` : "";
40
+ };
41
+ const renderElementToString = (node) => {
42
+ if (node === null || node === undefined || typeof node === "boolean") {
43
+ return "";
44
+ }
45
+ if (Array.isArray(node)) {
46
+ return node.map(renderElementToString).join("");
47
+ }
48
+ if (typeof node === "string" || typeof node === "number") {
49
+ return escapeHtml(String(node));
50
+ }
51
+ if (!isElementLike(node)) {
52
+ return "";
53
+ }
54
+ if (isHeadMarker(node)) {
55
+ return "";
56
+ }
57
+ if (typeof node.type === "function") {
58
+ return renderElementToString(node.type(node.props));
59
+ }
60
+ if (typeof node.type !== "string") {
61
+ return "";
62
+ }
63
+ const children = renderElementToString(node.props.children);
64
+ const attrs = renderAttributes(node.props);
65
+ return `<${node.type}${attrs}>${children}</${node.type}>`;
66
+ };
67
+ const resolveReactDomServer = async () => {
68
+ try {
69
+ return await import("react-dom/server");
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ };
75
+ const renderWithFallback = async (element) => {
76
+ const reactDomServer = await resolveReactDomServer();
77
+ if (reactDomServer?.renderToString) {
78
+ try {
79
+ return reactDomServer.renderToString(element);
80
+ }
81
+ catch {
82
+ // Fall back when custom markers (e.g., <Head>) are not React-renderable.
83
+ }
84
+ }
85
+ // Fallback renderer keeps local tests/dev flows working without react-dom.
86
+ return renderElementToString(element);
87
+ };
88
+ const resolvePageImport = (ctx) => {
89
+ const options = ctx.uiOptions ?? ctx.adapterOptions;
90
+ const pageModule = options?.pageModule;
91
+ const specifier = typeof pageModule === "string" ? pageModule : "./page";
92
+ return JSON.stringify(specifier);
93
+ };
94
+ const resolveHydrationMode = (ctx) => {
95
+ const options = ctx.uiOptions ?? ctx.adapterOptions;
96
+ return options?.hydration === "islands" ? "islands" : "full";
97
+ };
98
+ const resolveNavigationMode = (ctx) => {
99
+ const options = ctx.uiOptions ?? ctx.adapterOptions;
100
+ return options?.navigationMode === "url" ? "url" : "client";
101
+ };
102
+ const resolveClientRenderMode = (ctx) => {
103
+ const options = ctx.uiOptions ?? ctx.adapterOptions;
104
+ return options?.clientRenderMode === "module" ? "module" : "payload";
105
+ };
106
+ const resolveClientRouteModules = (ctx) => {
107
+ const options = ctx.uiOptions ?? ctx.adapterOptions;
108
+ const raw = options?.clientRouteModules;
109
+ if (!isRecord(raw)) {
110
+ return {};
111
+ }
112
+ const entries = {};
113
+ for (const [routeId, specifier] of Object.entries(raw)) {
114
+ if (typeof specifier !== "string") {
115
+ continue;
116
+ }
117
+ entries[routeId] = specifier;
118
+ }
119
+ return entries;
120
+ };
121
+ const resolveNestedLayoutsEnabled = (options) => {
122
+ if (!options) {
123
+ return true;
124
+ }
125
+ if ("nestedLayouts" in options) {
126
+ return options.nestedLayouts !== false;
127
+ }
128
+ return true;
129
+ };
130
+ const findRouteRecord = (routeGraph, routeId) => {
131
+ if (!routeGraph || !routeId) {
132
+ return null;
133
+ }
134
+ return routeGraph.routes.find((route) => route.id === routeId) ?? null;
135
+ };
136
+ const resolveLayoutFilesForRoute = (routeGraph, routeId) => {
137
+ const route = findRouteRecord(routeGraph, routeId);
138
+ return route?.layoutFiles ?? [];
139
+ };
140
+ const resolveLayoutModulePath = (layoutFile, rootDir, routeRoot) => {
141
+ if (!rootDir || !routeRoot) {
142
+ return layoutFile;
143
+ }
144
+ const relativePath = relative(rootDir, layoutFile);
145
+ if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`)) {
146
+ return layoutFile;
147
+ }
148
+ return join(routeRoot, relativePath);
149
+ };
150
+ const toPosixPath = (value) => value.split(sep).join("/");
151
+ const toFileUrl = (value) => {
152
+ const normalized = toPosixPath(value);
153
+ const prefix = normalized.startsWith("/") ? "" : "/";
154
+ return `file://${prefix}${encodeURI(normalized)}`;
155
+ };
156
+ const toImportSpecifier = (fromDir, targetPath) => {
157
+ const relativePath = toPosixPath(relative(fromDir, targetPath));
158
+ if (!relativePath || relativePath === ".") {
159
+ return "./";
160
+ }
161
+ return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
162
+ };
163
+ const resolveLayoutSpecifiersForRoute = (ctx, routeId) => {
164
+ if (!ctx.entryDir) {
165
+ return [];
166
+ }
167
+ const layoutFiles = resolveLayoutFilesForRoute(ctx.routeGraph, routeId);
168
+ if (layoutFiles.length === 0) {
169
+ return [];
170
+ }
171
+ return layoutFiles.map((layoutFile) => {
172
+ const modulePath = resolveLayoutModulePath(layoutFile, ctx.rootDir, ctx.routeRoot);
173
+ return toImportSpecifier(ctx.entryDir ?? "", modulePath);
174
+ });
175
+ };
176
+ const buildClientRouteLayoutLoaderSource = (layoutModules) => {
177
+ const declarations = [];
178
+ const entries = [];
179
+ let index = 0;
180
+ for (const [routeId, specifiers] of Object.entries(layoutModules).sort(([a], [b]) => a.localeCompare(b))) {
181
+ if (!Array.isArray(specifiers) || specifiers.length === 0) {
182
+ entries.push(`${JSON.stringify(routeId)}: []`);
183
+ continue;
184
+ }
185
+ const loaders = specifiers.map((specifier) => {
186
+ const loaderName = `loadLayoutModule${index++}`;
187
+ declarations.push(`const ${loaderName} = () => import(${JSON.stringify(specifier)});`);
188
+ return loaderName;
189
+ });
190
+ entries.push(`${JSON.stringify(routeId)}: [${loaders.join(", ")}]`);
191
+ }
192
+ return {
193
+ declarations,
194
+ objectLiteral: `{${entries.join(", ")}}`,
195
+ };
196
+ };
197
+ const layoutComponentCache = new Map();
198
+ const extractLayoutComponent = (loaded) => {
199
+ if (loaded && typeof loaded === "object" && "default" in loaded) {
200
+ const candidate = loaded.default;
201
+ if (typeof candidate === "function") {
202
+ return candidate;
203
+ }
204
+ }
205
+ if (typeof loaded === "function") {
206
+ return loaded;
207
+ }
208
+ return null;
209
+ };
210
+ const loadLayoutComponent = async (layoutFile, options) => {
211
+ const modulePath = resolveLayoutModulePath(layoutFile, options.rootDir, options.routeRoot);
212
+ const cacheKey = `${options.routeRoot ?? ""}::${modulePath}`;
213
+ if (layoutComponentCache.has(cacheKey)) {
214
+ return layoutComponentCache.get(cacheKey);
215
+ }
216
+ try {
217
+ const moduleUrl = toFileUrl(modulePath);
218
+ const loaded = await import(moduleUrl);
219
+ const component = extractLayoutComponent(loaded);
220
+ layoutComponentCache.set(cacheKey, component);
221
+ return component;
222
+ }
223
+ catch {
224
+ try {
225
+ const require = createRequire(import.meta.url);
226
+ const loaded = require(modulePath);
227
+ const component = extractLayoutComponent(loaded);
228
+ layoutComponentCache.set(cacheKey, component);
229
+ return component;
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ }
235
+ };
236
+ const resolveLayoutComponentsForRender = async (routeId, options) => {
237
+ if (!resolveNestedLayoutsEnabled(options)) {
238
+ return [];
239
+ }
240
+ const layoutFiles = resolveLayoutFilesForRoute(options.routeGraph, routeId);
241
+ if (layoutFiles.length === 0) {
242
+ return [];
243
+ }
244
+ const components = await Promise.all(layoutFiles.map((layoutFile) => loadLayoutComponent(layoutFile, options)));
245
+ return components.filter((component) => Boolean(component));
246
+ };
247
+ const applyLayoutsToElement = (element, props, layouts) => {
248
+ let tree = element;
249
+ for (let index = layouts.length - 1; index >= 0; index -= 1) {
250
+ const Layout = layouts[index];
251
+ if (typeof Layout === "function") {
252
+ const LayoutComponent = Layout;
253
+ tree = React.createElement(LayoutComponent, { ...props, children: tree });
254
+ }
255
+ }
256
+ return tree;
257
+ };
258
+ const buildClientRouteModuleLoaderSource = (modules) => {
259
+ const declarations = [];
260
+ const entries = [];
261
+ let index = 0;
262
+ for (const [routeId, specifier] of Object.entries(modules).sort(([a], [b]) => a.localeCompare(b))) {
263
+ const loaderName = `loadClientRouteModule${index++}`;
264
+ declarations.push(`const ${loaderName} = () => import(${JSON.stringify(specifier)});`);
265
+ entries.push(`${JSON.stringify(routeId)}: ${loaderName}`);
266
+ }
267
+ return {
268
+ declarations,
269
+ objectLiteral: `{${entries.join(", ")}}`,
270
+ };
271
+ };
272
+ const defaultClientEntry = (ctx) => {
273
+ const pageImport = resolvePageImport(ctx);
274
+ const hydrationMode = JSON.stringify(resolveHydrationMode(ctx));
275
+ const navigationMode = JSON.stringify(resolveNavigationMode(ctx));
276
+ const clientRenderMode = JSON.stringify(resolveClientRenderMode(ctx));
277
+ const routeGraph = JSON.stringify(ctx.routeGraph ?? { routes: [] });
278
+ const entryRouteId = JSON.stringify(typeof ctx.routeId === "string" ? ctx.routeId : "");
279
+ const routeModules = resolveClientRouteModules(ctx);
280
+ const routeModuleLoaders = buildClientRouteModuleLoaderSource(routeModules);
281
+ const adapterOptions = (ctx.uiOptions ?? ctx.adapterOptions);
282
+ const nestedLayoutsEnabled = resolveNestedLayoutsEnabled(adapterOptions);
283
+ const routeLayoutModules = nestedLayoutsEnabled && ctx.routeGraph
284
+ ? ctx.routeGraph.routes.reduce((acc, route) => {
285
+ acc[route.id] = resolveLayoutSpecifiersForRoute(ctx, route.id);
286
+ return acc;
287
+ }, {})
288
+ : {};
289
+ const routeLayoutLoaders = buildClientRouteLayoutLoaderSource(routeLayoutModules);
290
+ const entryLayoutSpecifiers = nestedLayoutsEnabled && typeof ctx.routeId === "string"
291
+ ? resolveLayoutSpecifiersForRoute(ctx, ctx.routeId)
292
+ : [];
293
+ const entryLayoutImports = entryLayoutSpecifiers.map((specifier, index) => `import EntryLayout${index} from ${JSON.stringify(specifier)};`);
294
+ const entryLayoutRefs = entryLayoutSpecifiers.map((_specifier, index) => `EntryLayout${index}`);
295
+ const entryLayoutArray = entryLayoutRefs.length > 0 ? `[${entryLayoutRefs.join(", ")}]` : "[]";
296
+ return [
297
+ "import React from \"react\";",
298
+ "import { createRoot, hydrateRoot } from \"react-dom/client\";",
299
+ "import {",
300
+ " RouterProvider,",
301
+ " createClientRouteModuleResolver,",
302
+ " createRouter,",
303
+ " installRouterIslands,",
304
+ "} from \"@tyndall/react\";",
305
+ `import Page from ${pageImport};`,
306
+ ...entryLayoutImports,
307
+ ...routeModuleLoaders.declarations,
308
+ ...routeLayoutLoaders.declarations,
309
+ "const ROUTE_PAYLOAD_EVENT = \"hyper:route-payload-applied\";",
310
+ "const HYDRATED_EVENT = \"hyper:hydrated\";",
311
+ "const container = document.getElementById(\"app\");",
312
+ "const readPropsPayload = () => {",
313
+ " const propsEl = document.getElementById(\"__HYPER_PROPS__\");",
314
+ " if (!propsEl?.textContent) {",
315
+ " return {};",
316
+ " }",
317
+ " try {",
318
+ " return JSON.parse(propsEl.textContent);",
319
+ " } catch {",
320
+ " return {};",
321
+ " }",
322
+ "};",
323
+ "let props = readPropsPayload();",
324
+ `const entryRouteId = ${entryRouteId};`,
325
+ "const runtimeModuleUrl = (() => {",
326
+ " try {",
327
+ " return new URL(import.meta.url).pathname;",
328
+ " } catch {",
329
+ " return \"\";",
330
+ " }",
331
+ "})();",
332
+ "const previousRuntimeModuleUrl = typeof window.__HYPER_CLIENT_RUNTIME_MODULE_URL__ === \"string\"",
333
+ " ? window.__HYPER_CLIENT_RUNTIME_MODULE_URL__",
334
+ " : \"\";",
335
+ "const runtimeGenerationChanged =",
336
+ " previousRuntimeModuleUrl.length > 0 && previousRuntimeModuleUrl !== runtimeModuleUrl;",
337
+ "if (runtimeGenerationChanged) {",
338
+ " // Prevent mixed React dispatchers across runtime generations during HMR.",
339
+ " const disposeRuntime = window.__HYPER_CLIENT_RUNTIME_DISPOSE__;",
340
+ " if (typeof disposeRuntime === \"function\") {",
341
+ " disposeRuntime(\"generation-change\");",
342
+ " }",
343
+ "}",
344
+ "window.__HYPER_CLIENT_RUNTIME_MODULE_URL__ = runtimeModuleUrl;",
345
+ "let runtimeAlreadyInitialized = window.__HYPER_CLIENT_RUNTIME_READY__ === true;",
346
+ `const routeGraph = ${routeGraph};`,
347
+ `const navigationMode = ${navigationMode};`,
348
+ `const clientRenderMode = ${clientRenderMode};`,
349
+ `let routeModuleLoaders = ${routeModuleLoaders.objectLiteral};`,
350
+ "window.__HYPER_CLIENT_ROUTE_MODULES__ = routeModuleLoaders;",
351
+ `let routeLayoutLoaders = ${routeLayoutLoaders.objectLiteral};`,
352
+ "window.__HYPER_CLIENT_ROUTE_LAYOUTS__ = routeLayoutLoaders;",
353
+ "const routeModuleResolver =",
354
+ " Object.keys(routeModuleLoaders).length > 0",
355
+ " ? createClientRouteModuleResolver(routeModuleLoaders)",
356
+ " : undefined;",
357
+ "const router = runtimeAlreadyInitialized && window.__HYPER_CLIENT_RUNTIME_ROUTER__",
358
+ " ? window.__HYPER_CLIENT_RUNTIME_ROUTER__",
359
+ " : createRouter(window.location.href, {",
360
+ " routeGraph,",
361
+ " navigationMode,",
362
+ " clientRenderMode,",
363
+ " resolveClientRouteModule: routeModuleResolver,",
364
+ " });",
365
+ "if (!runtimeAlreadyInitialized) {",
366
+ " installRouterIslands(router);",
367
+ " window.__HYPER_CLIENT_RUNTIME_ROUTER__ = router;",
368
+ "}",
369
+ `const configuredHydration = ${hydrationMode};`,
370
+ "const documentHydration = container?.getAttribute(\"data-hyper-hydration\");",
371
+ "const hydration = documentHydration === \"islands\" ? \"islands\" : configuredHydration;",
372
+ "if (container && hydration !== \"islands\") {",
373
+ " container.setAttribute(\"data-hyper-hydrated\", \"false\");",
374
+ "}",
375
+ "if (!runtimeAlreadyInitialized) {",
376
+ " window.__HYPER_HYDRATED__ = false;",
377
+ " window.__HYPER_CLIENT_APP_MOUNTED__ = false;",
378
+ "}",
379
+ "let activePage = Page;",
380
+ "let routeEntryPages = {};",
381
+ "if (entryRouteId) {",
382
+ " routeEntryPages[entryRouteId] = Page;",
383
+ "}",
384
+ "let routeEntryLayouts = {};",
385
+ "if (entryRouteId) {",
386
+ ` routeEntryLayouts[entryRouteId] = ${entryLayoutArray};`,
387
+ "}",
388
+ "let activeLayouts = routeEntryLayouts[entryRouteId] ?? [];",
389
+ "let activeProps = props;",
390
+ "let root = null;",
391
+ "const runtimeDisposers = [];",
392
+ "const registerRuntimeDisposer = (dispose) => {",
393
+ " if (typeof dispose === \"function\") {",
394
+ " runtimeDisposers.push(dispose);",
395
+ " }",
396
+ "};",
397
+ "const addRuntimeListener = (target, eventName, listener) => {",
398
+ " target.addEventListener(eventName, listener);",
399
+ " registerRuntimeDisposer(() => {",
400
+ " target.removeEventListener(eventName, listener);",
401
+ " });",
402
+ "};",
403
+ "const markHydrated = () => {",
404
+ " if (!container || hydration === \"islands\") {",
405
+ " return;",
406
+ " }",
407
+ " container.setAttribute(\"data-hyper-hydrated\", \"true\");",
408
+ " window.__HYPER_HYDRATED__ = true;",
409
+ " const routeId = typeof window.__HYPER_ROUTE_ID__ === \"string\"",
410
+ " ? window.__HYPER_ROUTE_ID__",
411
+ " : container.getAttribute(\"data-hyper-route\");",
412
+ " window.dispatchEvent(new CustomEvent(HYDRATED_EVENT, { detail: { routeId } }));",
413
+ "};",
414
+ "const applyLayouts = (PageComponent, props, layouts) => {",
415
+ " let tree = React.createElement(PageComponent, props);",
416
+ " if (Array.isArray(layouts)) {",
417
+ " for (let i = layouts.length - 1; i >= 0; i -= 1) {",
418
+ " const Layout = layouts[i];",
419
+ " if (typeof Layout === \"function\") {",
420
+ " tree = React.createElement(Layout, { ...props, children: tree });",
421
+ " }",
422
+ " }",
423
+ " }",
424
+ " return tree;",
425
+ "};",
426
+ "const renderActivePage = (hydrate = false) => {",
427
+ " if (!container || hydration === \"islands\") {",
428
+ " return;",
429
+ " }",
430
+ " const element = React.createElement(",
431
+ " RouterProvider,",
432
+ " { router, children: applyLayouts(activePage, activeProps, activeLayouts) }",
433
+ " );",
434
+ " if (root && typeof root.render === \"function\") {",
435
+ " window.__HYPER_CLIENT_APP_MOUNTED__ = true;",
436
+ " root.render(element);",
437
+ " markHydrated();",
438
+ " return;",
439
+ " }",
440
+ " if (hydrate && container.hasChildNodes()) {",
441
+ " root = hydrateRoot(container, element);",
442
+ " window.__HYPER_CLIENT_APP_MOUNTED__ = true;",
443
+ " markHydrated();",
444
+ " return;",
445
+ " }",
446
+ " root = createRoot(container);",
447
+ " window.__HYPER_CLIENT_APP_MOUNTED__ = true;",
448
+ " root.render(element);",
449
+ " markHydrated();",
450
+ "};",
451
+ "const resolvePageComponent = async (routeId) => {",
452
+ " const load = routeModuleLoaders[routeId];",
453
+ " if (typeof load !== \"function\") {",
454
+ " return routeEntryPages[routeId] ?? null;",
455
+ " }",
456
+ " try {",
457
+ " const loaded = await load();",
458
+ " if (loaded && typeof loaded === \"object\" && typeof loaded.default === \"function\") {",
459
+ " return loaded.default;",
460
+ " }",
461
+ " if (typeof loaded === \"function\") {",
462
+ " return loaded;",
463
+ " }",
464
+ " } catch {",
465
+ " return routeEntryPages[routeId] ?? null;",
466
+ " }",
467
+ " return routeEntryPages[routeId] ?? null;",
468
+ "};",
469
+ "const resolveLayoutComponents = async (routeId) => {",
470
+ " const cached = routeEntryLayouts[routeId];",
471
+ " if (Array.isArray(cached) && cached.length > 0) {",
472
+ " return cached;",
473
+ " }",
474
+ " const loaders = routeLayoutLoaders[routeId];",
475
+ " if (!Array.isArray(loaders) || loaders.length === 0) {",
476
+ " return [];",
477
+ " }",
478
+ " const modules = await Promise.all(",
479
+ " loaders.map((load) => {",
480
+ " if (typeof load !== \"function\") {",
481
+ " return null;",
482
+ " }",
483
+ " return load().catch(() => null);",
484
+ " })",
485
+ " );",
486
+ " return modules",
487
+ " .map((loaded) => {",
488
+ " if (loaded && typeof loaded === \"object\" && typeof loaded.default === \"function\") {",
489
+ " return loaded.default;",
490
+ " }",
491
+ " if (typeof loaded === \"function\") {",
492
+ " return loaded;",
493
+ " }",
494
+ " return null;",
495
+ " })",
496
+ " .filter(Boolean);",
497
+ "};",
498
+ "let routeRenderVersion = 0;",
499
+ "const rerenderRoute = async (routeId) => {",
500
+ " if (!routeId || typeof routeId !== \"string\") {",
501
+ " return;",
502
+ " }",
503
+ " const version = ++routeRenderVersion;",
504
+ " const nextPage = await resolvePageComponent(routeId);",
505
+ " const nextLayouts = await resolveLayoutComponents(routeId);",
506
+ " if (!nextPage || version !== routeRenderVersion) {",
507
+ " return;",
508
+ " }",
509
+ " activePage = nextPage;",
510
+ " activeLayouts = nextLayouts;",
511
+ " activeProps = readPropsPayload();",
512
+ " renderActivePage(false);",
513
+ "};",
514
+ "const applyRuntimeUpdate = (next) => {",
515
+ " if (!next || typeof next !== \"object\") {",
516
+ " return;",
517
+ " }",
518
+ " if (next.routeModuleLoaders && typeof next.routeModuleLoaders === \"object\") {",
519
+ " routeModuleLoaders = next.routeModuleLoaders;",
520
+ " window.__HYPER_CLIENT_ROUTE_MODULES__ = routeModuleLoaders;",
521
+ " }",
522
+ " if (next.routeLayoutLoaders && typeof next.routeLayoutLoaders === \"object\") {",
523
+ " routeLayoutLoaders = next.routeLayoutLoaders;",
524
+ " window.__HYPER_CLIENT_ROUTE_LAYOUTS__ = routeLayoutLoaders;",
525
+ " }",
526
+ " if (next.entryRouteId && typeof next.entryRouteId === \"string\" && typeof next.page === \"function\") {",
527
+ " routeEntryPages[next.entryRouteId] = next.page;",
528
+ " }",
529
+ " if (next.entryRouteId && Array.isArray(next.entryLayouts)) {",
530
+ " routeEntryLayouts[next.entryRouteId] = next.entryLayouts;",
531
+ " }",
532
+ "};",
533
+ "if (runtimeAlreadyInitialized) {",
534
+ " const updateRuntime = window.__HYPER_CLIENT_RUNTIME_UPDATE__;",
535
+ " if (typeof updateRuntime === \"function\") {",
536
+ " updateRuntime({",
537
+ " entryRouteId,",
538
+ " page: Page,",
539
+ " routeModuleLoaders,",
540
+ " routeLayoutLoaders,",
541
+ " entryLayouts: routeEntryLayouts[entryRouteId] ?? [],",
542
+ " });",
543
+ " }",
544
+ "}",
545
+ "const deferRouteSync = (callback) => {",
546
+ " if (typeof queueMicrotask === \"function\") {",
547
+ " queueMicrotask(callback);",
548
+ " return;",
549
+ " }",
550
+ " setTimeout(callback, 0);",
551
+ "};",
552
+ "const syncRouteFromDom = () => {",
553
+ " const routeId = typeof window.__HYPER_ROUTE_ID__ === \"string\"",
554
+ " ? window.__HYPER_ROUTE_ID__",
555
+ " : container?.getAttribute(\"data-hyper-route\");",
556
+ " void rerenderRoute(routeId);",
557
+ "};",
558
+ "const patchHistoryForRouteSync = () => {",
559
+ " if (window.__HYPER_CLIENT_HISTORY_PATCHED__ === true) {",
560
+ " return;",
561
+ " }",
562
+ " const history = window.history;",
563
+ " if (!history) {",
564
+ " return;",
565
+ " }",
566
+ " const wrap = (method) => {",
567
+ " const original = history[method];",
568
+ " if (typeof original !== \"function\") {",
569
+ " return;",
570
+ " }",
571
+ " try {",
572
+ " history[method] = (...args) => {",
573
+ " const value = original.apply(history, args);",
574
+ " const sync = window.__HYPER_CLIENT_RUNTIME_SYNC_ROUTE__;",
575
+ " if (typeof sync === \"function\") {",
576
+ " deferRouteSync(() => {",
577
+ " sync();",
578
+ " });",
579
+ " }",
580
+ " return value;",
581
+ " };",
582
+ " } catch {",
583
+ " // Ignore environments where History methods are not writable.",
584
+ " }",
585
+ " };",
586
+ " wrap(\"pushState\");",
587
+ " wrap(\"replaceState\");",
588
+ " window.__HYPER_CLIENT_HISTORY_PATCHED__ = true;",
589
+ "};",
590
+ "const disposeRuntime = () => {",
591
+ " while (runtimeDisposers.length > 0) {",
592
+ " const dispose = runtimeDisposers.pop();",
593
+ " if (typeof dispose === \"function\") {",
594
+ " try {",
595
+ " dispose();",
596
+ " } catch {",
597
+ " // Keep runtime disposal resilient for repeated HMR updates.",
598
+ " }",
599
+ " }",
600
+ " }",
601
+ " if (root && typeof root.unmount === \"function\") {",
602
+ " try {",
603
+ " root.unmount();",
604
+ " } catch {",
605
+ " // Ignore unmount failures during runtime replacement.",
606
+ " }",
607
+ " }",
608
+ " root = null;",
609
+ " window.__HYPER_CLIENT_RUNTIME_READY__ = false;",
610
+ " window.__HYPER_CLIENT_RUNTIME_ROUTER__ = undefined;",
611
+ " window.__HYPER_CLIENT_RUNTIME_UPDATE__ = undefined;",
612
+ " window.__HYPER_CLIENT_RUNTIME_RERENDER__ = undefined;",
613
+ " window.__HYPER_CLIENT_RUNTIME_SYNC_ROUTE__ = undefined;",
614
+ " window.__HYPER_CLIENT_RUNTIME_DISPOSE__ = undefined;",
615
+ " window.__HYPER_CLIENT_APP_MOUNTED__ = false;",
616
+ " window.__HYPER_HYDRATED__ = false;",
617
+ " if (container && hydration !== \"islands\") {",
618
+ " container.setAttribute(\"data-hyper-hydrated\", \"false\");",
619
+ " }",
620
+ "};",
621
+ "if (container && hydration !== \"islands\" && !runtimeAlreadyInitialized) {",
622
+ " const onRoutePayloadApplied = (event) => {",
623
+ " const detail = event && typeof event === \"object\" && \"detail\" in event",
624
+ " ? event.detail",
625
+ " : undefined;",
626
+ " const routeId = detail && typeof detail === \"object\" && \"routeId\" in detail",
627
+ " ? detail.routeId",
628
+ " : window.__HYPER_ROUTE_ID__;",
629
+ " void rerenderRoute(routeId);",
630
+ " };",
631
+ " const onPopState = () => {",
632
+ " deferRouteSync(syncRouteFromDom);",
633
+ " };",
634
+ " window.__HYPER_CLIENT_RUNTIME_UPDATE__ = applyRuntimeUpdate;",
635
+ " window.__HYPER_CLIENT_RUNTIME_RERENDER__ = (routeId) => {",
636
+ " void rerenderRoute(routeId);",
637
+ " };",
638
+ " window.__HYPER_CLIENT_RUNTIME_SYNC_ROUTE__ = syncRouteFromDom;",
639
+ " window.__HYPER_CLIENT_RUNTIME_DISPOSE__ = () => {",
640
+ " disposeRuntime();",
641
+ " };",
642
+ " window.__HYPER_CLIENT_RUNTIME_READY__ = true;",
643
+ " patchHistoryForRouteSync();",
644
+ " renderActivePage(true);",
645
+ " addRuntimeListener(window, ROUTE_PAYLOAD_EVENT, onRoutePayloadApplied);",
646
+ " addRuntimeListener(window, \"popstate\", onPopState);",
647
+ "} else if (runtimeAlreadyInitialized) {",
648
+ " const rerenderRuntime = window.__HYPER_CLIENT_RUNTIME_RERENDER__;",
649
+ " if (typeof rerenderRuntime === \"function\") {",
650
+ " const routeId = typeof window.__HYPER_ROUTE_ID__ === \"string\"",
651
+ " ? window.__HYPER_ROUTE_ID__",
652
+ " : container?.getAttribute(\"data-hyper-route\");",
653
+ " rerenderRuntime(routeId);",
654
+ " }",
655
+ "}",
656
+ ].join("\n");
657
+ };
658
+ const defaultServerEntry = (ctx) => {
659
+ const pageImport = resolvePageImport(ctx);
660
+ const hydrationMode = JSON.stringify(resolveHydrationMode(ctx));
661
+ const adapterOptions = (ctx.uiOptions ?? ctx.adapterOptions);
662
+ const nestedLayoutsEnabled = resolveNestedLayoutsEnabled(adapterOptions);
663
+ const layoutSpecifiers = nestedLayoutsEnabled && typeof ctx.routeId === "string"
664
+ ? resolveLayoutSpecifiersForRoute(ctx, ctx.routeId)
665
+ : [];
666
+ const layoutImports = layoutSpecifiers.map((specifier, index) => `import Layout${index} from ${JSON.stringify(specifier)};`);
667
+ const layoutRefs = layoutSpecifiers.map((_specifier, index) => `Layout${index}`);
668
+ const layoutArray = layoutRefs.length > 0 ? `[${layoutRefs.join(", ")}]` : "[]";
669
+ return [
670
+ "import React from \"react\";",
671
+ "import { renderToString, renderToPipeableStream } from \"react-dom/server\";",
672
+ "import { PassThrough } from \"stream\";",
673
+ "import { collectHeadFromTree } from \"@tyndall/react\";",
674
+ `import Page from ${pageImport};`,
675
+ ...layoutImports,
676
+ `export const hydration = ${hydrationMode};`,
677
+ "export const renderToHtml = async (ctx) => {",
678
+ " let element = React.createElement(Page, ctx.props);",
679
+ ` const layouts = ${layoutArray};`,
680
+ " if (Array.isArray(layouts) && layouts.length > 0) {",
681
+ " for (let i = layouts.length - 1; i >= 0; i -= 1) {",
682
+ " const Layout = layouts[i];",
683
+ " if (typeof Layout === \"function\") {",
684
+ " element = React.createElement(Layout, { ...ctx.props, children: element });",
685
+ " }",
686
+ " }",
687
+ " }",
688
+ " const html = renderToString(element);",
689
+ " const head = collectHeadFromTree(element);",
690
+ " return { html, head };",
691
+ "};",
692
+ "export const renderToStream = async (ctx) => {",
693
+ " if (typeof renderToPipeableStream !== \"function\") {",
694
+ " return null;",
695
+ " }",
696
+ " let element = React.createElement(Page, ctx.props);",
697
+ ` const layouts = ${layoutArray};`,
698
+ " if (Array.isArray(layouts) && layouts.length > 0) {",
699
+ " for (let i = layouts.length - 1; i >= 0; i -= 1) {",
700
+ " const Layout = layouts[i];",
701
+ " if (typeof Layout === \"function\") {",
702
+ " element = React.createElement(Layout, { ...ctx.props, children: element });",
703
+ " }",
704
+ " }",
705
+ " }",
706
+ " const head = collectHeadFromTree(element);",
707
+ " return await new Promise((resolve, reject) => {",
708
+ " let didError = false;",
709
+ " const stream = new PassThrough();",
710
+ " const { pipe, abort } = renderToPipeableStream(element, {",
711
+ " onShellReady() {",
712
+ " pipe(stream);",
713
+ " resolve({ stream, head, abort });",
714
+ " },",
715
+ " onShellError(error) {",
716
+ " reject(error);",
717
+ " },",
718
+ " onError() {",
719
+ " didError = true;",
720
+ " },",
721
+ " });",
722
+ " if (didError) {",
723
+ " abort();",
724
+ " }",
725
+ " });",
726
+ "};",
727
+ ].join("\n");
728
+ };
729
+ const defaultRenderToHtml = async (ctx, options) => {
730
+ if (!options.render) {
731
+ return { html: `<div data-hyper-react=\"true\">${ctx.routeId}</div>` };
732
+ }
733
+ const element = await options.render(ctx);
734
+ const layouts = await resolveLayoutComponentsForRender(ctx.routeId, options);
735
+ const composed = layouts.length > 0 ? applyLayoutsToElement(element, ctx.props, layouts) : element;
736
+ const html = await renderWithFallback(composed);
737
+ const collected = collectHeadFromTree(composed);
738
+ const provided = options.getHead?.(ctx);
739
+ const head = mergeHeadDescriptors(provided ?? {}, collected);
740
+ return { html, head };
741
+ };
742
+ export const createReactAdapter = (options = {}) => ({
743
+ name: options.name ?? "react",
744
+ createClientEntry: options.createClientEntry ?? defaultClientEntry,
745
+ createServerEntry: options.createServerEntry ?? defaultServerEntry,
746
+ renderToHtml: options.renderToHtml ?? ((ctx) => defaultRenderToHtml(ctx, options)),
747
+ serializeProps,
748
+ getHead: options.getHead,
749
+ hmrIntegration: options.hmrIntegration,
750
+ });