@tschk/moonshine-react 0.3.1 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tschk/moonshine-react",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "React renderer, islands, and signal hooks for Moonshine",
5
5
  "license": "ISC",
6
6
  "type": "module",
@@ -16,8 +16,8 @@
16
16
  "test": "bun test"
17
17
  },
18
18
  "dependencies": {
19
- "@tschk/moonshine": "^0.3.1",
20
- "@tschk/moonshine-framework": "^0.3.1",
19
+ "@tschk/moonshine": "^0.3.3",
20
+ "@tschk/moonshine-framework": "^0.3.3",
21
21
  "react": "^19.1.0",
22
22
  "react-dom": "^19.1.0"
23
23
  },
package/src/client.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  createElement,
3
+ useCallback,
3
4
  useSyncExternalStore,
4
5
  type ComponentType,
5
6
  type ReactNode,
@@ -93,9 +94,14 @@ export function useSignal<T>(
93
94
  source: Subscribable<T>,
94
95
  getServerSnapshot?: () => T,
95
96
  ): T {
96
- const subscribe = (onStoreChange: () => void) =>
97
- source.subscribe(onStoreChange);
98
- const getSnapshot = () => source();
97
+ // A fresh `subscribe` identity makes useSyncExternalStore resubscribe on
98
+ // every render, and dropping a memo's last listener tears down its whole
99
+ // upstream chain, so keep it stable for as long as the source is.
100
+ const subscribe = useCallback(
101
+ (onStoreChange: () => void) => source.subscribe(onStoreChange),
102
+ [source],
103
+ );
104
+ const getSnapshot = useCallback(() => source(), [source]);
99
105
  const server =
100
106
  getServerSnapshot ??
101
107
  (() =>
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { reactRenderer } from "./renderer";
1
+ export { reactRenderer, registerRouteModules } from "./renderer";
2
2
  export type { RenderContext, Renderer } from "@tschk/moonshine-framework";
3
3
  export {
4
4
  batch,
package/src/renderer.tsx CHANGED
@@ -28,9 +28,22 @@ type LayoutProps = PageProps & {
28
28
  children?: ReactNode;
29
29
  };
30
30
 
31
+ const routeModules = new Map<string, unknown>();
32
+
33
+ /**
34
+ * Supplies statically bundled route modules. Runtimes that cannot resolve a
35
+ * dynamic `import(file)` — Cloudflare Workers most notably — register the map
36
+ * their build already produced instead.
37
+ */
38
+ export function registerRouteModules(modules: Record<string, unknown>): void {
39
+ for (const [key, mod] of Object.entries(modules)) routeModules.set(key, mod);
40
+ }
41
+
31
42
  async function loadComponent(file: string): Promise<ComponentType<unknown>> {
32
- const mod = await import(file);
33
- return (mod.default ?? mod) as ComponentType<unknown>;
43
+ const registered = routeModules.get(file);
44
+ const mod = registered ?? (await import(file));
45
+ return ((mod as { default?: unknown }).default ??
46
+ mod) as ComponentType<unknown>;
34
47
  }
35
48
 
36
49
  async function buildBody(
package/src/serialize.ts CHANGED
@@ -14,16 +14,21 @@ function escapeHtmlValue(value: string): string {
14
14
  }
15
15
 
16
16
  export function serializeIslandProps(value: unknown): string {
17
- const seen = new WeakSet<object>();
17
+ // Ancestor chain, not a visited set: a value repeated in sibling positions
18
+ // is shared, not cyclic, and must still serialize.
19
+ const ancestors: unknown[] = [];
18
20
  function replacer(this: object, _key: string, v: unknown): unknown {
19
21
  if (v === undefined) return undefined;
20
22
  const type = typeof v;
21
23
  if (type === "function" || type === "symbol" || type === "bigint") {
22
24
  throw new TypeError(`cannot serialize ${type}`);
23
25
  }
24
- if (v !== null && (typeof v === "object" || Array.isArray(v))) {
25
- if (seen.has(v)) throw new TypeError("cyclic reference");
26
- seen.add(v);
26
+ if (v !== null && typeof v === "object") {
27
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
28
+ ancestors.pop();
29
+ }
30
+ if (ancestors.includes(v)) throw new TypeError("cyclic reference");
31
+ ancestors.push(v);
27
32
  }
28
33
  return v;
29
34
  }