@moloodev/layout-sdk 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ import { createElement, type ComponentType, type ReactNode } from "react";
2
+ import type { Spec } from "@json-render/core";
3
+ import type {
4
+ ComponentRenderProps,
5
+ ComponentRegistry,
6
+ } from "@json-render/react";
7
+ import {
8
+ valueValidator,
9
+ type ComponentContract,
10
+ type ResourceReference,
11
+ } from "./catalog-format";
12
+ import {
13
+ contractIdentity,
14
+ prepareSpec,
15
+ SpecError,
16
+ type ResolvedCatalog,
17
+ } from "./catalog";
18
+
19
+ type ResolvedResource = { reference: ResourceReference; url: string };
20
+ /** React 19 refs return a cleanup for each mounted slot root, including repeated instances. */
21
+ export type NativeSlotRefs = Readonly<
22
+ Record<string, (root: unknown | null) => void | (() => void)>
23
+ >;
24
+ export type RuntimeComponentProps = ComponentRenderProps & {
25
+ /** Optional observation of the implementation's actual root, without a layout wrapper. */
26
+ rootRef?: (root: unknown | null) => void;
27
+ /** Optional native slot containers. Attach directly as refs or forward their returned cleanup. */
28
+ slotRefs?: NativeSlotRefs;
29
+ surfaces?: Record<string, ReactNode>;
30
+ resources: Record<string, ResolvedResource>;
31
+ };
32
+ export type ImplementationPackage = {
33
+ name: string;
34
+ version: string;
35
+ target: string;
36
+ abi: 1;
37
+ entries: Record<
38
+ string,
39
+ {
40
+ contract: ComponentContract;
41
+ component: ComponentType<RuntimeComponentProps>;
42
+ }
43
+ >;
44
+ };
45
+ export type PreparedSurface = {
46
+ target: string;
47
+ spec: Spec;
48
+ registry: ComponentRegistry;
49
+ };
50
+
51
+ export class RuntimeError extends Error {
52
+ constructor(
53
+ readonly code: string,
54
+ readonly element?: string,
55
+ readonly field?: string,
56
+ ) {
57
+ super([code, element, field].filter(Boolean).join(": "));
58
+ this.name = "RuntimeError";
59
+ }
60
+ }
61
+
62
+ /** Prepare before mounting. All references resolve against explicitly installed packages. */
63
+ export function prepareSurface({
64
+ catalog,
65
+ target,
66
+ spec: input,
67
+ packages,
68
+ resolveResource,
69
+ }: {
70
+ catalog: ResolvedCatalog;
71
+ target: string;
72
+ spec: Spec;
73
+ packages: readonly ImplementationPackage[];
74
+ resolveResource: (reference: ResourceReference) => string;
75
+ }): PreparedSurface {
76
+ let spec: Spec;
77
+ try {
78
+ spec = prepareSpec(catalog, target, input);
79
+ } catch (error) {
80
+ if (error instanceof SpecError)
81
+ throw new RuntimeError(error.code, error.element, error.field);
82
+ throw error;
83
+ }
84
+ return bindSurface({ catalog, target, spec, packages, resolveResource });
85
+ }
86
+
87
+ /** Internal binding step. Callers must pass a spec returned by prepareSpec/validateLayout.
88
+ * Not exposed by the package; public preparation entry points always validate. */
89
+ export function bindSurface({
90
+ catalog,
91
+ target,
92
+ spec,
93
+ packages,
94
+ resolveResource,
95
+ }: {
96
+ catalog: ResolvedCatalog;
97
+ target: string;
98
+ spec: Spec;
99
+ packages: readonly ImplementationPackage[];
100
+ resolveResource: (reference: ResourceReference) => string;
101
+ }): PreparedSurface {
102
+ const used = new Set(
103
+ Object.values(spec.elements).map((element) => element.type),
104
+ );
105
+
106
+ const installed = new Map<string, ImplementationPackage>();
107
+ const packageKey = (name: string, version: string, renderer: string) =>
108
+ JSON.stringify([name, version, renderer]);
109
+ for (const implementation of packages) {
110
+ const key = packageKey(
111
+ implementation.name,
112
+ implementation.version,
113
+ implementation.target,
114
+ );
115
+ if (installed.has(key))
116
+ throw new RuntimeError(
117
+ "duplicate_implementation_package",
118
+ implementation.name,
119
+ );
120
+ installed.set(key, implementation);
121
+ }
122
+ const registry: ComponentRegistry = Object.create(null);
123
+ for (const key of used) {
124
+ const definition = catalog.components[key]!;
125
+ const reference = definition.implementations[target]!;
126
+ const implementation = installed.get(
127
+ packageKey(reference.package, reference.version, target),
128
+ );
129
+ if (!implementation || implementation.abi !== reference.abi)
130
+ throw new RuntimeError("missing_implementation_package", key);
131
+ const entry = Object.hasOwn(implementation.entries, reference.entry)
132
+ ? implementation.entries[reference.entry]
133
+ : undefined;
134
+ if (!entry)
135
+ throw new RuntimeError(
136
+ "missing_implementation_entry",
137
+ key,
138
+ reference.entry,
139
+ );
140
+ if (
141
+ contractIdentity(entry.contract) !== contractIdentity(definition.contract)
142
+ )
143
+ throw new RuntimeError("implementation_contract_mismatch", key);
144
+ const resources = Object.fromEntries(
145
+ Object.entries(definition.resources).map(([slot, resource]) => {
146
+ const url = resolveResource(resource);
147
+ if (!url) throw new RuntimeError("missing_resource_url", key, slot);
148
+ return [slot, { reference: resource, url }];
149
+ }),
150
+ );
151
+ const propsSchema = valueValidator(definition.contract.props);
152
+ registry[key] = function ValidatedComponent(props: ComponentRenderProps) {
153
+ // json-render resolves state/repeat bindings first. Validate actual runtime values too.
154
+ if (!propsSchema.safeParse(props.element.props).success)
155
+ throw new RuntimeError("invalid_resolved_props", key);
156
+ const emit = (event: string) => {
157
+ if (!definition.contract.events.includes(event))
158
+ throw new RuntimeError("undeclared_event", key, event);
159
+ props.emit(event);
160
+ };
161
+ return createElement(entry.component, { ...props, emit, resources });
162
+ };
163
+ }
164
+ return { target, spec, registry };
165
+ }
package/src/three.tsx ADDED
@@ -0,0 +1,19 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { isDevtoolsActive, subscribeDevtoolsActive } from "@json-render/core";
3
+ import { useThree } from "@react-three/fiber";
4
+ import { SurfaceContent, type SurfaceProps } from "./react";
5
+ import { RuntimeError } from "./runtime";
6
+
7
+ /** Mount under a real R3F root; usable with the R3F unit test renderer as well. */
8
+ export function ThreeScene(props: SurfaceProps) {
9
+ useThree();
10
+ const domDevtools = useSyncExternalStore(
11
+ subscribeDevtoolsActive,
12
+ isDevtoolsActive,
13
+ () => false,
14
+ );
15
+ if (domDevtools) throw new RuntimeError("dom_devtools_in_three_surface");
16
+ if (props.surface.target !== "react-three-fiber")
17
+ throw new RuntimeError("wrong_surface_target");
18
+ return <SurfaceContent {...props} />;
19
+ }