@tyndall/react 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -7,6 +7,10 @@ React adapter package that implements UIAdapter contracts, routing integration,
7
7
  - Provide React-based rendering adapter, factory, and registry
8
8
  - Provide client router and navigation components
9
9
  - Integrate route payload transitions and head updates
10
+ - Provide `_app` wrapper support and pass `{ Component, pageProps, routeData, routeId }` to App components
11
+ - Expose `RouteDataProvider` and `useRouteData` for consuming route data maps
12
+ - Provide `useBlockRouting` and navigation blocker hooks for client transitions and `beforeunload`
13
+ - Support `router.softReload()` and scroll restoration during client-side transitions
10
14
  - Coordinate hydration lifecycle markers and mounted-state guards for payload-driven navigation
11
15
  - Guard dev runtime generation changes with controlled dispose/re-initialize flow to avoid mixed React hook dispatcher states during HMR
12
16
  - Compose nested layout trees when route metadata supplies `layoutFiles`
package/dist/adapter.d.ts CHANGED
@@ -1,16 +1,25 @@
1
+ import React from "react";
1
2
  import type { HmrIntegration, RouteGraph, UIAdapter, UIAdapterEntryContext, UIAdapterRenderContext, UIAdapterRenderResult } from "@tyndall/core";
2
3
  export interface ReactAdapterOptions {
3
4
  name?: string;
4
5
  createClientEntry?: (ctx: UIAdapterEntryContext) => string;
5
6
  createServerEntry?: (ctx: UIAdapterEntryContext) => string;
6
7
  renderToHtml?: (ctx: UIAdapterRenderContext) => Promise<UIAdapterRenderResult> | UIAdapterRenderResult;
7
- render?: (ctx: UIAdapterRenderContext) => Promise<unknown> | unknown;
8
+ render?: (ctx: UIAdapterRenderContext) => Promise<React.ReactElement | null> | React.ReactElement | null;
8
9
  getHead?: UIAdapter["getHead"];
9
10
  hmrIntegration?: HmrIntegration;
10
11
  routeGraph?: RouteGraph;
11
12
  nestedLayouts?: boolean;
12
13
  rootDir?: string;
13
14
  routeRoot?: string;
15
+ appModule?: string;
16
+ appComponent?: React.ComponentType<AppComponentProps>;
17
+ }
18
+ export interface AppComponentProps {
19
+ Component: React.ComponentType<Record<string, unknown>>;
20
+ pageProps: Record<string, unknown>;
21
+ routeData: Record<string, unknown>;
22
+ routeId: string;
14
23
  }
15
24
  export declare const createReactAdapter: (options?: ReactAdapterOptions) => UIAdapter;
16
25
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,SAAS,EACT,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,MAAM,CAAC;IAC3D,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,MAAM,CAAC;IAC3D,YAAY,CAAC,EAAE,CACb,GAAG,EAAE,sBAAsB,KACxB,OAAO,CAAC,qBAAqB,CAAC,GAAG,qBAAqB,CAAC;IAC5D,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IACrE,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAizBD,eAAO,MAAM,kBAAkB,GAAI,UAAS,mBAAwB,KAAG,SAQrE,CAAC"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,SAAS,EACT,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,eAAe,CAAC;AAKvB,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,MAAM,CAAC;IAC3D,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,MAAM,CAAC;IAC3D,YAAY,CAAC,EAAE,CACb,GAAG,EAAE,sBAAsB,KACxB,OAAO,CAAC,qBAAqB,CAAC,GAAG,qBAAqB,CAAC;IAC5D,MAAM,CAAC,EAAE,CACP,GAAG,EAAE,sBAAsB,KACxB,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IACpE,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;CACvD;AAMD,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;CACjB;AAiiCD,eAAO,MAAM,kBAAkB,GAAI,UAAS,mBAAwB,KAAG,SAQrE,CAAC"}
package/dist/adapter.js CHANGED
@@ -3,8 +3,13 @@ import { createRequire } from "node:module";
3
3
  import { join, relative, sep } from "node:path";
4
4
  import { mergeHeadDescriptors, serializeProps } from "@tyndall/core";
5
5
  import { collectHeadFromTree } from "./head.js";
6
+ import { RouteDataProvider } from "./route-data.js";
6
7
  const REACT_ELEMENT = Symbol.for("react.element");
7
8
  const REACT_TRANSITIONAL_ELEMENT = Symbol.for("react.transitional.element");
9
+ const REACT_FRAGMENT = Symbol.for("react.fragment");
10
+ const REACT_PROVIDER = Symbol.for("react.provider");
11
+ const REACT_MEMO = Symbol.for("react.memo");
12
+ const REACT_FORWARD_REF = Symbol.for("react.forward_ref");
8
13
  const HEAD_ELEMENT = Symbol.for("hyper.head");
9
14
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10
15
  const isElementLike = (value) => isRecord(value) &&
@@ -54,6 +59,30 @@ const renderElementToString = (node) => {
54
59
  if (isHeadMarker(node)) {
55
60
  return "";
56
61
  }
62
+ if (node.type === REACT_FRAGMENT) {
63
+ return renderElementToString(node.props.children);
64
+ }
65
+ if (isRecord(node.type) && node.type.$$typeof === REACT_PROVIDER) {
66
+ return renderElementToString(node.props.children);
67
+ }
68
+ if (isRecord(node.type) && node.type.$$typeof === REACT_MEMO) {
69
+ const innerType = node.type.type;
70
+ if (typeof innerType === "function") {
71
+ return renderElementToString(innerType(node.props));
72
+ }
73
+ if (typeof innerType === "string") {
74
+ return renderElementToString({ ...node, type: innerType });
75
+ }
76
+ return "";
77
+ }
78
+ if (isRecord(node.type) && node.type.$$typeof === REACT_FORWARD_REF) {
79
+ const render = node.type
80
+ .render;
81
+ if (typeof render === "function") {
82
+ return renderElementToString(render(node.props, null));
83
+ }
84
+ return "";
85
+ }
57
86
  if (typeof node.type === "function") {
58
87
  return renderElementToString(node.type(node.props));
59
88
  }
@@ -75,12 +104,7 @@ const resolveReactDomServer = async () => {
75
104
  const renderWithFallback = async (element) => {
76
105
  const reactDomServer = await resolveReactDomServer();
77
106
  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
- }
107
+ return reactDomServer.renderToString(element);
84
108
  }
85
109
  // Fallback renderer keeps local tests/dev flows working without react-dom.
86
110
  return renderElementToString(element);
@@ -91,6 +115,11 @@ const resolvePageImport = (ctx) => {
91
115
  const specifier = typeof pageModule === "string" ? pageModule : "./page";
92
116
  return JSON.stringify(specifier);
93
117
  };
118
+ const resolveAppImport = (ctx) => {
119
+ const options = ctx.uiOptions ?? ctx.adapterOptions;
120
+ const appModule = options?.appModule;
121
+ return typeof appModule === "string" && appModule.length > 0 ? appModule : null;
122
+ };
94
123
  const resolveHydrationMode = (ctx) => {
95
124
  const options = ctx.uiOptions ?? ctx.adapterOptions;
96
125
  return options?.hydration === "islands" ? "islands" : "full";
@@ -103,6 +132,13 @@ const resolveClientRenderMode = (ctx) => {
103
132
  const options = ctx.uiOptions ?? ctx.adapterOptions;
104
133
  return options?.clientRenderMode === "module" ? "module" : "payload";
105
134
  };
135
+ const resolveScrollRestoration = (ctx) => {
136
+ const options = ctx.uiOptions ?? ctx.adapterOptions;
137
+ if (options && "scrollRestoration" in options) {
138
+ return options.scrollRestoration !== false;
139
+ }
140
+ return true;
141
+ };
106
142
  const resolveClientRouteModules = (ctx) => {
107
143
  const options = ctx.uiOptions ?? ctx.adapterOptions;
108
144
  const raw = options?.clientRouteModules;
@@ -137,8 +173,8 @@ const resolveLayoutFilesForRoute = (routeGraph, routeId) => {
137
173
  const route = findRouteRecord(routeGraph, routeId);
138
174
  return route?.layoutFiles ?? [];
139
175
  };
140
- const resolveLayoutModulePath = (layoutFile, rootDir, routeRoot) => {
141
- if (!rootDir || !routeRoot) {
176
+ const resolveLayoutModulePath = (layoutFile, rootDir, routeRoot, preferRouteRoot) => {
177
+ if (!preferRouteRoot || !rootDir || !routeRoot) {
142
178
  return layoutFile;
143
179
  }
144
180
  const relativePath = relative(rootDir, layoutFile);
@@ -168,8 +204,18 @@ const resolveLayoutSpecifiersForRoute = (ctx, routeId) => {
168
204
  if (layoutFiles.length === 0) {
169
205
  return [];
170
206
  }
207
+ const preferRouteRoot = typeof ctx.routeId === "string" && ctx.routeId === routeId;
171
208
  return layoutFiles.map((layoutFile) => {
172
- const modulePath = resolveLayoutModulePath(layoutFile, ctx.rootDir, ctx.routeRoot);
209
+ const modulePath = resolveLayoutModulePath(layoutFile, ctx.rootDir, ctx.routeRoot, preferRouteRoot);
210
+ return toImportSpecifier(ctx.entryDir ?? "", modulePath);
211
+ });
212
+ };
213
+ const resolveLayoutSpecifiersForEntry = (ctx, layoutFiles) => {
214
+ if (!ctx.entryDir || !layoutFiles || layoutFiles.length === 0) {
215
+ return [];
216
+ }
217
+ return layoutFiles.map((layoutFile) => {
218
+ const modulePath = resolveLayoutModulePath(layoutFile, ctx.rootDir, ctx.routeRoot, true);
173
219
  return toImportSpecifier(ctx.entryDir ?? "", modulePath);
174
220
  });
175
221
  };
@@ -195,6 +241,7 @@ const buildClientRouteLayoutLoaderSource = (layoutModules) => {
195
241
  };
196
242
  };
197
243
  const layoutComponentCache = new Map();
244
+ const appComponentCache = new Map();
198
245
  const extractLayoutComponent = (loaded) => {
199
246
  if (loaded && typeof loaded === "object" && "default" in loaded) {
200
247
  const candidate = loaded.default;
@@ -207,8 +254,28 @@ const extractLayoutComponent = (loaded) => {
207
254
  }
208
255
  return null;
209
256
  };
257
+ const extractAppComponent = (loaded) => {
258
+ if (loaded && typeof loaded === "object") {
259
+ if ("default" in loaded) {
260
+ const candidate = loaded.default;
261
+ if (typeof candidate === "function") {
262
+ return candidate;
263
+ }
264
+ }
265
+ if ("App" in loaded) {
266
+ const candidate = loaded.App;
267
+ if (typeof candidate === "function") {
268
+ return candidate;
269
+ }
270
+ }
271
+ }
272
+ if (typeof loaded === "function") {
273
+ return loaded;
274
+ }
275
+ return null;
276
+ };
210
277
  const loadLayoutComponent = async (layoutFile, options) => {
211
- const modulePath = resolveLayoutModulePath(layoutFile, options.rootDir, options.routeRoot);
278
+ const modulePath = resolveLayoutModulePath(layoutFile, options.rootDir, options.routeRoot, true);
212
279
  const cacheKey = `${options.routeRoot ?? ""}::${modulePath}`;
213
280
  if (layoutComponentCache.has(cacheKey)) {
214
281
  return layoutComponentCache.get(cacheKey);
@@ -233,6 +300,40 @@ const loadLayoutComponent = async (layoutFile, options) => {
233
300
  }
234
301
  }
235
302
  };
303
+ const loadAppComponent = async (modulePath) => {
304
+ if (appComponentCache.has(modulePath)) {
305
+ return appComponentCache.get(modulePath);
306
+ }
307
+ try {
308
+ const moduleUrl = toFileUrl(modulePath);
309
+ const loaded = await import(moduleUrl);
310
+ const component = extractAppComponent(loaded);
311
+ appComponentCache.set(modulePath, component);
312
+ return component;
313
+ }
314
+ catch {
315
+ try {
316
+ const require = createRequire(import.meta.url);
317
+ const loaded = require(modulePath);
318
+ const component = extractAppComponent(loaded);
319
+ appComponentCache.set(modulePath, component);
320
+ return component;
321
+ }
322
+ catch {
323
+ return null;
324
+ }
325
+ }
326
+ };
327
+ const resolveAppComponentForRender = async (options) => {
328
+ if (typeof options.appComponent === "function") {
329
+ return options.appComponent;
330
+ }
331
+ if (!options.appModule) {
332
+ return null;
333
+ }
334
+ const loaded = await loadAppComponent(options.appModule);
335
+ return typeof loaded === "function" ? loaded : null;
336
+ };
236
337
  const resolveLayoutComponentsForRender = async (routeId, options) => {
237
338
  if (!resolveNestedLayoutsEnabled(options)) {
238
339
  return [];
@@ -271,9 +372,11 @@ const buildClientRouteModuleLoaderSource = (modules) => {
271
372
  };
272
373
  const defaultClientEntry = (ctx) => {
273
374
  const pageImport = resolvePageImport(ctx);
375
+ const appImport = resolveAppImport(ctx);
274
376
  const hydrationMode = JSON.stringify(resolveHydrationMode(ctx));
275
377
  const navigationMode = JSON.stringify(resolveNavigationMode(ctx));
276
378
  const clientRenderMode = JSON.stringify(resolveClientRenderMode(ctx));
379
+ const scrollRestoration = JSON.stringify(resolveScrollRestoration(ctx));
277
380
  const routeGraph = JSON.stringify(ctx.routeGraph ?? { routes: [] });
278
381
  const entryRouteId = JSON.stringify(typeof ctx.routeId === "string" ? ctx.routeId : "");
279
382
  const routeModules = resolveClientRouteModules(ctx);
@@ -288,25 +391,49 @@ const defaultClientEntry = (ctx) => {
288
391
  : {};
289
392
  const routeLayoutLoaders = buildClientRouteLayoutLoaderSource(routeLayoutModules);
290
393
  const entryLayoutSpecifiers = nestedLayoutsEnabled && typeof ctx.routeId === "string"
291
- ? resolveLayoutSpecifiersForRoute(ctx, ctx.routeId)
394
+ ? ctx.entryLayoutFiles && ctx.entryLayoutFiles.length > 0
395
+ ? resolveLayoutSpecifiersForEntry(ctx, ctx.entryLayoutFiles)
396
+ : resolveLayoutSpecifiersForRoute(ctx, ctx.routeId)
292
397
  : [];
293
398
  const entryLayoutImports = entryLayoutSpecifiers.map((specifier, index) => `import EntryLayout${index} from ${JSON.stringify(specifier)};`);
294
399
  const entryLayoutRefs = entryLayoutSpecifiers.map((_specifier, index) => `EntryLayout${index}`);
295
400
  const entryLayoutArray = entryLayoutRefs.length > 0 ? `[${entryLayoutRefs.join(", ")}]` : "[]";
401
+ const appImportSource = appImport ? `import * as AppModule from ${JSON.stringify(appImport)};` : "";
402
+ const appResolverSource = appImport
403
+ ? [
404
+ "const resolveAppComponent = (module) => {",
405
+ " if (!module || typeof module !== \"object\") {",
406
+ " return null;",
407
+ " }",
408
+ " if (typeof module.default === \"function\") {",
409
+ " return module.default;",
410
+ " }",
411
+ " if (typeof module.App === \"function\") {",
412
+ " return module.App;",
413
+ " }",
414
+ " return null;",
415
+ "};",
416
+ "const AppComponent = resolveAppComponent(AppModule);",
417
+ ]
418
+ : ["const AppComponent = null;"];
296
419
  return [
297
420
  "import React from \"react\";",
298
421
  "import { createRoot, hydrateRoot } from \"react-dom/client\";",
299
422
  "import {",
300
423
  " RouterProvider,",
424
+ " RouteDataProvider,",
301
425
  " createClientRouteModuleResolver,",
302
426
  " createRouter,",
303
427
  " installRouterIslands,",
304
428
  "} from \"@tyndall/react\";",
305
429
  `import Page from ${pageImport};`,
430
+ ...(appImportSource ? [appImportSource] : []),
306
431
  ...entryLayoutImports,
307
432
  ...routeModuleLoaders.declarations,
308
433
  ...routeLayoutLoaders.declarations,
434
+ ...appResolverSource,
309
435
  "const ROUTE_PAYLOAD_EVENT = \"hyper:route-payload-applied\";",
436
+ "const ROUTE_DATA_EVENT = \"hyper:route-data-applied\";",
310
437
  "const HYDRATED_EVENT = \"hyper:hydrated\";",
311
438
  "const container = document.getElementById(\"app\");",
312
439
  "const readPropsPayload = () => {",
@@ -320,7 +447,20 @@ const defaultClientEntry = (ctx) => {
320
447
  " return {};",
321
448
  " }",
322
449
  "};",
450
+ "const readRouteDataPayload = () => {",
451
+ " const dataEl = document.getElementById(\"__HYPER_ROUTE_DATA__\");",
452
+ " if (!dataEl?.textContent) {",
453
+ " return {};",
454
+ " }",
455
+ " try {",
456
+ " return JSON.parse(dataEl.textContent);",
457
+ " } catch {",
458
+ " return {};",
459
+ " }",
460
+ "};",
323
461
  "let props = readPropsPayload();",
462
+ "let routeData = readRouteDataPayload();",
463
+ "window.__HYPER_ROUTE_DATA__ = routeData;",
324
464
  `const entryRouteId = ${entryRouteId};`,
325
465
  "const runtimeModuleUrl = (() => {",
326
466
  " try {",
@@ -346,6 +486,7 @@ const defaultClientEntry = (ctx) => {
346
486
  `const routeGraph = ${routeGraph};`,
347
487
  `const navigationMode = ${navigationMode};`,
348
488
  `const clientRenderMode = ${clientRenderMode};`,
489
+ `const scrollRestoration = ${scrollRestoration};`,
349
490
  `let routeModuleLoaders = ${routeModuleLoaders.objectLiteral};`,
350
491
  "window.__HYPER_CLIENT_ROUTE_MODULES__ = routeModuleLoaders;",
351
492
  `let routeLayoutLoaders = ${routeLayoutLoaders.objectLiteral};`,
@@ -360,6 +501,7 @@ const defaultClientEntry = (ctx) => {
360
501
  " routeGraph,",
361
502
  " navigationMode,",
362
503
  " clientRenderMode,",
504
+ " scrollRestoration,",
363
505
  " resolveClientRouteModule: routeModuleResolver,",
364
506
  " });",
365
507
  "if (!runtimeAlreadyInitialized) {",
@@ -387,6 +529,7 @@ const defaultClientEntry = (ctx) => {
387
529
  "}",
388
530
  "let activeLayouts = routeEntryLayouts[entryRouteId] ?? [];",
389
531
  "let activeProps = props;",
532
+ "let activeRouteData = routeData;",
390
533
  "let root = null;",
391
534
  "const runtimeDisposers = [];",
392
535
  "const registerRuntimeDisposer = (dispose) => {",
@@ -427,9 +570,22 @@ const defaultClientEntry = (ctx) => {
427
570
  " if (!container || hydration === \"islands\") {",
428
571
  " return;",
429
572
  " }",
573
+ " const routeId = typeof window.__HYPER_ROUTE_ID__ === \"string\"",
574
+ " ? window.__HYPER_ROUTE_ID__",
575
+ " : container.getAttribute(\"data-hyper-route\") || entryRouteId;",
576
+ " const RoutedPage = (pageProps) => applyLayouts(activePage, pageProps, activeLayouts);",
577
+ " const appTree = AppComponent",
578
+ " ? React.createElement(AppComponent, {",
579
+ " Component: RoutedPage,",
580
+ " pageProps: activeProps,",
581
+ " routeData: activeRouteData,",
582
+ " routeId,",
583
+ " })",
584
+ " : React.createElement(RoutedPage, activeProps);",
430
585
  " const element = React.createElement(",
431
- " RouterProvider,",
432
- " { router, children: applyLayouts(activePage, activeProps, activeLayouts) }",
586
+ " RouteDataProvider,",
587
+ " { value: activeRouteData },",
588
+ " React.createElement(RouterProvider, { router, children: appTree })",
433
589
  " );",
434
590
  " if (root && typeof root.render === \"function\") {",
435
591
  " window.__HYPER_CLIENT_APP_MOUNTED__ = true;",
@@ -509,6 +665,7 @@ const defaultClientEntry = (ctx) => {
509
665
  " activePage = nextPage;",
510
666
  " activeLayouts = nextLayouts;",
511
667
  " activeProps = readPropsPayload();",
668
+ " activeRouteData = readRouteDataPayload();",
512
669
  " renderActivePage(false);",
513
670
  "};",
514
671
  "const applyRuntimeUpdate = (next) => {",
@@ -628,6 +785,10 @@ const defaultClientEntry = (ctx) => {
628
785
  " : window.__HYPER_ROUTE_ID__;",
629
786
  " void rerenderRoute(routeId);",
630
787
  " };",
788
+ " const onRouteDataApplied = () => {",
789
+ " activeRouteData = readRouteDataPayload();",
790
+ " renderActivePage(false);",
791
+ " };",
631
792
  " const onPopState = () => {",
632
793
  " deferRouteSync(syncRouteFromDom);",
633
794
  " };",
@@ -643,6 +804,7 @@ const defaultClientEntry = (ctx) => {
643
804
  " patchHistoryForRouteSync();",
644
805
  " renderActivePage(true);",
645
806
  " addRuntimeListener(window, ROUTE_PAYLOAD_EVENT, onRoutePayloadApplied);",
807
+ " addRuntimeListener(window, ROUTE_DATA_EVENT, onRouteDataApplied);",
646
808
  " addRuntimeListener(window, \"popstate\", onPopState);",
647
809
  "} else if (runtimeAlreadyInitialized) {",
648
810
  " const rerenderRuntime = window.__HYPER_CLIENT_RUNTIME_RERENDER__;",
@@ -657,6 +819,7 @@ const defaultClientEntry = (ctx) => {
657
819
  };
658
820
  const defaultServerEntry = (ctx) => {
659
821
  const pageImport = resolvePageImport(ctx);
822
+ const appImport = resolveAppImport(ctx);
660
823
  const hydrationMode = JSON.stringify(resolveHydrationMode(ctx));
661
824
  const adapterOptions = (ctx.uiOptions ?? ctx.adapterOptions);
662
825
  const nestedLayoutsEnabled = resolveNestedLayoutsEnabled(adapterOptions);
@@ -666,48 +829,94 @@ const defaultServerEntry = (ctx) => {
666
829
  const layoutImports = layoutSpecifiers.map((specifier, index) => `import Layout${index} from ${JSON.stringify(specifier)};`);
667
830
  const layoutRefs = layoutSpecifiers.map((_specifier, index) => `Layout${index}`);
668
831
  const layoutArray = layoutRefs.length > 0 ? `[${layoutRefs.join(", ")}]` : "[]";
832
+ const appImportSource = appImport ? `import * as AppModule from ${JSON.stringify(appImport)};` : "";
833
+ const appResolverSource = appImport
834
+ ? [
835
+ "const resolveAppComponent = (module) => {",
836
+ " if (!module || typeof module !== \"object\") {",
837
+ " return null;",
838
+ " }",
839
+ " if (typeof module.default === \"function\") {",
840
+ " return module.default;",
841
+ " }",
842
+ " if (typeof module.App === \"function\") {",
843
+ " return module.App;",
844
+ " }",
845
+ " return null;",
846
+ "};",
847
+ "const AppComponent = resolveAppComponent(AppModule);",
848
+ ]
849
+ : ["const AppComponent = null;"];
669
850
  return [
670
851
  "import React from \"react\";",
671
852
  "import { renderToString, renderToPipeableStream } from \"react-dom/server\";",
672
853
  "import { PassThrough } from \"stream\";",
673
- "import { collectHeadFromTree } from \"@tyndall/react\";",
854
+ "import { collectHeadFromTree, RouteDataProvider } from \"@tyndall/react\";",
674
855
  `import Page from ${pageImport};`,
856
+ ...(appImportSource ? [appImportSource] : []),
675
857
  ...layoutImports,
858
+ ...appResolverSource,
676
859
  `export const hydration = ${hydrationMode};`,
677
860
  "export const renderToHtml = async (ctx) => {",
678
- " let element = React.createElement(Page, ctx.props);",
679
861
  ` 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 });",
862
+ " const createRoutedTree = (pageProps) => {",
863
+ " let element = React.createElement(Page, pageProps);",
864
+ " if (Array.isArray(layouts) && layouts.length > 0) {",
865
+ " for (let i = layouts.length - 1; i >= 0; i -= 1) {",
866
+ " const Layout = layouts[i];",
867
+ " if (typeof Layout === \"function\") {",
868
+ " element = React.createElement(Layout, { ...pageProps, children: element });",
869
+ " }",
685
870
  " }",
686
871
  " }",
687
- " }",
688
- " const html = renderToString(element);",
689
- " const head = collectHeadFromTree(element);",
872
+ " return element;",
873
+ " };",
874
+ " const routedElement = createRoutedTree(ctx.props);",
875
+ " const appTree = AppComponent",
876
+ " ? React.createElement(AppComponent, {",
877
+ " Component: createRoutedTree,",
878
+ " pageProps: ctx.props,",
879
+ " routeData: ctx.routeData ?? {},",
880
+ " routeId: ctx.routeId,",
881
+ " })",
882
+ " : routedElement;",
883
+ " const tree = React.createElement(RouteDataProvider, { value: ctx.routeData ?? {} }, appTree);",
884
+ " const html = renderToString(tree);",
885
+ " const head = collectHeadFromTree(tree);",
690
886
  " return { html, head };",
691
887
  "};",
692
888
  "export const renderToStream = async (ctx) => {",
693
889
  " if (typeof renderToPipeableStream !== \"function\") {",
694
890
  " return null;",
695
891
  " }",
696
- " let element = React.createElement(Page, ctx.props);",
697
892
  ` 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 });",
893
+ " const createRoutedTree = (pageProps) => {",
894
+ " let element = React.createElement(Page, pageProps);",
895
+ " if (Array.isArray(layouts) && layouts.length > 0) {",
896
+ " for (let i = layouts.length - 1; i >= 0; i -= 1) {",
897
+ " const Layout = layouts[i];",
898
+ " if (typeof Layout === \"function\") {",
899
+ " element = React.createElement(Layout, { ...pageProps, children: element });",
900
+ " }",
703
901
  " }",
704
902
  " }",
705
- " }",
706
- " const head = collectHeadFromTree(element);",
903
+ " return element;",
904
+ " };",
905
+ " const routedElement = createRoutedTree(ctx.props);",
906
+ " const appTree = AppComponent",
907
+ " ? React.createElement(AppComponent, {",
908
+ " Component: createRoutedTree,",
909
+ " pageProps: ctx.props,",
910
+ " routeData: ctx.routeData ?? {},",
911
+ " routeId: ctx.routeId,",
912
+ " })",
913
+ " : routedElement;",
914
+ " const tree = React.createElement(RouteDataProvider, { value: ctx.routeData ?? {} }, appTree);",
915
+ " const head = collectHeadFromTree(tree);",
707
916
  " return await new Promise((resolve, reject) => {",
708
917
  " let didError = false;",
709
918
  " const stream = new PassThrough();",
710
- " const { pipe, abort } = renderToPipeableStream(element, {",
919
+ " const { pipe, abort } = renderToPipeableStream(tree, {",
711
920
  " onShellReady() {",
712
921
  " pipe(stream);",
713
922
  " resolve({ stream, head, abort });",
@@ -733,8 +942,19 @@ const defaultRenderToHtml = async (ctx, options) => {
733
942
  const element = await options.render(ctx);
734
943
  const layouts = await resolveLayoutComponentsForRender(ctx.routeId, options);
735
944
  const composed = layouts.length > 0 ? applyLayoutsToElement(element, ctx.props, layouts) : element;
736
- const html = await renderWithFallback(composed);
737
- const collected = collectHeadFromTree(composed);
945
+ const appComponent = await resolveAppComponentForRender(options);
946
+ const RoutedPage = () => composed;
947
+ const appTree = appComponent
948
+ ? React.createElement(appComponent, {
949
+ Component: RoutedPage,
950
+ pageProps: ctx.props,
951
+ routeData: ctx.routeData ?? {},
952
+ routeId: ctx.routeId,
953
+ })
954
+ : composed;
955
+ const tree = React.createElement(RouteDataProvider, { value: ctx.routeData ?? {} }, appTree);
956
+ const html = await renderWithFallback(tree);
957
+ const collected = collectHeadFromTree(tree);
738
958
  const provided = options.getHead?.(ctx);
739
959
  const head = mergeHeadDescriptors(provided ?? {}, collected);
740
960
  return { html, head };
@@ -0,0 +1,2 @@
1
+ export declare const useBlockRouting: (enabled: boolean, message?: string) => void;
2
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,EAAE,UAAU,MAAM,KAAG,IA8BpE,CAAC"}
package/dist/hooks.js ADDED
@@ -0,0 +1,31 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { getRouter } from "./router.js";
3
+ export const useBlockRouting = (enabled, message) => {
4
+ const messageRef = useRef(message);
5
+ messageRef.current = message;
6
+ useEffect(() => {
7
+ if (!enabled) {
8
+ return;
9
+ }
10
+ const router = getRouter();
11
+ const blocker = () => messageRef.current ?? "";
12
+ const unblock = router.block(blocker);
13
+ const handleBeforeUnload = (event) => {
14
+ const text = messageRef.current ?? "";
15
+ if (!text) {
16
+ return;
17
+ }
18
+ event.preventDefault();
19
+ event.returnValue = text;
20
+ };
21
+ if (typeof window !== "undefined") {
22
+ window.addEventListener("beforeunload", handleBeforeUnload);
23
+ }
24
+ return () => {
25
+ unblock();
26
+ if (typeof window !== "undefined") {
27
+ window.removeEventListener("beforeunload", handleBeforeUnload);
28
+ }
29
+ };
30
+ }, [enabled]);
31
+ };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  export { createReactAdapter } from "./adapter.js";
2
- export type { ReactAdapterOptions } from "./adapter.js";
2
+ export type { AppComponentProps, ReactAdapterOptions } from "./adapter.js";
3
3
  export { createReactUiAdapterFactory, createReactAdapterRegistry } from "./registry.js";
4
4
  export { Link, Head, RouterProvider } from "./components.js";
5
5
  export type { LinkProps, HeadProps, RouterProviderProps, ElementLike } from "./components.js";
6
+ export { RouteDataProvider, useRouteData } from "./route-data.js";
7
+ export type { RouteDataMap } from "./route-data.js";
6
8
  export { createRouter, getRouter, useRouter } from "./router.js";
7
- export type { ClientRenderMode, ClientRouteModule, ClientRouteModuleContext, ClientRouteModuleLoader, ClientRouteModuleLoaderMap, ClientRouteModuleResolver, ClientRouteModuleResolverContext, ClientRouteRenderResult, DynamicManifestEnvelope, DynamicManifestRenderer, DynamicManifestResolver, HydrationMode, NavigationMode, RenderPolicy, RoutePayload, RoutePayloadApplier, RoutePayloadResolver, Router, RouterOptions, RouterQuery, } from "./router.js";
9
+ export { useBlockRouting } from "./hooks.js";
10
+ export type { ClientRenderMode, ClientRouteModule, ClientRouteModuleContext, ClientRouteModuleLoader, ClientRouteModuleLoaderMap, ClientRouteModuleResolver, ClientRouteModuleResolverContext, ClientRouteRenderResult, DynamicManifestEnvelope, DynamicManifestRenderer, DynamicManifestResolver, HydrationMode, NavigationMode, NavigationBlockContext, NavigationBlocker, RenderPolicy, RoutePayload, RouteRedirectPayload, RoutePayloadApplier, RoutePayloadResolver, Router, RouterOptions, RouterQuery, } from "./router.js";
8
11
  export { createClientRouteModuleResolver } from "./router.js";
9
12
  export { installRouterIslands } from "./islands.js";
10
13
  export { collectHeadFromTree } from "./head.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAExF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC7D,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9F,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,wBAAwB,EACxB,uBAAuB,EACvB,0BAA0B,EAC1B,yBAAyB,EACzB,gCAAgC,EAChC,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EACvB,aAAa,EACb,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,oBAAoB,EACpB,MAAM,EACN,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EACL,0BAA0B,EAC1B,6BAA6B,GAC9B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,oBAAoB,EACpB,yBAAyB,EACzB,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,uBAAuB,CAAC;AAE/B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAExF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC7D,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9F,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,wBAAwB,EACxB,uBAAuB,EACvB,0BAA0B,EAC1B,yBAAyB,EACzB,gCAAgC,EAChC,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EACvB,uBAAuB,EACvB,aAAa,EACb,cAAc,EACd,sBAAsB,EACtB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,MAAM,EACN,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EACL,0BAA0B,EAC1B,6BAA6B,GAC9B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,oBAAoB,EACpB,yBAAyB,EACzB,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,uBAAuB,CAAC;AAE/B,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  export { createReactAdapter } from "./adapter.js";
2
2
  export { createReactUiAdapterFactory, createReactAdapterRegistry } from "./registry.js";
3
3
  export { Link, Head, RouterProvider } from "./components.js";
4
+ export { RouteDataProvider, useRouteData } from "./route-data.js";
4
5
  export { createRouter, getRouter, useRouter } from "./router.js";
6
+ export { useBlockRouting } from "./hooks.js";
5
7
  export { createClientRouteModuleResolver } from "./router.js";
6
8
  export { installRouterIslands } from "./islands.js";
7
9
  export { collectHeadFromTree } from "./head.js";
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAWzF,eAAO,MAAM,2BAA2B,QAAO,gBAiB9C,CAAC;AAEF,eAAO,MAAM,0BAA0B,QAAO,iBAE5C,CAAC"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA8B,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAiBrG,eAAO,MAAM,2BAA2B,QAAO,gBAwB9C,CAAC;AAEF,eAAO,MAAM,0BAA0B,QAAO,iBAE5C,CAAC"}
package/dist/registry.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createElement } from "react";
1
2
  import { createReactAdapter } from "./adapter.js";
2
3
  const isFunction = (value) => typeof value === "function";
3
4
  export const createReactUiAdapterFactory = () => (options) => {
@@ -9,8 +10,14 @@ export const createReactUiAdapterFactory = () => (options) => {
9
10
  ? normalized.routeHead
10
11
  : undefined;
11
12
  return createReactAdapter({
12
- render: routeRender ? ({ props }) => routeRender(props) : undefined,
13
+ render: routeRender ? ({ props }) => createElement(routeRender, props) : undefined,
13
14
  getHead: routeHead ? ({ props }) => routeHead(props) : undefined,
15
+ routeGraph: normalized.routeGraph,
16
+ rootDir: normalized.rootDir,
17
+ routeRoot: normalized.routeRoot,
18
+ nestedLayouts: normalized.nestedLayouts,
19
+ appModule: typeof normalized.appModule === "string" ? normalized.appModule : undefined,
20
+ appComponent: typeof normalized.appComponent === "function" ? normalized.appComponent : undefined,
14
21
  });
15
22
  };
16
23
  export const createReactAdapterRegistry = () => ({
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ export type RouteDataMap = Record<string, unknown>;
3
+ export declare const RouteDataProvider: React.Provider<RouteDataMap>;
4
+ export declare const useRouteData: () => RouteDataMap;
5
+ //# sourceMappingURL=route-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-data.d.ts","sourceRoot":"","sources":["../src/route-data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAInD,eAAO,MAAM,iBAAiB,8BAA4B,CAAC;AAE3D,eAAO,MAAM,YAAY,QAAO,YAAkD,CAAC"}