@ultrst-armada/frnlts 0.0.0-stage → 0.2.2
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 +101 -2
- package/dist/chunk-FGHXAN4Q.js +1125 -0
- package/dist/chunk-LG2AFOLX.js +239 -0
- package/dist/events-C5ocSAHh.d.ts +687 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +134 -0
- package/dist/react/index.d.ts +24 -0
- package/dist/react/index.js +97 -0
- package/dist/runtime/index.d.ts +272 -0
- package/dist/runtime/index.js +134 -0
- package/package.json +70 -4
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyFrnlTheme,
|
|
3
|
+
getFrnlComponentHandle,
|
|
4
|
+
getFrnlEvent,
|
|
5
|
+
getFrnlLibrary,
|
|
6
|
+
registerFrnlTheme,
|
|
7
|
+
setFrnlVisualParameter,
|
|
8
|
+
validateFrnlThemeAgainstDefinition
|
|
9
|
+
} from "./chunk-LG2AFOLX.js";
|
|
10
|
+
|
|
11
|
+
// src/runtime/core.ts
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
var lifecyclePhaseSchema = z.enum([
|
|
14
|
+
"created",
|
|
15
|
+
"initializing",
|
|
16
|
+
"mounted",
|
|
17
|
+
"active",
|
|
18
|
+
"suspended",
|
|
19
|
+
"resuming",
|
|
20
|
+
"disposing",
|
|
21
|
+
"disposed",
|
|
22
|
+
"failed"
|
|
23
|
+
]);
|
|
24
|
+
var allowedTransitions = {
|
|
25
|
+
created: ["initializing", "disposing"],
|
|
26
|
+
initializing: ["mounted", "failed", "disposing"],
|
|
27
|
+
mounted: ["active", "suspended", "disposing", "failed"],
|
|
28
|
+
active: ["suspended", "disposing", "failed"],
|
|
29
|
+
suspended: ["resuming", "disposing", "failed"],
|
|
30
|
+
resuming: ["active", "failed", "disposing"],
|
|
31
|
+
disposing: ["disposed"],
|
|
32
|
+
disposed: [],
|
|
33
|
+
failed: ["initializing", "disposing", "disposed"]
|
|
34
|
+
};
|
|
35
|
+
function canTransition(from, to) {
|
|
36
|
+
return allowedTransitions[from]?.includes(to) ?? false;
|
|
37
|
+
}
|
|
38
|
+
var LifecycleError = class extends Error {
|
|
39
|
+
constructor(from, to) {
|
|
40
|
+
super(`Invalid FrnlTS lifecycle transition: ${from} -> ${to}`);
|
|
41
|
+
this.from = from;
|
|
42
|
+
this.to = to;
|
|
43
|
+
this.name = "LifecycleError";
|
|
44
|
+
}
|
|
45
|
+
from;
|
|
46
|
+
to;
|
|
47
|
+
};
|
|
48
|
+
function createLifecycle(initial = "created") {
|
|
49
|
+
lifecyclePhaseSchema.parse(initial);
|
|
50
|
+
let phase = initial;
|
|
51
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
52
|
+
return {
|
|
53
|
+
get phase() {
|
|
54
|
+
return phase;
|
|
55
|
+
},
|
|
56
|
+
transition(to) {
|
|
57
|
+
lifecyclePhaseSchema.parse(to);
|
|
58
|
+
if (to === phase) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!canTransition(phase, to)) {
|
|
62
|
+
throw new LifecycleError(phase, to);
|
|
63
|
+
}
|
|
64
|
+
const transitionEvent = { from: phase, to };
|
|
65
|
+
phase = to;
|
|
66
|
+
for (const listener of [...listeners]) {
|
|
67
|
+
listener(transitionEvent);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
subscribe(listener) {
|
|
71
|
+
listeners.add(listener);
|
|
72
|
+
return () => {
|
|
73
|
+
listeners.delete(listener);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var contractSchema = z.object({
|
|
79
|
+
id: z.string().min(1),
|
|
80
|
+
version: z.number().int().positive(),
|
|
81
|
+
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
82
|
+
outputs: z.record(z.string(), z.unknown()).optional(),
|
|
83
|
+
events: z.array(z.string().min(1)).optional(),
|
|
84
|
+
commands: z.array(z.string().min(1)).optional(),
|
|
85
|
+
capabilities: z.array(z.string().min(1)).optional()
|
|
86
|
+
});
|
|
87
|
+
function defineContract(contract) {
|
|
88
|
+
contractSchema.parse(contract);
|
|
89
|
+
return Object.freeze({ ...contract });
|
|
90
|
+
}
|
|
91
|
+
var capabilitySchema = z.object({
|
|
92
|
+
id: z.string().min(1),
|
|
93
|
+
version: z.number().int().positive(),
|
|
94
|
+
provides: z.array(z.string().min(1)).optional(),
|
|
95
|
+
requires: z.array(z.string().min(1)).optional()
|
|
96
|
+
});
|
|
97
|
+
function defineCapability(capability) {
|
|
98
|
+
capabilitySchema.parse(capability);
|
|
99
|
+
return Object.freeze({ ...capability });
|
|
100
|
+
}
|
|
101
|
+
var FrnlFault = class extends Error {
|
|
102
|
+
code;
|
|
103
|
+
recoverable;
|
|
104
|
+
retryable;
|
|
105
|
+
cause;
|
|
106
|
+
constructor(options) {
|
|
107
|
+
super(options.message);
|
|
108
|
+
this.name = "FrnlFault";
|
|
109
|
+
this.code = options.code;
|
|
110
|
+
this.recoverable = options.recoverable ?? false;
|
|
111
|
+
this.retryable = options.retryable ?? false;
|
|
112
|
+
this.cause = options.cause;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function defineEffect(id, run) {
|
|
116
|
+
z.string().min(1).parse(id);
|
|
117
|
+
if (typeof run !== "function") {
|
|
118
|
+
throw new TypeError("Effect run must be a function");
|
|
119
|
+
}
|
|
120
|
+
return Object.freeze({ id, run });
|
|
121
|
+
}
|
|
122
|
+
function defineCommand(id, payload) {
|
|
123
|
+
z.string().min(1).parse(id);
|
|
124
|
+
return Object.freeze({ id, payload });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/runtime/resources.ts
|
|
128
|
+
function defineResource(resource) {
|
|
129
|
+
if (!resource || !resource.id || typeof resource.id !== "string") {
|
|
130
|
+
throw new TypeError("Invalid Frnl resource: missing valid id");
|
|
131
|
+
}
|
|
132
|
+
if (typeof resource.acquire !== "function") {
|
|
133
|
+
throw new TypeError(`Invalid Frnl resource '${resource.id}': acquire must be a function`);
|
|
134
|
+
}
|
|
135
|
+
return Object.freeze({ ...resource });
|
|
136
|
+
}
|
|
137
|
+
function createResourceController(resource, scopeId) {
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const state = {
|
|
140
|
+
status: "idle",
|
|
141
|
+
value: void 0,
|
|
142
|
+
error: void 0,
|
|
143
|
+
inFlightPromise: void 0,
|
|
144
|
+
inFlightReleasePromise: void 0
|
|
145
|
+
};
|
|
146
|
+
const context = Object.freeze({
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
scopeId
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
id: resource.id,
|
|
152
|
+
get status() {
|
|
153
|
+
return state.status;
|
|
154
|
+
},
|
|
155
|
+
get value() {
|
|
156
|
+
return state.value;
|
|
157
|
+
},
|
|
158
|
+
get error() {
|
|
159
|
+
return state.error;
|
|
160
|
+
},
|
|
161
|
+
async load() {
|
|
162
|
+
if (state.status === "ready") {
|
|
163
|
+
return state.value;
|
|
164
|
+
}
|
|
165
|
+
if (state.status === "loading" && state.inFlightPromise) {
|
|
166
|
+
return state.inFlightPromise;
|
|
167
|
+
}
|
|
168
|
+
if (state.status === "released") {
|
|
169
|
+
throw new FrnlFault({
|
|
170
|
+
code: "RESOURCE_RELEASED",
|
|
171
|
+
message: `Resource '${resource.id}' has already been released`
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (controller.signal.aborted) {
|
|
175
|
+
throw new FrnlFault({
|
|
176
|
+
code: "RESOURCE_ABORTED",
|
|
177
|
+
message: `Resource '${resource.id}' acquisition aborted`
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
state.status = "loading";
|
|
181
|
+
state.error = void 0;
|
|
182
|
+
state.inFlightPromise = (async () => {
|
|
183
|
+
try {
|
|
184
|
+
const acquired = await Promise.resolve(resource.acquire(context));
|
|
185
|
+
if (controller.signal.aborted || state.status === "released") {
|
|
186
|
+
if (resource.release && acquired !== void 0) {
|
|
187
|
+
try {
|
|
188
|
+
await Promise.resolve(resource.release(acquired, context));
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw new FrnlFault({
|
|
193
|
+
code: "RESOURCE_ABORTED",
|
|
194
|
+
message: `Resource '${resource.id}' acquired after abort or release`
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
state.value = acquired;
|
|
198
|
+
state.status = "ready";
|
|
199
|
+
return acquired;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (state.status !== "released") {
|
|
202
|
+
state.status = "error";
|
|
203
|
+
state.error = err instanceof FrnlFault ? err : new FrnlFault({
|
|
204
|
+
code: "RESOURCE_ACQUIRE_FAILED",
|
|
205
|
+
message: err instanceof Error ? err.message : String(err),
|
|
206
|
+
retryable: true,
|
|
207
|
+
cause: err
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
throw state.error ?? err;
|
|
211
|
+
} finally {
|
|
212
|
+
state.inFlightPromise = void 0;
|
|
213
|
+
}
|
|
214
|
+
})();
|
|
215
|
+
return state.inFlightPromise;
|
|
216
|
+
},
|
|
217
|
+
async release() {
|
|
218
|
+
if (state.status === "released") {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (state.inFlightReleasePromise) {
|
|
222
|
+
return state.inFlightReleasePromise;
|
|
223
|
+
}
|
|
224
|
+
state.inFlightReleasePromise = (async () => {
|
|
225
|
+
state.status = "released";
|
|
226
|
+
controller.abort();
|
|
227
|
+
if (state.inFlightPromise) {
|
|
228
|
+
try {
|
|
229
|
+
await state.inFlightPromise;
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const previousValue = state.value;
|
|
234
|
+
state.value = void 0;
|
|
235
|
+
if (previousValue !== void 0 && resource.release) {
|
|
236
|
+
try {
|
|
237
|
+
await Promise.resolve(resource.release(previousValue, context));
|
|
238
|
+
} catch (releaseErr) {
|
|
239
|
+
state.error = releaseErr instanceof FrnlFault ? releaseErr : new FrnlFault({
|
|
240
|
+
code: "RESOURCE_RELEASE_FAILED",
|
|
241
|
+
message: releaseErr instanceof Error ? releaseErr.message : String(releaseErr),
|
|
242
|
+
cause: releaseErr
|
|
243
|
+
});
|
|
244
|
+
throw state.error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
return state.inFlightReleasePromise;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function createScope(id, parent) {
|
|
253
|
+
if (parent && (parent.signal.aborted || parent.isDisposed)) {
|
|
254
|
+
throw new FrnlFault({
|
|
255
|
+
code: "SCOPE_DISPOSED",
|
|
256
|
+
message: `Cannot create child scope from disposed parent '${parent.id}'`
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const resources = /* @__PURE__ */ new Set();
|
|
261
|
+
const childScopes = /* @__PURE__ */ new Set();
|
|
262
|
+
let isDisposed = false;
|
|
263
|
+
let inFlightDisposePromise;
|
|
264
|
+
const scope = {
|
|
265
|
+
id,
|
|
266
|
+
parent,
|
|
267
|
+
signal: controller.signal,
|
|
268
|
+
get isDisposed() {
|
|
269
|
+
return isDisposed;
|
|
270
|
+
},
|
|
271
|
+
_registerChild(child) {
|
|
272
|
+
if (isDisposed) {
|
|
273
|
+
void child.dispose();
|
|
274
|
+
} else {
|
|
275
|
+
childScopes.add(child);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
_removeChild(child) {
|
|
279
|
+
childScopes.delete(child);
|
|
280
|
+
},
|
|
281
|
+
add(resource) {
|
|
282
|
+
if (controller.signal.aborted || isDisposed) {
|
|
283
|
+
throw new FrnlFault({
|
|
284
|
+
code: "SCOPE_DISPOSED",
|
|
285
|
+
message: `Scope '${id}' has been disposed`
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const resCtrl = createResourceController(resource, id);
|
|
289
|
+
resources.add(resCtrl);
|
|
290
|
+
return resCtrl;
|
|
291
|
+
},
|
|
292
|
+
createChildScope(childId) {
|
|
293
|
+
if (controller.signal.aborted || isDisposed) {
|
|
294
|
+
throw new FrnlFault({
|
|
295
|
+
code: "SCOPE_DISPOSED",
|
|
296
|
+
message: `Cannot create child scope from disposed parent '${id}'`
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return createScope(childId, scope);
|
|
300
|
+
},
|
|
301
|
+
async dispose() {
|
|
302
|
+
if (isDisposed) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (inFlightDisposePromise) {
|
|
306
|
+
return inFlightDisposePromise;
|
|
307
|
+
}
|
|
308
|
+
inFlightDisposePromise = (async () => {
|
|
309
|
+
isDisposed = true;
|
|
310
|
+
if (!controller.signal.aborted) {
|
|
311
|
+
controller.abort();
|
|
312
|
+
}
|
|
313
|
+
if (parent?._removeChild) {
|
|
314
|
+
parent._removeChild(scope);
|
|
315
|
+
}
|
|
316
|
+
const childPromises = [...childScopes].map((child) => child.dispose());
|
|
317
|
+
const resourcePromises = [...resources].map((res) => res.release());
|
|
318
|
+
childScopes.clear();
|
|
319
|
+
resources.clear();
|
|
320
|
+
await Promise.allSettled([...childPromises, ...resourcePromises]);
|
|
321
|
+
})();
|
|
322
|
+
return inFlightDisposePromise;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
if (parent?._registerChild) {
|
|
326
|
+
parent._registerChild(scope);
|
|
327
|
+
}
|
|
328
|
+
if (parent) {
|
|
329
|
+
parent.signal.addEventListener("abort", () => void scope.dispose(), { once: true });
|
|
330
|
+
}
|
|
331
|
+
return scope;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/runtime/dependencies.ts
|
|
335
|
+
var DependencyGraph = class _DependencyGraph {
|
|
336
|
+
nodes = /* @__PURE__ */ new Map();
|
|
337
|
+
providers = /* @__PURE__ */ new Map();
|
|
338
|
+
// capability -> set of nodeIds
|
|
339
|
+
register(node) {
|
|
340
|
+
if (!node || !node.id || typeof node.id !== "string") {
|
|
341
|
+
throw new TypeError("Invalid dependency node: id must be a non-empty string");
|
|
342
|
+
}
|
|
343
|
+
if (this.nodes.has(node.id)) {
|
|
344
|
+
throw new Error(`Duplicate dependency node: ${node.id}`);
|
|
345
|
+
}
|
|
346
|
+
const frozen = Object.freeze({ ...node });
|
|
347
|
+
this.nodes.set(node.id, frozen);
|
|
348
|
+
for (const prov of node.provides ?? []) {
|
|
349
|
+
if (!this.providers.has(prov)) {
|
|
350
|
+
this.providers.set(prov, /* @__PURE__ */ new Set());
|
|
351
|
+
}
|
|
352
|
+
this.providers.get(prov).add(node.id);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
registerCapability(capability) {
|
|
356
|
+
this.register({
|
|
357
|
+
id: capability.id,
|
|
358
|
+
version: capability.version,
|
|
359
|
+
requires: capability.requires,
|
|
360
|
+
provides: capability.provides
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
get(id) {
|
|
364
|
+
return this.nodes.get(id);
|
|
365
|
+
}
|
|
366
|
+
list() {
|
|
367
|
+
return [...this.nodes.values()];
|
|
368
|
+
}
|
|
369
|
+
resolveDependencyNodeId(dependencyName) {
|
|
370
|
+
if (this.nodes.has(dependencyName)) {
|
|
371
|
+
return dependencyName;
|
|
372
|
+
}
|
|
373
|
+
const provSet = this.providers.get(dependencyName);
|
|
374
|
+
if (provSet && provSet.size > 0) {
|
|
375
|
+
return [...provSet][0];
|
|
376
|
+
}
|
|
377
|
+
return void 0;
|
|
378
|
+
}
|
|
379
|
+
validate() {
|
|
380
|
+
const errors = [];
|
|
381
|
+
for (const node of this.nodes.values()) {
|
|
382
|
+
for (const req of node.requires ?? []) {
|
|
383
|
+
const resolvedId = this.resolveDependencyNodeId(req);
|
|
384
|
+
if (!resolvedId) {
|
|
385
|
+
errors.push(`${node.id} requires missing dependency ${req}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return errors;
|
|
390
|
+
}
|
|
391
|
+
topologicalOrder() {
|
|
392
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
393
|
+
const visited = /* @__PURE__ */ new Set();
|
|
394
|
+
const result = [];
|
|
395
|
+
const visit = (id, path = []) => {
|
|
396
|
+
if (visiting.has(id)) {
|
|
397
|
+
const cycle = [...path, id].join(" -> ");
|
|
398
|
+
throw new Error(`Dependency cycle detected at ${id}: ${cycle}`);
|
|
399
|
+
}
|
|
400
|
+
if (visited.has(id)) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
visiting.add(id);
|
|
404
|
+
const node = this.nodes.get(id);
|
|
405
|
+
for (const req of node?.requires ?? []) {
|
|
406
|
+
const resolvedId = this.resolveDependencyNodeId(req);
|
|
407
|
+
if (resolvedId && this.nodes.has(resolvedId)) {
|
|
408
|
+
visit(resolvedId, [...path, id]);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
visiting.delete(id);
|
|
412
|
+
visited.add(id);
|
|
413
|
+
result.push(id);
|
|
414
|
+
};
|
|
415
|
+
for (const id of this.nodes.keys()) {
|
|
416
|
+
visit(id);
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
clear() {
|
|
421
|
+
this.nodes.clear();
|
|
422
|
+
this.providers.clear();
|
|
423
|
+
}
|
|
424
|
+
static fromCapabilities(capabilities) {
|
|
425
|
+
const graph = new _DependencyGraph();
|
|
426
|
+
for (const cap of capabilities) {
|
|
427
|
+
graph.registerCapability(cap);
|
|
428
|
+
}
|
|
429
|
+
return graph;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// src/runtime/registry.ts
|
|
434
|
+
var FrnlRegistry = class {
|
|
435
|
+
entries = /* @__PURE__ */ new Map();
|
|
436
|
+
register(entry) {
|
|
437
|
+
if (!entry || !entry.id || typeof entry.id !== "string") {
|
|
438
|
+
throw new TypeError("Invalid registry entry: id must be a non-empty string");
|
|
439
|
+
}
|
|
440
|
+
if (!Number.isInteger(entry.version) || entry.version < 1) {
|
|
441
|
+
throw new TypeError(
|
|
442
|
+
`Invalid registry entry '${entry.id}': version must be a positive integer, received ${entry.version}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const current = this.entries.get(entry.id);
|
|
446
|
+
if (current && current.version > entry.version) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Cannot register older version (${entry.version}) of '${entry.id}'. Active version is ${current.version}.`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
this.entries.set(entry.id, Object.freeze({ ...entry }));
|
|
452
|
+
}
|
|
453
|
+
has(id) {
|
|
454
|
+
return this.entries.has(id);
|
|
455
|
+
}
|
|
456
|
+
get(id) {
|
|
457
|
+
return this.entries.get(id);
|
|
458
|
+
}
|
|
459
|
+
resolve(id) {
|
|
460
|
+
const entry = this.entries.get(id);
|
|
461
|
+
if (!entry) {
|
|
462
|
+
throw new Error(`Frnl registration not found: ${id}`);
|
|
463
|
+
}
|
|
464
|
+
return entry.value;
|
|
465
|
+
}
|
|
466
|
+
list() {
|
|
467
|
+
return [...this.entries.values()];
|
|
468
|
+
}
|
|
469
|
+
requireCapabilities(id, available) {
|
|
470
|
+
const entry = this.entries.get(id);
|
|
471
|
+
if (!entry) {
|
|
472
|
+
throw new Error(`Frnl registration not found: ${id}`);
|
|
473
|
+
}
|
|
474
|
+
const availableSet = new Set(available);
|
|
475
|
+
const required = [];
|
|
476
|
+
if (entry.contract?.capabilities) {
|
|
477
|
+
required.push(...entry.contract.capabilities);
|
|
478
|
+
}
|
|
479
|
+
if (entry.capabilities) {
|
|
480
|
+
for (const cap of entry.capabilities) {
|
|
481
|
+
if (cap.requires) {
|
|
482
|
+
required.push(...cap.requires);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
for (const cap of required) {
|
|
487
|
+
if (!availableSet.has(cap)) {
|
|
488
|
+
throw new Error(`Missing capability '${cap}' for registration '${id}'`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
toDependencyGraph() {
|
|
493
|
+
const graph = new DependencyGraph();
|
|
494
|
+
for (const entry of this.entries.values()) {
|
|
495
|
+
const requires = [];
|
|
496
|
+
if (entry.contract?.capabilities) {
|
|
497
|
+
requires.push(...entry.contract.capabilities);
|
|
498
|
+
}
|
|
499
|
+
const provides = [];
|
|
500
|
+
if (entry.capabilities) {
|
|
501
|
+
for (const cap of entry.capabilities) {
|
|
502
|
+
if (cap.provides) {
|
|
503
|
+
provides.push(...cap.provides);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
graph.register({
|
|
508
|
+
id: entry.id,
|
|
509
|
+
version: entry.version,
|
|
510
|
+
requires,
|
|
511
|
+
provides
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return graph;
|
|
515
|
+
}
|
|
516
|
+
clear() {
|
|
517
|
+
this.entries.clear();
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// src/runtime/routing.ts
|
|
522
|
+
function escapeRegex(str) {
|
|
523
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
524
|
+
}
|
|
525
|
+
function normalizePattern(pattern) {
|
|
526
|
+
if (!pattern || pattern.trim() === "") {
|
|
527
|
+
return "/";
|
|
528
|
+
}
|
|
529
|
+
const trimmed = pattern.trim();
|
|
530
|
+
const withLeading = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
531
|
+
return withLeading.length > 1 && withLeading.endsWith("/") ? withLeading.slice(0, -1) : withLeading;
|
|
532
|
+
}
|
|
533
|
+
function compileRoute(pattern) {
|
|
534
|
+
const norm = normalizePattern(pattern);
|
|
535
|
+
const paramNames = [];
|
|
536
|
+
let wildcardName;
|
|
537
|
+
const parts = norm.split("/");
|
|
538
|
+
const regexParts = parts.map((part) => {
|
|
539
|
+
if (part.startsWith(":")) {
|
|
540
|
+
const name = part.slice(1);
|
|
541
|
+
paramNames.push(name);
|
|
542
|
+
return "([^/]+)";
|
|
543
|
+
}
|
|
544
|
+
if (part === "*" || part.startsWith("*")) {
|
|
545
|
+
const name = part.length > 1 ? part.slice(1) : "wildcard";
|
|
546
|
+
wildcardName = name;
|
|
547
|
+
paramNames.push(name);
|
|
548
|
+
return "(.*)";
|
|
549
|
+
}
|
|
550
|
+
return escapeRegex(part);
|
|
551
|
+
});
|
|
552
|
+
const source = regexParts.join("/");
|
|
553
|
+
return {
|
|
554
|
+
regex: new RegExp(`^${source}/?$`),
|
|
555
|
+
paramNames,
|
|
556
|
+
wildcardName
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function matchRoute(route, url) {
|
|
560
|
+
const parsed = new URL(url, "http://frnl.local");
|
|
561
|
+
const compiled = compileRoute(route.pattern);
|
|
562
|
+
const match = compiled.regex.exec(parsed.pathname);
|
|
563
|
+
if (!match) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const params = {};
|
|
567
|
+
compiled.paramNames.forEach((name, index) => {
|
|
568
|
+
const rawVal = match[index + 1] ?? "";
|
|
569
|
+
params[name] = decodeURIComponent(rawVal);
|
|
570
|
+
});
|
|
571
|
+
const query = {};
|
|
572
|
+
parsed.searchParams.forEach((value, key) => {
|
|
573
|
+
query[key] = value;
|
|
574
|
+
});
|
|
575
|
+
return {
|
|
576
|
+
id: route.id,
|
|
577
|
+
params: Object.freeze(params),
|
|
578
|
+
query: Object.freeze(query),
|
|
579
|
+
path: parsed.pathname
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function buildRouteUrl(route, params = {}, query = {}, options = {}) {
|
|
583
|
+
const norm = normalizePattern(route.pattern);
|
|
584
|
+
const parts = norm.split("/");
|
|
585
|
+
const consumedParams = /* @__PURE__ */ new Set();
|
|
586
|
+
const builtParts = parts.map((part) => {
|
|
587
|
+
if (part.startsWith(":")) {
|
|
588
|
+
const name = part.slice(1);
|
|
589
|
+
consumedParams.add(name);
|
|
590
|
+
const value = params[name];
|
|
591
|
+
if (value === void 0 || value === null || value === "") {
|
|
592
|
+
throw new Error(`Missing route parameter: ${name}`);
|
|
593
|
+
}
|
|
594
|
+
return encodeURIComponent(value);
|
|
595
|
+
}
|
|
596
|
+
if (part === "*" || part.startsWith("*")) {
|
|
597
|
+
const name = part.length > 1 ? part.slice(1) : "wildcard";
|
|
598
|
+
consumedParams.add(name);
|
|
599
|
+
consumedParams.add("wildcard");
|
|
600
|
+
const value = params[name] ?? params.wildcard ?? "";
|
|
601
|
+
return value.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
602
|
+
}
|
|
603
|
+
return part;
|
|
604
|
+
});
|
|
605
|
+
const path = builtParts.join("/") || "/";
|
|
606
|
+
const unconsumed = {};
|
|
607
|
+
for (const [key, value] of Object.entries(params)) {
|
|
608
|
+
if (!consumedParams.has(key)) {
|
|
609
|
+
if (options.strict) {
|
|
610
|
+
throw new Error(`Unknown route parameter: ${key}`);
|
|
611
|
+
}
|
|
612
|
+
unconsumed[key] = value;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const finalQuery = {
|
|
616
|
+
...options.appendUnmatchedParamsToQuery ? unconsumed : {},
|
|
617
|
+
...query
|
|
618
|
+
};
|
|
619
|
+
const searchParams = new URLSearchParams();
|
|
620
|
+
for (const [key, value] of Object.entries(finalQuery)) {
|
|
621
|
+
if (value !== void 0 && value !== null) {
|
|
622
|
+
searchParams.set(key, value);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const queryString = searchParams.toString();
|
|
626
|
+
return queryString ? `${path}?${queryString}` : path;
|
|
627
|
+
}
|
|
628
|
+
var FrnlRouter = class {
|
|
629
|
+
routeMap = /* @__PURE__ */ new Map();
|
|
630
|
+
patternMap = /* @__PURE__ */ new Map();
|
|
631
|
+
// pattern -> routeId
|
|
632
|
+
constructor(routes = []) {
|
|
633
|
+
for (const route of routes) {
|
|
634
|
+
this.addRoute(route);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
addRoute(route) {
|
|
638
|
+
if (!route || !route.id || typeof route.id !== "string") {
|
|
639
|
+
throw new TypeError("Invalid route: id must be a non-empty string");
|
|
640
|
+
}
|
|
641
|
+
if (this.routeMap.has(route.id)) {
|
|
642
|
+
throw new Error(`Duplicate route id: ${route.id}`);
|
|
643
|
+
}
|
|
644
|
+
const norm = normalizePattern(route.pattern);
|
|
645
|
+
const existingRouteId = this.patternMap.get(norm);
|
|
646
|
+
if (existingRouteId) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Conflicting route pattern '${route.pattern}' already registered by route '${existingRouteId}'`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
this.routeMap.set(route.id, Object.freeze({ ...route }));
|
|
652
|
+
this.patternMap.set(norm, route.id);
|
|
653
|
+
}
|
|
654
|
+
getRoute(id) {
|
|
655
|
+
return this.routeMap.get(id);
|
|
656
|
+
}
|
|
657
|
+
match(url) {
|
|
658
|
+
for (const route of this.routeMap.values()) {
|
|
659
|
+
const match = matchRoute(route, url);
|
|
660
|
+
if (match) {
|
|
661
|
+
return match;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
async resolve(url) {
|
|
667
|
+
const match = this.match(url);
|
|
668
|
+
if (!match) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
const route = this.routeMap.get(match.id);
|
|
672
|
+
const data = route?.load ? await route.load() : void 0;
|
|
673
|
+
return { match, data };
|
|
674
|
+
}
|
|
675
|
+
list() {
|
|
676
|
+
return [...this.routeMap.values()];
|
|
677
|
+
}
|
|
678
|
+
clear() {
|
|
679
|
+
this.routeMap.clear();
|
|
680
|
+
this.patternMap.clear();
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// src/runtime/accessibility.ts
|
|
685
|
+
function validateAccessibility(contract, attributes) {
|
|
686
|
+
const issues = [];
|
|
687
|
+
if (contract.nameRequired) {
|
|
688
|
+
const hasLabel = typeof attributes["aria-label"] === "string" && attributes["aria-label"].trim().length > 0;
|
|
689
|
+
const hasLabelledBy = typeof attributes["aria-labelledby"] === "string" && attributes["aria-labelledby"].trim().length > 0;
|
|
690
|
+
if (!hasLabel && !hasLabelledBy) {
|
|
691
|
+
issues.push({
|
|
692
|
+
code: "ACCESSIBLE_NAME_REQUIRED",
|
|
693
|
+
message: "An accessible name is required via aria-label or aria-labelledby."
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
for (const attr of contract.requiredAttributes ?? []) {
|
|
698
|
+
if (!(attr in attributes) || attributes[attr] === void 0 || attributes[attr] === null || attributes[attr] === "") {
|
|
699
|
+
issues.push({
|
|
700
|
+
code: "REQUIRED_ATTRIBUTE_MISSING",
|
|
701
|
+
message: `Missing required attribute: ${attr}`
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (contract.role && attributes.role !== contract.role) {
|
|
706
|
+
issues.push({
|
|
707
|
+
code: "ROLE_MISMATCH",
|
|
708
|
+
message: `Expected role '${contract.role}', received '${String(attributes.role)}'`
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (contract.focusable) {
|
|
712
|
+
if (attributes.tabIndex === -1 || attributes["aria-hidden"] === true) {
|
|
713
|
+
issues.push({
|
|
714
|
+
code: "FOCUS_RESTRICTED",
|
|
715
|
+
message: "Element is marked as focusable but focus is suppressed via tabIndex or aria-hidden."
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return issues;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/runtime/store.ts
|
|
723
|
+
import { createStore } from "zustand/vanilla";
|
|
724
|
+
import { z as z3 } from "zod";
|
|
725
|
+
|
|
726
|
+
// src/runtime/validation.ts
|
|
727
|
+
import { z as z2 } from "zod";
|
|
728
|
+
var entityIdSchema = z2.string().min(1);
|
|
729
|
+
var stateNameSchema = z2.string().min(1);
|
|
730
|
+
var propertyKeySchema = z2.string().min(1);
|
|
731
|
+
var propertyValueSchema = z2.unknown();
|
|
732
|
+
var propertiesSchema = z2.record(z2.string(), propertyValueSchema);
|
|
733
|
+
var frnlThemeStateSchema = z2.object({
|
|
734
|
+
visualParameters: propertiesSchema.optional()
|
|
735
|
+
}).strict();
|
|
736
|
+
var frnlThemeComponentSchema = z2.object({
|
|
737
|
+
states: z2.record(z2.string().min(1), frnlThemeStateSchema).optional()
|
|
738
|
+
}).strict();
|
|
739
|
+
var frnlThemeDefinitionSchema = z2.record(
|
|
740
|
+
z2.string().min(1),
|
|
741
|
+
frnlThemeComponentSchema
|
|
742
|
+
);
|
|
743
|
+
var entityStateSchema = z2.object({
|
|
744
|
+
state: stateNameSchema,
|
|
745
|
+
properties: propertiesSchema
|
|
746
|
+
});
|
|
747
|
+
var entityStateChangedPayloadSchema = z2.object({
|
|
748
|
+
entityId: entityIdSchema,
|
|
749
|
+
previousState: stateNameSchema,
|
|
750
|
+
state: stateNameSchema
|
|
751
|
+
});
|
|
752
|
+
var entityPropertyChangedPayloadSchema = z2.object({
|
|
753
|
+
entityId: entityIdSchema,
|
|
754
|
+
key: propertyKeySchema,
|
|
755
|
+
value: propertyValueSchema
|
|
756
|
+
});
|
|
757
|
+
var flowTriggeredPayloadSchema = z2.object({
|
|
758
|
+
flowId: z2.string().min(1),
|
|
759
|
+
componentId: entityIdSchema,
|
|
760
|
+
trigger: z2.string().min(1),
|
|
761
|
+
payload: propertyValueSchema.optional()
|
|
762
|
+
});
|
|
763
|
+
var frnlEventSchema = z2.discriminatedUnion("type", [
|
|
764
|
+
z2.object({ type: z2.literal("entity.stateChanged"), payload: entityStateChangedPayloadSchema }),
|
|
765
|
+
z2.object({ type: z2.literal("entity.propertyChanged"), payload: entityPropertyChangedPayloadSchema }),
|
|
766
|
+
z2.object({ type: z2.literal("flow.triggered"), payload: flowTriggeredPayloadSchema })
|
|
767
|
+
]);
|
|
768
|
+
function parseFrnlInput(schema, input) {
|
|
769
|
+
return schema.parse(input);
|
|
770
|
+
}
|
|
771
|
+
function validateFrnlInput(schema, input) {
|
|
772
|
+
return schema.safeParse(input).success;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/runtime/store.ts
|
|
776
|
+
function createFrnlStore(initial) {
|
|
777
|
+
const initialEntities = z3.record(z3.string(), entityStateSchema).parse(initial?.entities ?? {});
|
|
778
|
+
return createStore((set, get) => ({
|
|
779
|
+
entities: initialEntities,
|
|
780
|
+
events: [],
|
|
781
|
+
componentDefinitions: {},
|
|
782
|
+
eventHandlers: {},
|
|
783
|
+
componentParents: {},
|
|
784
|
+
visualOverrides: {},
|
|
785
|
+
themes: {},
|
|
786
|
+
componentThemes: {},
|
|
787
|
+
libraries: {},
|
|
788
|
+
registerComponentDefinition: (componentId, definition) => {
|
|
789
|
+
const id = entityIdSchema.parse(componentId);
|
|
790
|
+
for (const theme of Object.values(get().themes)) {
|
|
791
|
+
validateFrnlThemeAgainstDefinition(theme, definition);
|
|
792
|
+
}
|
|
793
|
+
set((current) => ({ componentDefinitions: { ...current.componentDefinitions, [id]: definition } }));
|
|
794
|
+
},
|
|
795
|
+
registerTheme: (name, theme) => {
|
|
796
|
+
const themeName = z3.string().min(1).parse(name);
|
|
797
|
+
const validatedTheme = frnlThemeDefinitionSchema.parse(theme);
|
|
798
|
+
for (const definition of Object.values(get().componentDefinitions)) {
|
|
799
|
+
validateFrnlThemeAgainstDefinition(validatedTheme, definition);
|
|
800
|
+
}
|
|
801
|
+
set((current) => ({
|
|
802
|
+
themes: { ...current.themes, [themeName]: validatedTheme }
|
|
803
|
+
}));
|
|
804
|
+
},
|
|
805
|
+
applyTheme: (componentId, themeName) => {
|
|
806
|
+
const id = entityIdSchema.parse(componentId);
|
|
807
|
+
const name = z3.string().min(1).parse(themeName);
|
|
808
|
+
set((current) => {
|
|
809
|
+
if (!current.entities[id]) {
|
|
810
|
+
throw new Error(`Unknown Frnl component: ${id}`);
|
|
811
|
+
}
|
|
812
|
+
if (!current.themes[name]) {
|
|
813
|
+
throw new Error(`Unknown Frnl theme: ${name}`);
|
|
814
|
+
}
|
|
815
|
+
const visualOverrides = { ...current.visualOverrides };
|
|
816
|
+
delete visualOverrides[id];
|
|
817
|
+
return {
|
|
818
|
+
componentThemes: { ...current.componentThemes, [id]: name },
|
|
819
|
+
visualOverrides
|
|
820
|
+
};
|
|
821
|
+
});
|
|
822
|
+
},
|
|
823
|
+
registerEventHandlers: (componentId, handlers) => {
|
|
824
|
+
const id = entityIdSchema.parse(componentId);
|
|
825
|
+
set((current) => ({ eventHandlers: { ...current.eventHandlers, [id]: handlers } }));
|
|
826
|
+
},
|
|
827
|
+
unregisterEventHandlers: (componentId) => {
|
|
828
|
+
const id = entityIdSchema.parse(componentId);
|
|
829
|
+
set((current) => {
|
|
830
|
+
const next = { ...current.eventHandlers };
|
|
831
|
+
delete next[id];
|
|
832
|
+
return { eventHandlers: next };
|
|
833
|
+
});
|
|
834
|
+
},
|
|
835
|
+
unregisterEntity: (entityId) => {
|
|
836
|
+
const id = entityIdSchema.parse(entityId);
|
|
837
|
+
set((current) => {
|
|
838
|
+
const entities = { ...current.entities };
|
|
839
|
+
const componentDefinitions = { ...current.componentDefinitions };
|
|
840
|
+
const eventHandlers = { ...current.eventHandlers };
|
|
841
|
+
const componentParents = { ...current.componentParents };
|
|
842
|
+
const visualOverrides = { ...current.visualOverrides };
|
|
843
|
+
const componentThemes = { ...current.componentThemes };
|
|
844
|
+
delete entities[id];
|
|
845
|
+
delete componentDefinitions[id];
|
|
846
|
+
delete eventHandlers[id];
|
|
847
|
+
delete componentParents[id];
|
|
848
|
+
delete visualOverrides[id];
|
|
849
|
+
delete componentThemes[id];
|
|
850
|
+
return { entities, componentDefinitions, eventHandlers, componentParents, visualOverrides, componentThemes };
|
|
851
|
+
});
|
|
852
|
+
},
|
|
853
|
+
setVisualOverride: (componentId, parameter, value) => {
|
|
854
|
+
const id = entityIdSchema.parse(componentId);
|
|
855
|
+
const key = propertyKeySchema.parse(parameter);
|
|
856
|
+
set((current) => ({
|
|
857
|
+
visualOverrides: {
|
|
858
|
+
...current.visualOverrides,
|
|
859
|
+
[id]: { ...current.visualOverrides[id] ?? {}, [key]: value }
|
|
860
|
+
}
|
|
861
|
+
}));
|
|
862
|
+
},
|
|
863
|
+
clearVisualOverrides: (componentId) => {
|
|
864
|
+
const id = entityIdSchema.parse(componentId);
|
|
865
|
+
set((current) => {
|
|
866
|
+
const visualOverrides = { ...current.visualOverrides };
|
|
867
|
+
delete visualOverrides[id];
|
|
868
|
+
return { visualOverrides };
|
|
869
|
+
});
|
|
870
|
+
},
|
|
871
|
+
registerLibrary: (address, library) => {
|
|
872
|
+
const key = z3.string().min(1).parse(address);
|
|
873
|
+
set((current) => ({ libraries: { ...current.libraries, [key]: library } }));
|
|
874
|
+
},
|
|
875
|
+
unregisterLibrary: (address) => {
|
|
876
|
+
const key = z3.string().min(1).parse(address);
|
|
877
|
+
set((current) => {
|
|
878
|
+
const libraries = { ...current.libraries };
|
|
879
|
+
delete libraries[key];
|
|
880
|
+
return { libraries };
|
|
881
|
+
});
|
|
882
|
+
},
|
|
883
|
+
registerComponentParent: (componentId, parentId) => {
|
|
884
|
+
const id = entityIdSchema.parse(componentId);
|
|
885
|
+
const parent = parentId === void 0 ? void 0 : entityIdSchema.parse(parentId);
|
|
886
|
+
if (parent === id) throw new Error(`Frnl component cannot be its own parent: ${id}`);
|
|
887
|
+
set((current) => ({
|
|
888
|
+
componentParents: { ...current.componentParents, [id]: parent }
|
|
889
|
+
}));
|
|
890
|
+
},
|
|
891
|
+
emit: (event) => {
|
|
892
|
+
const validated = frnlEventSchema.parse(event);
|
|
893
|
+
set((current) => ({ events: [...current.events, validated] }));
|
|
894
|
+
},
|
|
895
|
+
/**
|
|
896
|
+
* registerEntity performs an idempotent upsert of declarative metadata.
|
|
897
|
+
*
|
|
898
|
+
* Semantic Authority Model:
|
|
899
|
+
* - Declarative Metadata (authoritative from registration / props):
|
|
900
|
+
* `properties`.
|
|
901
|
+
* - Runtime State (authoritative from runtime / interactions):
|
|
902
|
+
* `state` (active state is preserved across re-registrations),
|
|
903
|
+
* `events` (event history is preserved).
|
|
904
|
+
*/
|
|
905
|
+
registerEntity: (entityId, entity) => {
|
|
906
|
+
const id = entityIdSchema.parse(entityId);
|
|
907
|
+
const validated = entityStateSchema.parse(entity);
|
|
908
|
+
set((current) => {
|
|
909
|
+
const existing = current.entities[id];
|
|
910
|
+
const nextEntity = existing ? {
|
|
911
|
+
// Runtime state is preserved if already set
|
|
912
|
+
state: existing.state,
|
|
913
|
+
// Declarative metadata is merged / updated
|
|
914
|
+
properties: { ...existing.properties, ...validated.properties }
|
|
915
|
+
} : validated;
|
|
916
|
+
return {
|
|
917
|
+
entities: { ...current.entities, [id]: nextEntity }
|
|
918
|
+
};
|
|
919
|
+
});
|
|
920
|
+
},
|
|
921
|
+
setEntityState: (entityId, state) => {
|
|
922
|
+
const id = entityIdSchema.parse(entityId);
|
|
923
|
+
const nextState = stateNameSchema.parse(state);
|
|
924
|
+
set((current) => {
|
|
925
|
+
const entity = current.entities[id];
|
|
926
|
+
if (!entity) return current;
|
|
927
|
+
if (entity.state === nextState) return current;
|
|
928
|
+
const previousState = entity.state;
|
|
929
|
+
const entityPayload = entityStateChangedPayloadSchema.parse({
|
|
930
|
+
entityId: id,
|
|
931
|
+
previousState,
|
|
932
|
+
state: nextState
|
|
933
|
+
});
|
|
934
|
+
const newEvents = [
|
|
935
|
+
{ type: "entity.stateChanged", payload: entityPayload }
|
|
936
|
+
];
|
|
937
|
+
const visualOverrides = { ...current.visualOverrides };
|
|
938
|
+
delete visualOverrides[id];
|
|
939
|
+
return {
|
|
940
|
+
entities: { ...current.entities, [id]: { ...entity, state: nextState } },
|
|
941
|
+
visualOverrides,
|
|
942
|
+
events: [...current.events, ...newEvents]
|
|
943
|
+
};
|
|
944
|
+
});
|
|
945
|
+
},
|
|
946
|
+
setEntityProperty: (entityId, key, value) => {
|
|
947
|
+
const id = entityIdSchema.parse(entityId);
|
|
948
|
+
const property = propertyKeySchema.parse(key);
|
|
949
|
+
set((current) => {
|
|
950
|
+
const entity = current.entities[id];
|
|
951
|
+
if (!entity) return current;
|
|
952
|
+
const payload = entityPropertyChangedPayloadSchema.parse({
|
|
953
|
+
entityId: id,
|
|
954
|
+
key: property,
|
|
955
|
+
value
|
|
956
|
+
});
|
|
957
|
+
return {
|
|
958
|
+
entities: {
|
|
959
|
+
...current.entities,
|
|
960
|
+
[id]: { ...entity, properties: { ...entity.properties, [property]: value } }
|
|
961
|
+
},
|
|
962
|
+
events: [...current.events, { type: "entity.propertyChanged", payload }]
|
|
963
|
+
};
|
|
964
|
+
});
|
|
965
|
+
},
|
|
966
|
+
setEntityProperties: (entityId, properties) => {
|
|
967
|
+
const id = entityIdSchema.parse(entityId);
|
|
968
|
+
const validated = propertiesSchema.parse(properties);
|
|
969
|
+
set((current) => {
|
|
970
|
+
const entity = current.entities[id];
|
|
971
|
+
if (!entity) return current;
|
|
972
|
+
const newEvents = [];
|
|
973
|
+
for (const [key, value] of Object.entries(validated)) {
|
|
974
|
+
if (entity.properties[key] !== value) {
|
|
975
|
+
newEvents.push({
|
|
976
|
+
type: "entity.propertyChanged",
|
|
977
|
+
payload: entityPropertyChangedPayloadSchema.parse({
|
|
978
|
+
entityId: id,
|
|
979
|
+
key,
|
|
980
|
+
value
|
|
981
|
+
})
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
entities: {
|
|
987
|
+
...current.entities,
|
|
988
|
+
[id]: {
|
|
989
|
+
...entity,
|
|
990
|
+
properties: { ...entity.properties, ...validated }
|
|
991
|
+
}
|
|
992
|
+
},
|
|
993
|
+
events: [...current.events, ...newEvents]
|
|
994
|
+
};
|
|
995
|
+
});
|
|
996
|
+
},
|
|
997
|
+
triggerFlow: (flowId, componentId, trigger, payload) => {
|
|
998
|
+
const fId = z3.string().min(1).parse(flowId);
|
|
999
|
+
const cId = entityIdSchema.parse(componentId);
|
|
1000
|
+
const trig = z3.string().min(1).parse(trigger);
|
|
1001
|
+
const eventPayload = flowTriggeredPayloadSchema.parse({
|
|
1002
|
+
flowId: fId,
|
|
1003
|
+
componentId: cId,
|
|
1004
|
+
trigger: trig,
|
|
1005
|
+
payload
|
|
1006
|
+
});
|
|
1007
|
+
set((current) => ({
|
|
1008
|
+
events: [
|
|
1009
|
+
...current.events,
|
|
1010
|
+
{ type: "flow.triggered", payload: eventPayload }
|
|
1011
|
+
]
|
|
1012
|
+
}));
|
|
1013
|
+
}
|
|
1014
|
+
}));
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/runtime/facade.ts
|
|
1018
|
+
var FrnlFacade = class {
|
|
1019
|
+
constructor(store) {
|
|
1020
|
+
this.store = store;
|
|
1021
|
+
}
|
|
1022
|
+
store;
|
|
1023
|
+
component(componentId) {
|
|
1024
|
+
return getFrnlComponentHandle(this.store, componentId);
|
|
1025
|
+
}
|
|
1026
|
+
event(componentId, eventName) {
|
|
1027
|
+
return getFrnlEvent(this.store, componentId, eventName);
|
|
1028
|
+
}
|
|
1029
|
+
library(address) {
|
|
1030
|
+
return getFrnlLibrary(this.store, address);
|
|
1031
|
+
}
|
|
1032
|
+
setVisualParameter(address, value) {
|
|
1033
|
+
setFrnlVisualParameter(this.store, address, value);
|
|
1034
|
+
}
|
|
1035
|
+
registerTheme(name, theme) {
|
|
1036
|
+
registerFrnlTheme(this.store, name, theme);
|
|
1037
|
+
}
|
|
1038
|
+
applyThemeToAllComponents(themeName) {
|
|
1039
|
+
if (!this.store.getState().themes[themeName]) {
|
|
1040
|
+
throw new Error(`Unknown Frnl theme: ${themeName}`);
|
|
1041
|
+
}
|
|
1042
|
+
for (const componentId of Object.keys(this.store.getState().entities)) {
|
|
1043
|
+
applyFrnlTheme(this.store, componentId, themeName);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
function createFrnlFacade(store) {
|
|
1048
|
+
return new FrnlFacade(store);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/runtime/component-definition.ts
|
|
1052
|
+
import { z as z4 } from "zod";
|
|
1053
|
+
var frnlVisualDefinitionSchema = z4.object({
|
|
1054
|
+
parameters: z4.record(z4.string(), z4.unknown())
|
|
1055
|
+
});
|
|
1056
|
+
var frnlPhaseDefinitionSchema = z4.object({
|
|
1057
|
+
visualDefinition: frnlVisualDefinitionSchema
|
|
1058
|
+
});
|
|
1059
|
+
var frnlEventDefinitionSchema = z4.object({
|
|
1060
|
+
functionId: z4.string().min(1)
|
|
1061
|
+
});
|
|
1062
|
+
var frnlStateDefinitionSchema = z4.object({
|
|
1063
|
+
name: z4.string().min(1),
|
|
1064
|
+
events: z4.record(z4.string().min(1), frnlEventDefinitionSchema),
|
|
1065
|
+
visualParameters: z4.record(z4.string(), z4.unknown()).default({}),
|
|
1066
|
+
phases: z4.object({
|
|
1067
|
+
attackPhase: frnlPhaseDefinitionSchema.optional(),
|
|
1068
|
+
sustainPhase: frnlPhaseDefinitionSchema,
|
|
1069
|
+
releasePhase: frnlPhaseDefinitionSchema.optional()
|
|
1070
|
+
})
|
|
1071
|
+
});
|
|
1072
|
+
var frnlComponentDefinitionSchema = z4.object({
|
|
1073
|
+
name: z4.string().min(1),
|
|
1074
|
+
states: z4.record(z4.string().min(1), frnlStateDefinitionSchema)
|
|
1075
|
+
});
|
|
1076
|
+
function defineFrnlComponent(definition) {
|
|
1077
|
+
return frnlComponentDefinitionSchema.parse(definition);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
export {
|
|
1081
|
+
lifecyclePhaseSchema,
|
|
1082
|
+
canTransition,
|
|
1083
|
+
LifecycleError,
|
|
1084
|
+
createLifecycle,
|
|
1085
|
+
contractSchema,
|
|
1086
|
+
defineContract,
|
|
1087
|
+
capabilitySchema,
|
|
1088
|
+
defineCapability,
|
|
1089
|
+
FrnlFault,
|
|
1090
|
+
defineEffect,
|
|
1091
|
+
defineCommand,
|
|
1092
|
+
defineResource,
|
|
1093
|
+
createResourceController,
|
|
1094
|
+
createScope,
|
|
1095
|
+
DependencyGraph,
|
|
1096
|
+
FrnlRegistry,
|
|
1097
|
+
matchRoute,
|
|
1098
|
+
buildRouteUrl,
|
|
1099
|
+
FrnlRouter,
|
|
1100
|
+
validateAccessibility,
|
|
1101
|
+
entityIdSchema,
|
|
1102
|
+
stateNameSchema,
|
|
1103
|
+
propertyKeySchema,
|
|
1104
|
+
propertyValueSchema,
|
|
1105
|
+
propertiesSchema,
|
|
1106
|
+
frnlThemeStateSchema,
|
|
1107
|
+
frnlThemeComponentSchema,
|
|
1108
|
+
frnlThemeDefinitionSchema,
|
|
1109
|
+
entityStateSchema,
|
|
1110
|
+
entityStateChangedPayloadSchema,
|
|
1111
|
+
entityPropertyChangedPayloadSchema,
|
|
1112
|
+
flowTriggeredPayloadSchema,
|
|
1113
|
+
frnlEventSchema,
|
|
1114
|
+
parseFrnlInput,
|
|
1115
|
+
validateFrnlInput,
|
|
1116
|
+
createFrnlStore,
|
|
1117
|
+
FrnlFacade,
|
|
1118
|
+
createFrnlFacade,
|
|
1119
|
+
frnlVisualDefinitionSchema,
|
|
1120
|
+
frnlPhaseDefinitionSchema,
|
|
1121
|
+
frnlEventDefinitionSchema,
|
|
1122
|
+
frnlStateDefinitionSchema,
|
|
1123
|
+
frnlComponentDefinitionSchema,
|
|
1124
|
+
defineFrnlComponent
|
|
1125
|
+
};
|