@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.
- package/dist/bookPreview.d.ts +3 -0
- package/dist/bookPreview.d.ts.map +1 -1
- package/dist/bookPreview.js +5 -0
- package/dist/bookPreview.js.map +1 -1
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -0
- package/dist/esbuildFlatten.d.ts +15 -0
- package/dist/esbuildFlatten.d.ts.map +1 -0
- package/dist/esbuildFlatten.js +39 -0
- package/dist/esbuildFlatten.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +2 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +51 -14
- package/dist/loader.js.map +1 -1
- package/dist/node/bookConfig.d.ts +13 -0
- package/dist/node/bookConfig.d.ts.map +1 -0
- package/dist/node/bookConfig.js +54 -0
- package/dist/node/bookConfig.js.map +1 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.js.map +1 -0
- package/dist/packageScope.d.ts.map +1 -1
- package/dist/packageScope.js +15 -7
- package/dist/packageScope.js.map +1 -1
- package/dist/propsSchema.d.ts +2 -0
- package/dist/propsSchema.d.ts.map +1 -1
- package/dist/propsSchema.js +36 -0
- package/dist/propsSchema.js.map +1 -1
- package/dist/react/createRuntime.d.ts.map +1 -1
- package/dist/react/createRuntime.js +3 -21
- package/dist/react/createRuntime.js.map +1 -1
- package/dist/react/useRegistryModule.d.ts +4 -0
- package/dist/react/useRegistryModule.d.ts.map +1 -0
- package/dist/react/useRegistryModule.js +8 -0
- package/dist/react/useRegistryModule.js.map +1 -0
- package/dist/registry.d.ts +13 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +52 -5
- package/dist/registry.js.map +1 -1
- package/dist/registryModule.d.ts.map +1 -1
- package/dist/registryModule.js +13 -2
- package/dist/registryModule.js.map +1 -1
- package/dist/relativeRequires.d.ts +12 -0
- package/dist/relativeRequires.d.ts.map +1 -1
- package/dist/relativeRequires.js +33 -0
- package/dist/relativeRequires.js.map +1 -1
- package/dist/sources/remote.d.ts +14 -8
- package/dist/sources/remote.d.ts.map +1 -1
- package/dist/sources/remote.js +73 -19
- package/dist/sources/remote.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -8
- package/src/bookPreview.ts +7 -0
- package/src/constants.ts +9 -0
- package/src/esbuildFlatten.ts +47 -0
- package/src/index.ts +22 -2
- package/src/loader.ts +42 -14
- package/src/node/bookConfig.ts +63 -0
- package/src/node/index.ts +9 -0
- package/src/packageScope.ts +15 -7
- package/src/propsSchema.ts +35 -0
- package/src/react/createRuntime.tsx +3 -16
- package/src/react/useRegistryModule.ts +15 -0
- package/src/registry.ts +58 -5
- package/src/registryModule.ts +12 -3
- package/src/relativeRequires.ts +48 -0
- package/src/sources/remote.ts +79 -26
- package/src/types.ts +6 -0
- package/LICENSE +0 -184
package/dist/types.d.ts.map
CHANGED
|
@@ -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.
|
|
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
|
+
}
|
package/src/bookPreview.ts
CHANGED
|
@@ -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). */
|
package/src/constants.ts
ADDED
|
@@ -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
|
-
/**
|
|
4
|
-
function
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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",
|
|
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
|
+
}
|
package/src/packageScope.ts
CHANGED
|
@@ -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),
|
package/src/propsSchema.ts
CHANGED
|
@@ -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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
|
65
|
-
|
|
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)
|
|
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)
|
|
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);
|
package/src/registryModule.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|