@omriashke/dynamico-core 0.1.9 → 0.1.12

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.
Files changed (77) hide show
  1. package/dist/bookPreview.d.ts +3 -0
  2. package/dist/bookPreview.d.ts.map +1 -1
  3. package/dist/bookPreview.js +5 -0
  4. package/dist/bookPreview.js.map +1 -1
  5. package/dist/constants.d.ts +6 -0
  6. package/dist/constants.d.ts.map +1 -0
  7. package/dist/constants.js +8 -0
  8. package/dist/constants.js.map +1 -0
  9. package/dist/esbuildFlatten.d.ts +15 -0
  10. package/dist/esbuildFlatten.d.ts.map +1 -0
  11. package/dist/esbuildFlatten.js +39 -0
  12. package/dist/esbuildFlatten.js.map +1 -0
  13. package/dist/index.d.ts +7 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +7 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/loader.d.ts +2 -0
  18. package/dist/loader.d.ts.map +1 -1
  19. package/dist/loader.js +51 -14
  20. package/dist/loader.js.map +1 -1
  21. package/dist/node/bookConfig.d.ts +13 -0
  22. package/dist/node/bookConfig.d.ts.map +1 -0
  23. package/dist/node/bookConfig.js +54 -0
  24. package/dist/node/bookConfig.js.map +1 -0
  25. package/dist/node/index.d.ts +2 -0
  26. package/dist/node/index.d.ts.map +1 -0
  27. package/dist/node/index.js +2 -0
  28. package/dist/node/index.js.map +1 -0
  29. package/dist/packageScope.d.ts.map +1 -1
  30. package/dist/packageScope.js +15 -7
  31. package/dist/packageScope.js.map +1 -1
  32. package/dist/propsSchema.d.ts +2 -0
  33. package/dist/propsSchema.d.ts.map +1 -1
  34. package/dist/propsSchema.js +36 -0
  35. package/dist/propsSchema.js.map +1 -1
  36. package/dist/react/createRuntime.d.ts.map +1 -1
  37. package/dist/react/createRuntime.js +3 -21
  38. package/dist/react/createRuntime.js.map +1 -1
  39. package/dist/react/useRegistryModule.d.ts +4 -0
  40. package/dist/react/useRegistryModule.d.ts.map +1 -0
  41. package/dist/react/useRegistryModule.js +8 -0
  42. package/dist/react/useRegistryModule.js.map +1 -0
  43. package/dist/registry.d.ts +13 -0
  44. package/dist/registry.d.ts.map +1 -1
  45. package/dist/registry.js +52 -5
  46. package/dist/registry.js.map +1 -1
  47. package/dist/registryModule.d.ts.map +1 -1
  48. package/dist/registryModule.js +13 -2
  49. package/dist/registryModule.js.map +1 -1
  50. package/dist/relativeRequires.d.ts +12 -0
  51. package/dist/relativeRequires.d.ts.map +1 -1
  52. package/dist/relativeRequires.js +33 -0
  53. package/dist/relativeRequires.js.map +1 -1
  54. package/dist/sources/remote.d.ts +14 -8
  55. package/dist/sources/remote.d.ts.map +1 -1
  56. package/dist/sources/remote.js +73 -19
  57. package/dist/sources/remote.js.map +1 -1
  58. package/dist/types.d.ts +6 -0
  59. package/dist/types.d.ts.map +1 -1
  60. package/package.json +17 -8
  61. package/src/bookPreview.ts +7 -0
  62. package/src/constants.ts +9 -0
  63. package/src/esbuildFlatten.ts +47 -0
  64. package/src/index.ts +22 -2
  65. package/src/loader.ts +42 -14
  66. package/src/node/bookConfig.ts +63 -0
  67. package/src/node/index.ts +9 -0
  68. package/src/packageScope.ts +15 -7
  69. package/src/propsSchema.ts +35 -0
  70. package/src/react/createRuntime.tsx +3 -16
  71. package/src/react/useRegistryModule.ts +15 -0
  72. package/src/registry.ts +58 -5
  73. package/src/registryModule.ts +12 -3
  74. package/src/relativeRequires.ts +48 -0
  75. package/src/sources/remote.ts +79 -26
  76. package/src/types.ts +6 -0
  77. package/LICENSE +0 -184
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omriashke/dynamico-core",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "Renderer-agnostic core for dynamico: source adapters, module loader, versioned component registry.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,7 +10,14 @@
10
10
  "exports": {
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
13
- "import": "./dist/index.js"
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./node": {
18
+ "types": "./dist/node/index.d.ts",
19
+ "import": "./dist/node/index.js",
20
+ "default": "./dist/node/index.js"
14
21
  }
15
22
  },
16
23
  "files": [
@@ -33,17 +40,19 @@
33
40
  "publishConfig": {
34
41
  "access": "public"
35
42
  },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit",
46
+ "test": "npm run build && node --test test/*.test.mjs",
47
+ "dev": "tsc -p tsconfig.json -w"
48
+ },
36
49
  "peerDependencies": {
37
50
  "react": ">=18"
38
51
  },
39
52
  "devDependencies": {
53
+ "@types/node": "^22.10.0",
40
54
  "@types/react": "^18.3.0",
41
55
  "react": "^19.0.0",
42
56
  "typescript": "^5.6.3"
43
- },
44
- "scripts": {
45
- "build": "tsc -p tsconfig.json",
46
- "typecheck": "tsc -p tsconfig.json --noEmit",
47
- "dev": "tsc -p tsconfig.json -w"
48
57
  }
49
- }
58
+ }
@@ -3,6 +3,13 @@ import type { PropsSchema } from "./types.js";
3
3
 
4
4
  export type BookPreviewJson = Record<string, unknown>;
5
5
 
6
+ /** Filenames recognized as Dynamico Book catalog configs on disk. */
7
+ export const BOOK_CONFIG_FILENAMES = ["book.config.json", "storybook.config.json"] as const;
8
+
9
+ export function isBookConfigFilename(name: string): boolean {
10
+ return (BOOK_CONFIG_FILENAMES as readonly string[]).includes(name);
11
+ }
12
+
6
13
  export interface BookPreviewConfig {
7
14
  fixtures?: Record<string, BookPreviewJson>;
8
15
  /** Registry components wrapping every preview (outermost last). */
@@ -0,0 +1,9 @@
1
+ /** Registry manifest filename at the source directory root. */
2
+ export const MANIFEST_FILENAME = "dynamico.config.json" as const;
3
+
4
+ /** Matches co-located author test files — not registry components. */
5
+ export const COMPONENT_TEST_RE = /\.test\.(tsx|jsx|ts|js)$/;
6
+
7
+ export function isComponentTestFilename(filename: string): boolean {
8
+ return COMPONENT_TEST_RE.test(filename);
9
+ }
@@ -0,0 +1,47 @@
1
+ /** Marker appended once so loadModule does not double-flatten esbuild bundles. */
2
+ export const ESBUILD_FLATTEN_MARKER = ";// dynamico-flat-exports";
3
+
4
+ export interface EsbuildExportEntry {
5
+ exportKey: string;
6
+ varName: string;
7
+ }
8
+
9
+ /** Parse esbuild `__export(mod, { name: () => var, ... })` named export entries. */
10
+ export function parseEsbuildNamedExports(code: string): EsbuildExportEntry[] {
11
+ const exportBlockMatch = code.match(/__export\(\w+,\s*\{([\s\S]*?)\}\s*\)/);
12
+ if (!exportBlockMatch) return [];
13
+ const entries: EsbuildExportEntry[] = [];
14
+ for (const m of exportBlockMatch[1].matchAll(/(\w+):\s*\(\)\s*=>\s*(\w+)/g)) {
15
+ entries.push({ exportKey: m[1], varName: m[2] });
16
+ }
17
+ return entries;
18
+ }
19
+
20
+ /**
21
+ * Append a plain `module.exports = { ... }` assignment so Hermes and relative
22
+ * imports (e.g. `require("../Colors").Colors`) see named exports, not only
23
+ * getter-based defaults.
24
+ */
25
+ export function appendPlainEsbuildExports(code: string): string {
26
+ if (code.includes(ESBUILD_FLATTEN_MARKER)) return code;
27
+
28
+ const entries = parseEsbuildNamedExports(code);
29
+ if (entries.length === 0) {
30
+ const defaultMatch = code.match(/default:\s*\(\)\s*=>\s*(\w+)/);
31
+ if (!defaultMatch) return code;
32
+ const fn = defaultMatch[1];
33
+ const propsMatch = code.match(/propsSchema:\s*\(\)\s*=>\s*(\w+)/);
34
+ const propsPart = propsMatch ? `,propsSchema:${propsMatch[1]}` : "";
35
+ return `${code}${ESBUILD_FLATTEN_MARKER}\n;(function(){try{if(typeof ${fn}==='function'){module.exports={__esModule:true,default:${fn}${propsPart}};}}catch(e){}})();\n`;
36
+ }
37
+
38
+ const parts = entries.map(({ exportKey, varName }) =>
39
+ exportKey === "default" ? `default:${varName}` : `${exportKey}:${varName}`,
40
+ );
41
+ const defaultEntry = entries.find((e) => e.exportKey === "default");
42
+ const guard = defaultEntry
43
+ ? `(typeof ${defaultEntry.varName}!=='undefined')`
44
+ : "true";
45
+
46
+ return `${code}${ESBUILD_FLATTEN_MARKER}\n;(function(){try{if(${guard}){module.exports={__esModule:true,${parts.join(",")}};}}catch(e){}})();\n`;
47
+ }
package/src/index.ts CHANGED
@@ -17,9 +17,14 @@ export type {
17
17
  } from "./types.js";
18
18
 
19
19
  export { Registry } from "./registry.js";
20
- export { loadModule } from "./loader.js";
20
+ export { loadModule, resolveModuleDefault } from "./loader.js";
21
+ export {
22
+ appendPlainEsbuildExports,
23
+ parseEsbuildNamedExports,
24
+ ESBUILD_FLATTEN_MARKER,
25
+ } from "./esbuildFlatten.js";
21
26
  export { createRemoteSource, type RemoteSourceOptions } from "./sources/remote.js";
22
- export { validateProps, type PropsValidationResult } from "./propsSchema.js";
27
+ export { validateProps, extractPropsSchema, type PropsValidationResult } from "./propsSchema.js";
23
28
  export { generateDefaultProps } from "./defaultProps.js";
24
29
  export {
25
30
  collectBookPreviewPropSets,
@@ -27,12 +32,26 @@ export {
27
32
  resolveBookFixtures,
28
33
  resolveBookPropValues,
29
34
  validateBookPreviewsForComponent,
35
+ BOOK_CONFIG_FILENAMES,
36
+ isBookConfigFilename,
30
37
  type BookPreviewBlock,
31
38
  type BookPreviewConfig,
32
39
  type BookPreviewEntry,
33
40
  type BookPreviewPropSet,
34
41
  type BookPreviewValidationResult,
35
42
  } from "./bookPreview.js";
43
+ export {
44
+ resolveRelativeComponentName,
45
+ extractRelativeRequires,
46
+ collectRelativeComponentDeps,
47
+ validateRelativeImports,
48
+ type RelativeImportValidation,
49
+ } from "./relativeRequires.js";
50
+ export {
51
+ MANIFEST_FILENAME,
52
+ COMPONENT_TEST_RE,
53
+ isComponentTestFilename,
54
+ } from "./constants.js";
36
55
  export {
37
56
  createRuntime,
38
57
  type RuntimeAPI,
@@ -40,6 +59,7 @@ export {
40
59
  type DynamicoProviderProps,
41
60
  type DynamicComponentProps,
42
61
  } from "./react/createRuntime.js";
62
+ export { createUseRegistryModule } from "./react/useRegistryModule.js";
43
63
  export {
44
64
  createPackageScope,
45
65
  createPackageScopeFromNames,
package/src/loader.ts CHANGED
@@ -1,11 +1,26 @@
1
1
  import type { Scope } from "./types.js";
2
+ import { appendPlainEsbuildExports } from "./esbuildFlatten.js";
2
3
 
3
- /** esbuild CJS bundles assign exports before the component function; materialize at end. */
4
- function patchEsbuildDefaultExport(code: string): string {
5
- const m = code.match(/default:\s*\(\)\s*=>\s*(\w+)/);
6
- if (!m) return code;
7
- const fn = m[1];
8
- return `${code}\n;try{var __d=${fn};if(typeof __d==='function'){try{Object.defineProperty(module.exports,'default',{value:__d,enumerable:true,writable:true,configurable:true});}catch(e){module.exports.default=__d;}}}catch(e){}\n`;
4
+ /** Copy exports to a plain object (no getters) so Hermes can read `.default` reliably. */
5
+ function toPlainExports(exports: Record<string, unknown>): Record<string, unknown> {
6
+ const plain: Record<string, unknown> = {};
7
+ for (const key of Object.getOwnPropertyNames(exports)) {
8
+ const desc = Object.getOwnPropertyDescriptor(exports, key);
9
+ if (desc?.get && !desc.set) {
10
+ try {
11
+ const value = desc.get.call(exports);
12
+ if (value !== undefined) plain[key] = value;
13
+ } catch {
14
+ /* skip broken getter */
15
+ }
16
+ } else if (desc && "value" in desc) {
17
+ plain[key] = desc.value;
18
+ } else {
19
+ plain[key] = exports[key];
20
+ }
21
+ }
22
+ if (!("__esModule" in plain)) plain.__esModule = true;
23
+ return plain;
9
24
  }
10
25
 
11
26
  /** Replace getter-only exports with plain values (Hermes-safe). */
@@ -24,7 +39,6 @@ function materializeGetterExports(exports: Record<string, unknown>): void {
24
39
  writable: true,
25
40
  });
26
41
  } catch {
27
- // Hermes may leave non-configurable getters; assign directly.
28
42
  (exports as Record<string, unknown>)[key] = value;
29
43
  }
30
44
  }
@@ -34,6 +48,25 @@ function materializeGetterExports(exports: Record<string, unknown>): void {
34
48
  }
35
49
  }
36
50
 
51
+ /** Resolve the default export from a loaded CJS module object (Hermes-safe). */
52
+ export function resolveModuleDefault(exp: unknown): unknown {
53
+ if (typeof exp === "function") return exp;
54
+ if (!exp || typeof exp !== "object") return undefined;
55
+ const exports = exp as Record<string, unknown>;
56
+ const desc = Object.getOwnPropertyDescriptor(exports, "default");
57
+ if (desc?.get && !desc.set) {
58
+ try {
59
+ const fromGetter = desc.get.call(exports);
60
+ if (typeof fromGetter === "function") return fromGetter;
61
+ } catch {
62
+ /* fall through */
63
+ }
64
+ }
65
+ const d = exports.default;
66
+ if (typeof d === "function") return d;
67
+ return undefined;
68
+ }
69
+
37
70
  /**
38
71
  * Execute a CommonJS-style code string in a controlled scope.
39
72
  *
@@ -57,9 +90,6 @@ export function loadModule(
57
90
  if (name.startsWith("./") || name.startsWith("../") || name.startsWith("/")) {
58
91
  return requireRelative(name);
59
92
  }
60
- // Use `in` (triggers Proxy `has` traps) so that hosts can supply scope
61
- // via a Proxy and resolve module bindings lazily. Falls back to
62
- // `hasOwnProperty` semantics for plain object scopes.
63
93
  if (name in scope) {
64
94
  return scope[name];
65
95
  }
@@ -68,11 +98,9 @@ export function loadModule(
68
98
  );
69
99
  };
70
100
 
71
- // The compiled body is just the function body; arguments are well-known.
72
101
  // eslint-disable-next-line no-new-func
73
- const fn = new Function("module", "exports", "require", patchEsbuildDefaultExport(code));
102
+ const fn = new Function("module", "exports", "require", appendPlainEsbuildExports(code));
74
103
  fn(moduleObj, moduleObj.exports, requireFn);
75
104
  materializeGetterExports(moduleObj.exports);
76
-
77
- return moduleObj.exports;
105
+ return toPlainExports(moduleObj.exports);
78
106
  }
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import {
5
+ BOOK_CONFIG_FILENAMES,
6
+ isBookConfigFilename,
7
+ type BookPreviewConfig,
8
+ } from "../bookPreview.js";
9
+
10
+ export type BookConfigFilename = (typeof BOOK_CONFIG_FILENAMES)[number];
11
+
12
+ export interface BookConfigFile {
13
+ filename: BookConfigFilename;
14
+ source: string;
15
+ }
16
+
17
+ export function bookConfigPathSync(dir: string): string | null {
18
+ for (const name of BOOK_CONFIG_FILENAMES) {
19
+ const filePath = join(dir, name);
20
+ if (existsSync(filePath)) return filePath;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ export function readBookPreviewConfigSync(dir: string): BookPreviewConfig | undefined {
26
+ const filePath = bookConfigPathSync(dir);
27
+ if (!filePath) return undefined;
28
+ try {
29
+ return JSON.parse(readFileSync(filePath, "utf8")) as BookPreviewConfig;
30
+ } catch {
31
+ return undefined;
32
+ }
33
+ }
34
+
35
+ export async function readBookPreviewConfigAsync(dir: string): Promise<BookPreviewConfig | undefined> {
36
+ for (const name of BOOK_CONFIG_FILENAMES) {
37
+ const filePath = join(dir, name);
38
+ if (!existsSync(filePath)) continue;
39
+ try {
40
+ return JSON.parse(await readFile(filePath, "utf8")) as BookPreviewConfig;
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ /** Raw book catalog file to ship with `dynamico push --dir`. */
49
+ export async function readBookConfigFileAsync(dir: string): Promise<BookConfigFile | undefined> {
50
+ for (const filename of BOOK_CONFIG_FILENAMES) {
51
+ const filePath = join(dir, filename);
52
+ if (!existsSync(filePath)) continue;
53
+ const source = await readFile(filePath, "utf8");
54
+ JSON.parse(source);
55
+ return { filename, source };
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ export function isBookConfigPath(relPath: string): boolean {
61
+ const base = relPath.split(/[/\\]/).pop() ?? relPath;
62
+ return isBookConfigFilename(base);
63
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ bookConfigPathSync,
3
+ readBookPreviewConfigSync,
4
+ readBookPreviewConfigAsync,
5
+ readBookConfigFileAsync,
6
+ isBookConfigPath,
7
+ type BookConfigFilename,
8
+ type BookConfigFile,
9
+ } from "./bookConfig.js";
@@ -1,6 +1,7 @@
1
1
  import { createElement, useSyncExternalStore, type ComponentType } from "react";
2
2
  import type { CompiledModule, Scope, Source } from "./types.js";
3
3
  import { loadModule } from "./loader.js";
4
+ import { resolveRelativeComponentName } from "./relativeRequires.js";
4
5
 
5
6
  export interface PackageScopeOptions {
6
7
  /** Registry component names (export name = registry name). */
@@ -22,6 +23,7 @@ interface ModuleState {
22
23
  /** Bumped on every ingest so useSyncExternalStore subscribers re-render. */
23
24
  revision: number;
24
25
  listeners: Set<() => void>;
26
+ watchRelease?: () => void;
25
27
  }
26
28
 
27
29
  /**
@@ -52,9 +54,17 @@ export function createPackageScope(
52
54
 
53
55
  const subscribe = (name: string, listener: () => void): (() => void) => {
54
56
  const state = getState(name);
57
+ const wasEmpty = state.listeners.size === 0;
55
58
  state.listeners.add(listener);
59
+ if (wasEmpty && source.watch) {
60
+ state.watchRelease = source.watch(name);
61
+ }
56
62
  return () => {
57
63
  state.listeners.delete(listener);
64
+ if (state.listeners.size === 0) {
65
+ state.watchRelease?.();
66
+ state.watchRelease = undefined;
67
+ }
58
68
  };
59
69
  };
60
70
 
@@ -74,11 +84,7 @@ export function createPackageScope(
74
84
  let makeLazy: (name: string) => LazyComponent;
75
85
 
76
86
  const requireRelative = (specifier: string): unknown => {
77
- const base = specifier
78
- .replace(/^\.+\//, "")
79
- .replace(/\.[tj]sx?$/, "")
80
- .split("/")
81
- .pop();
87
+ const base = resolveRelativeComponentName(specifier);
82
88
  if (!base) throw new Error(`dynamico: cannot resolve '${specifier}'`);
83
89
  ensureLoaded(base);
84
90
  const dep = modules.get(base)?.factory;
@@ -128,14 +134,16 @@ export function createPackageScope(
128
134
  };
129
135
 
130
136
  source.subscribe(({ module }) => {
131
- if (componentSet.has(module.name)) {
137
+ if (!componentSet.has(module.name)) return;
138
+ const state = getState(module.name);
139
+ if (state.factory !== undefined || state.listeners.size > 0 || state.loading) {
132
140
  ingest(module.name, module);
133
141
  }
134
142
  });
135
143
 
136
144
  makeLazy = (name: string): LazyComponent => {
137
- ensureLoaded(name);
138
145
  const Lazy: LazyComponent = (props) => {
146
+ ensureLoaded(name);
139
147
  const revision = useSyncExternalStore(
140
148
  (cb) => subscribe(name, cb),
141
149
  () => getRevision(name),
@@ -43,3 +43,38 @@ function describe(v: unknown): string {
43
43
  if (typeof v === "function") return "function";
44
44
  return typeof v;
45
45
  }
46
+
47
+ /** Parse `export const propsSchema = { … }` from component source text. */
48
+ export function extractPropsSchema(source: string): PropsSchema | undefined {
49
+ const marker = "export const propsSchema";
50
+ const idx = source.indexOf(marker);
51
+ if (idx < 0) return undefined;
52
+
53
+ const after = source.slice(idx + marker.length);
54
+ const eq = after.indexOf("=");
55
+ if (eq < 0) return undefined;
56
+
57
+ let rest = after.slice(eq + 1).trimStart();
58
+ if (!rest.startsWith("{")) return undefined;
59
+
60
+ let depth = 0;
61
+ let end = 0;
62
+ for (let i = 0; i < rest.length; i++) {
63
+ const ch = rest[i];
64
+ if (ch === "{") depth++;
65
+ else if (ch === "}") {
66
+ depth--;
67
+ if (depth === 0) {
68
+ end = i + 1;
69
+ break;
70
+ }
71
+ }
72
+ }
73
+ if (end === 0) return undefined;
74
+
75
+ try {
76
+ return new Function(`return (${rest.slice(0, end)})`)() as PropsSchema;
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ }
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { Registry } from "../registry.js";
3
+ import { resolveModuleDefault } from "../loader.js";
3
4
  import type {
4
5
  ComponentFactory,
5
6
  DynamicError,
@@ -187,22 +188,8 @@ export function createRuntime(
187
188
  }
188
189
 
189
190
  function pickDefault(factory: ComponentFactory): unknown {
190
- if (typeof factory === "function") return factory;
191
- if (!factory || typeof factory !== "object") return undefined;
192
- if ("default" in factory) {
193
- const desc = Object.getOwnPropertyDescriptor(factory, "default");
194
- if (desc?.get && !desc.set) {
195
- try {
196
- const fromGetter = desc.get.call(factory);
197
- if (typeof fromGetter === "function") return fromGetter;
198
- } catch {
199
- /* fall through */
200
- }
201
- }
202
- const d = factory.default;
203
- if (typeof d === "function") return d;
204
- }
205
- return undefined;
191
+ const d = resolveModuleDefault(factory);
192
+ return typeof d === "function" ? d : undefined;
206
193
  }
207
194
 
208
195
  function defaultErrorView(error: DynamicError): React.ReactElement {
@@ -0,0 +1,15 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import type { RegistryModuleSubscription } from "../registryModule.js";
3
+
4
+ /** React hook factory — re-renders when a registry data module (e.g. Colors) is pushed. */
5
+ export function createUseRegistryModule<T extends Record<string, unknown>>(
6
+ subscription: RegistryModuleSubscription<T>,
7
+ ): () => T {
8
+ return function useRegistryModule() {
9
+ return useSyncExternalStore(
10
+ subscription.subscribe,
11
+ subscription.getSnapshot,
12
+ subscription.getSnapshot,
13
+ );
14
+ };
15
+ }
package/src/registry.ts CHANGED
@@ -23,16 +23,38 @@ export class Registry {
23
23
  private listeners = new Map<string, Set<RegistryListener>>();
24
24
  private anyListeners = new Set<RegistryListener>();
25
25
  private inflight = new Map<string, Promise<RegistryEntry>>();
26
+ /** WS push cache — modules are ingested only when ensure() or a subscriber asks. */
27
+ private moduleCache = new Map<string, import("./types.js").CompiledModule>();
28
+ private watchReleases = new Map<string, () => void>();
26
29
 
27
30
  constructor(
28
31
  private readonly source: Source,
29
32
  private scope: Scope,
30
33
  ) {
31
34
  this.source.subscribe(({ module }) => {
32
- void this.ingestAsync(module.name, module);
35
+ if (module.removed) {
36
+ this.moduleCache.delete(module.name);
37
+ if (this.entries.has(module.name) || (this.listeners.get(module.name)?.size ?? 0) > 0) {
38
+ void this.ingestAsync(module.name, module);
39
+ }
40
+ return;
41
+ }
42
+ this.moduleCache.set(module.name, module);
43
+ if (this.shouldIngestOnPush(module.name)) {
44
+ void this.ingestAsync(module.name, module);
45
+ }
33
46
  });
34
47
  }
35
48
 
49
+ /** Re-ingest a WS push when the component is already loaded or has active subscribers. */
50
+ private shouldIngestOnPush(name: string): boolean {
51
+ return (
52
+ this.entries.has(name) ||
53
+ (this.listeners.get(name)?.size ?? 0) > 0 ||
54
+ this.inflight.has(name)
55
+ );
56
+ }
57
+
36
58
  /** Replace or extend the current scope (rare; typically set once). */
37
59
  setScope(scope: Scope): void {
38
60
  this.scope = scope;
@@ -61,8 +83,8 @@ export class Registry {
61
83
  if (existing) return existing;
62
84
  const pending = this.inflight.get(name);
63
85
  if (pending) return pending;
64
- const p = this.source
65
- .fetch(name)
86
+ const cached = this.moduleCache.get(name);
87
+ const p = (cached ? Promise.resolve(cached) : this.source.fetch(name))
66
88
  .then((module) => this.ingestAsync(name, module))
67
89
  .finally(() => {
68
90
  this.inflight.delete(name);
@@ -78,10 +100,23 @@ export class Registry {
78
100
  set = new Set();
79
101
  this.listeners.set(name, set);
80
102
  }
103
+ const wasEmpty = set.size === 0;
81
104
  set.add(listener);
105
+ if (wasEmpty && this.source.watch) {
106
+ this.watchReleases.set(name, this.source.watch(name));
107
+ }
108
+ const cached = this.moduleCache.get(name);
109
+ if (cached && !this.entries.has(name) && !this.inflight.has(name)) {
110
+ void this.ingestAsync(name, cached);
111
+ }
82
112
  return () => {
83
113
  set!.delete(listener);
84
- if (set!.size === 0) this.listeners.delete(name);
114
+ if (set!.size === 0) {
115
+ this.listeners.delete(name);
116
+ const release = this.watchReleases.get(name);
117
+ release?.();
118
+ this.watchReleases.delete(name);
119
+ }
85
120
  };
86
121
  }
87
122
 
@@ -148,7 +183,10 @@ export class Registry {
148
183
  get(target, prop) {
149
184
  if (typeof prop !== "string") return undefined;
150
185
  const resolved = registry.resolveExport(name, prop);
151
- if (resolved !== undefined) return resolved;
186
+ if (resolved !== undefined) {
187
+ delete target[prop];
188
+ return resolved;
189
+ }
152
190
  if (prop in target) return target[prop];
153
191
  const c = make(prop);
154
192
  target[prop] = c;
@@ -163,11 +201,26 @@ export class Registry {
163
201
  /**
164
202
  * Take a CompiledModule from the source, evaluate it (or record a compile
165
203
  * error), update the entry, and notify listeners.
204
+ *
205
+ * Same-version deduplication: if the current entry already carries this
206
+ * exact version (and has no error), skip re-evaluation entirely. This is the
207
+ * primary guard against the WebSocket connect-replay pattern where the server
208
+ * immediately pushes the current module for every watched component on every
209
+ * (re)connect — without this, each push produces a brand-new JS function
210
+ * object and React unmounts+remounts the entire component tree even though
211
+ * nothing changed, causing visible flicker and lost interaction state.
166
212
  */
167
213
  private async ingestAsync(
168
214
  name: string,
169
215
  module: import("./types.js").CompiledModule,
170
216
  ): Promise<RegistryEntry> {
217
+ if (!module.removed && !module.error) {
218
+ const current = this.entries.get(name);
219
+ if (current && !current.error && current.version === module.version) {
220
+ return current;
221
+ }
222
+ }
223
+
171
224
  if (module.removed) {
172
225
  this.entries.delete(name);
173
226
  this.lazyProxies.delete(name);
@@ -43,6 +43,7 @@ export function createRegistryModuleSubscription<T extends Record<string, unknow
43
43
  ): RegistryModuleSubscription<T> {
44
44
  let snapshot = { ...defaults } as T;
45
45
  const listeners = new Set<() => void>();
46
+ let watchRelease: (() => void) | undefined;
46
47
 
47
48
  const notify = () => {
48
49
  for (const listener of listeners) listener();
@@ -83,12 +84,20 @@ export function createRegistryModuleSubscription<T extends Record<string, unknow
83
84
  }
84
85
  });
85
86
 
86
- void reload();
87
-
88
87
  return {
89
88
  subscribe(listener) {
90
89
  listeners.add(listener);
91
- return () => listeners.delete(listener);
90
+ if (listeners.size === 1) {
91
+ if (source.watch) watchRelease = source.watch(name);
92
+ void reload();
93
+ }
94
+ return () => {
95
+ listeners.delete(listener);
96
+ if (listeners.size === 0) {
97
+ watchRelease?.();
98
+ watchRelease = undefined;
99
+ }
100
+ };
92
101
  },
93
102
  getSnapshot: () => snapshot,
94
103
  proxy,