@kumikijs/runtime 0.1.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.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @kumikijs/runtime
2
+
3
+ Kumiki DOM runtime — mounts compiled Kumiki apps, dispatches effects, and manages the signal graph. Part of [Kumiki](https://github.com/kage1020/Kumiki).
4
+
5
+ You normally do not import this directly: the compiler embeds the runtime into generated apps. It is published so generated apps and tooling can resolve it.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm i @kumikijs/runtime
11
+ ```
12
+
13
+ ## Exports
14
+
15
+ - `@kumikijs/runtime` — the runtime API (`mount`, `runScenario`, `smoke`, …).
16
+ - `@kumikijs/runtime/bundle` — the prebuilt, self-contained runtime bundle as a single file, embedded verbatim into generated apps.
17
+
18
+ ## License
19
+
20
+ Apache-2.0
@@ -0,0 +1,363 @@
1
+ //#region src/scenario.d.ts
2
+ /** One thing to do to the app. Exactly one field should be set. */
3
+ type Action = {
4
+ dispatch: string;
5
+ payload?: Record<string, unknown>;
6
+ } | {
7
+ clickText: string;
8
+ } | {
9
+ click: string;
10
+ } | {
11
+ fill: string;
12
+ value: string;
13
+ } | {
14
+ choose: string;
15
+ value: string;
16
+ } | {
17
+ navigate: string;
18
+ };
19
+ /** Assertions evaluated against the snapshot taken after a step. */
20
+ type Expect = {
21
+ /** No runtime errors since the previous step. */noErrors?: boolean; /** Partial match against the slot state (slot name → expected value). */
22
+ state?: Record<string, unknown>; /** Substrings that must appear in the rendered text. */
23
+ domIncludes?: string[]; /** Substrings that must NOT appear in the rendered text. */
24
+ domExcludes?: string[];
25
+ };
26
+ type ScenarioStep = {
27
+ label?: string;
28
+ do?: Action;
29
+ expect?: Expect;
30
+ };
31
+ /** A scripted effect outcome, returned in order each time the effect fires. */
32
+ type EffectScript = {
33
+ outcome: "ok" | "err";
34
+ value?: unknown;
35
+ };
36
+ type Scenario = {
37
+ steps: ScenarioStep[]; /** Per-effect queues of scripted results (keeps the loop hermetic). */
38
+ effects?: Record<string, EffectScript[]>; /** Default result for effects with no script. Default: { outcome: "ok", value: null }. */
39
+ defaultEffect?: EffectScript;
40
+ };
41
+ type StepResult = {
42
+ label?: string;
43
+ action?: string;
44
+ errors: string[];
45
+ emits: {
46
+ effect: string;
47
+ args: unknown[];
48
+ }[];
49
+ state: Record<string, unknown>;
50
+ domText: string;
51
+ failures: string[];
52
+ };
53
+ type ScenarioReport = {
54
+ ok: boolean;
55
+ steps: StepResult[];
56
+ };
57
+ declare function runScenario(app: AppShape, root: HTMLElement, scenario: Scenario, opts?: {
58
+ settleMs?: number;
59
+ }): Promise<ScenarioReport>;
60
+ //#endregion
61
+ //#region src/smoke.d.ts
62
+ type SmokePhase = "mount" | "initial-render" | "interaction" | "async";
63
+ type SmokeIssue = {
64
+ phase: SmokePhase;
65
+ message: string; /** What triggered it, e.g. "click button[0] (\"Create issue\")". */
66
+ trigger?: string | undefined;
67
+ };
68
+ type SmokeReport = {
69
+ ok: boolean;
70
+ mounted: boolean;
71
+ rendered: boolean;
72
+ interactions: number;
73
+ issues: SmokeIssue[];
74
+ };
75
+ type SmokeOptions = {
76
+ /** Drive interactive elements after the initial render. Default: true. */interact?: boolean; /** Max interactive elements to exercise. Default: 40. */
77
+ maxInteractions?: number; /** Milliseconds to let async effects/timers settle after each step. Default: 30. */
78
+ settleMs?: number;
79
+ };
80
+ /**
81
+ * Mount `app` into `root`, drive its UI, and report runtime failures.
82
+ * Runs in any DOM environment (jsdom for CI, a real browser for the playground).
83
+ */
84
+ declare function smoke(app: AppShape, root: HTMLElement, opts?: SmokeOptions): Promise<SmokeReport>;
85
+ //#endregion
86
+ //#region src/index.d.ts
87
+ type RefinementCheck = (v: unknown) => boolean;
88
+ type EventHandler = (el: Record<string, unknown>) => void;
89
+ type TileNode = {
90
+ kind: "page" | "column" | "row" | "card" | "box";
91
+ children: TileNode[];
92
+ props?: TileProps;
93
+ } | {
94
+ kind: "heading" | "text";
95
+ text: string;
96
+ props?: TileProps;
97
+ } | {
98
+ kind: "button";
99
+ text: string;
100
+ props?: TileProps;
101
+ loading?: boolean;
102
+ disabled?: boolean;
103
+ } | {
104
+ kind: "input";
105
+ props?: TileProps;
106
+ bind?: string;
107
+ bindPath?: string[];
108
+ value?: string;
109
+ type?: string;
110
+ placeholder?: string;
111
+ required?: boolean;
112
+ autoFocus?: boolean;
113
+ id?: string;
114
+ } | {
115
+ kind: "textarea";
116
+ props?: TileProps;
117
+ bind?: string;
118
+ bindPath?: string[];
119
+ value?: string;
120
+ rows?: number;
121
+ placeholder?: string;
122
+ id?: string;
123
+ } | {
124
+ kind: "check";
125
+ checked: boolean;
126
+ props?: TileProps;
127
+ } | {
128
+ kind: "spinner";
129
+ props?: TileProps;
130
+ } | {
131
+ kind: "skeleton";
132
+ props?: TileProps;
133
+ } | {
134
+ kind: "form";
135
+ children: TileNode[];
136
+ props?: TileProps;
137
+ } | {
138
+ kind: "label";
139
+ text: string;
140
+ props?: TileProps;
141
+ } | {
142
+ kind: "link";
143
+ text: string;
144
+ to: string;
145
+ props?: TileProps;
146
+ } | {
147
+ kind: "markdown";
148
+ text: string;
149
+ props?: TileProps;
150
+ } | {
151
+ kind: "image";
152
+ src: string;
153
+ props?: TileProps;
154
+ } | {
155
+ kind: "icon";
156
+ name: string;
157
+ props?: TileProps;
158
+ } | {
159
+ kind: "select";
160
+ props?: TileProps;
161
+ bind?: string;
162
+ bindPath?: string[];
163
+ value?: unknown;
164
+ options?: Array<{
165
+ label: unknown;
166
+ value: unknown;
167
+ }>;
168
+ placeholder?: string;
169
+ } | {
170
+ kind: "radio";
171
+ props?: TileProps;
172
+ group?: string;
173
+ value?: unknown;
174
+ selected?: boolean;
175
+ } | {
176
+ kind: "grid" | "stack" | "region" | "scroll" | "panel" | "fieldset";
177
+ children: TileNode[];
178
+ props?: TileProps;
179
+ } | {
180
+ kind: "divider";
181
+ props?: TileProps;
182
+ };
183
+ type TileProps = Record<string, unknown> & {
184
+ onClick?: EventHandler;
185
+ onSubmit?: EventHandler;
186
+ onChange?: EventHandler;
187
+ onInput?: EventHandler;
188
+ el?: Record<string, unknown>;
189
+ };
190
+ type SlotMeta = {
191
+ value: unknown;
192
+ refine?: RefinementCheck;
193
+ volatile?: boolean;
194
+ };
195
+ type ReducerSpec = {
196
+ name: string;
197
+ selector?: {
198
+ tile: string;
199
+ id?: string;
200
+ };
201
+ event: {
202
+ kind: "ui";
203
+ ev: "click" | "submit" | "change" | "input";
204
+ } | {
205
+ kind: "effect";
206
+ effect: string;
207
+ outcome: "ok" | "err";
208
+ } | {
209
+ kind: "timer";
210
+ intervalMs: number;
211
+ } | {
212
+ kind: "lifecycle";
213
+ name: string;
214
+ };
215
+ apply: (slots: Record<string, unknown>, payload: Record<string, unknown>) => {
216
+ slots: Record<string, unknown>;
217
+ emits: EmitSpec[];
218
+ };
219
+ };
220
+ type EmitSpec = {
221
+ effect: string;
222
+ args: unknown[];
223
+ };
224
+ type EffectSpec = {
225
+ name: string;
226
+ cap: string;
227
+ policy?: {
228
+ kind: "latest";
229
+ } | {
230
+ kind: "latest-per-key";
231
+ keyOf: (input: unknown) => string;
232
+ } | {
233
+ kind: "queue";
234
+ } | {
235
+ kind: "debounce";
236
+ ms: number;
237
+ } | {
238
+ kind: "throttle";
239
+ ms: number;
240
+ } | {
241
+ kind: "once";
242
+ };
243
+ invoke: (input: unknown, caps: CapabilityRegistry) => Promise<EffectResult>;
244
+ };
245
+ type EffectResult = {
246
+ kind: "ok";
247
+ value: unknown;
248
+ } | {
249
+ kind: "err";
250
+ value: unknown;
251
+ };
252
+ type CapabilityRegistry = {
253
+ has(cap: string): boolean;
254
+ };
255
+ type RouteEntry = {
256
+ pattern: string; /** Returns the TileNode for this route given the current state. */
257
+ tile: () => TileNode;
258
+ };
259
+ type RedirectEntry = {
260
+ pattern: string;
261
+ redirectTo: string;
262
+ };
263
+ type ThemeValue = string | number | {
264
+ [k: string]: ThemeValue;
265
+ };
266
+ type Theme = {
267
+ [k: string]: ThemeValue;
268
+ };
269
+ type AppShape = {
270
+ slots: Record<string, SlotMeta>;
271
+ caps: string[];
272
+ reducers: ReducerSpec[];
273
+ effects: Record<string, EffectSpec>;
274
+ init: EmitSpec[];
275
+ routes?: Array<RouteEntry | RedirectEntry>;
276
+ http?: {
277
+ baseUrl?: string;
278
+ headers?: () => Record<string, string>;
279
+ on401?: string;
280
+ timeout?: number;
281
+ }; /** Phase 4: registered themes by name. */
282
+ themes?: Record<string, Theme>; /** Phase 4: selected theme name. */
283
+ themeName?: string | null;
284
+ root?: () => TileNode;
285
+ live?: Record<string, unknown>;
286
+ _rerender?: () => void;
287
+ };
288
+ declare function mount(app: AppShape, target: HTMLElement): {
289
+ dispose: () => void;
290
+ };
291
+ declare const _stdlib: {
292
+ mapSize(m: unknown): number;
293
+ mapKeys(m: Record<string, unknown> | undefined | null): string[];
294
+ mapValues(m: Record<string, unknown> | undefined | null): unknown[];
295
+ mapEntries(m: Record<string, unknown> | undefined | null): unknown[];
296
+ mapGet(m: Record<string, unknown> | undefined | null, k: string): unknown; /** Polymorphic `.get-or(default)` for Option-like values. */
297
+ getOr(v: unknown, fallback: unknown): unknown;
298
+ mapGetOr(m: Record<string, unknown> | undefined | null, k: string, def: unknown): unknown;
299
+ mapInsert(m: Record<string, unknown>, k: string, v: unknown): Record<string, unknown>;
300
+ mapRemove(m: Record<string, unknown>, k: string): Record<string, unknown>;
301
+ mapFilter(m: Record<string, unknown>, pred: (k: string, v: unknown) => boolean): Record<string, unknown>;
302
+ /**
303
+ * Polymorphic `.filter` dispatch — used by codegen when the receiver type
304
+ * isn't statically known (e.g. `m.keys.filter(...)` vs `m.filter(...)`).
305
+ * Arrays go through Array.prototype.filter; objects (Maps in Kumiki) fall
306
+ * back to the (k, v) → boolean predicate of mapFilter.
307
+ */
308
+ filter(coll: unknown, pred: (...args: unknown[]) => boolean): unknown;
309
+ listSize(xs: unknown[]): number;
310
+ listFilter<T>(xs: T[], pred: (x: T) => boolean): T[];
311
+ listMap<T, U>(xs: T[], fn: (x: T) => U): U[]; /** Polymorphic `.map`: over List elements, or over Option/Result Some/Ok. */
312
+ mapOver(coll: unknown, fn: (x: unknown) => unknown): unknown; /** Option(T).flat-map(f): Some(v) -> f(v), None -> None. f returns an Option. */
313
+ flatMapOption(opt: unknown, fn: (x: unknown) => unknown): unknown;
314
+ listSortBy<T>(xs: T[], keyOf: (x: T) => number): T[]; /** List(T).fold(init, expr): left fold with $1=acc, $2=elem. */
315
+ listFold<T, A>(xs: T[], init: A, fn: (acc: A, x: T) => A): A;
316
+ setHas(s: Record<string, true> | undefined, x: unknown): boolean;
317
+ setToggle(s: Record<string, true> | undefined, x: unknown): Record<string, true>;
318
+ add(a: unknown, b: unknown): unknown;
319
+ show(v: unknown): string;
320
+ eq(a: unknown, b: unknown): boolean;
321
+ freshId(): string;
322
+ now(): number;
323
+ recordCopy(rec: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
324
+ unwrap(opt: unknown): unknown;
325
+ optionGetOr(opt: unknown, def: unknown): unknown;
326
+ Some(v: unknown): {
327
+ _tag: "Some";
328
+ _0: unknown;
329
+ };
330
+ None: {
331
+ _tag: "None";
332
+ };
333
+ Ok(v: unknown): {
334
+ _tag: "Ok";
335
+ _0: unknown;
336
+ };
337
+ Err(v: unknown): {
338
+ _tag: "Err";
339
+ _0: unknown;
340
+ };
341
+ variant(tag: string, ...args: unknown[]): {
342
+ _tag: string;
343
+ [k: string]: unknown;
344
+ };
345
+ variantIs(v: unknown, tag: string): boolean; /** List(T).chunk(n) → List(List(T)). The last chunk may be shorter. */
346
+ listChunk(xs: unknown[] | undefined | null, n: number): unknown[]; /** List(T).zip(other) → List(Tuple(T, U)); truncates to the shorter list. */
347
+ listZip(a: unknown[] | undefined | null, b: unknown[] | undefined | null): unknown[]; /** Map(K,V).update(k, fn): apply fn to the current value of k, no-op if absent. */
348
+ mapUpdate(m: Record<string, unknown> | undefined | null, k: string, fn: (v: unknown) => unknown): Record<string, unknown>; /** Set(T).add(x). Sets are stored as `{ [String(x)]: true }`. */
349
+ setAdd(s: Record<string, true> | undefined | null, x: unknown): Record<string, true>; /** Set(T).union(other). */
350
+ setUnion(a: Record<string, true> | undefined | null, b: Record<string, true> | undefined | null): Record<string, true>; /** Set(T).intersect(other) — keys present in both. */
351
+ setIntersect(a: Record<string, true> | undefined | null, b: Record<string, true> | undefined | null): Record<string, true>; /** Set(T).diff(other) — keys in a not in b. */
352
+ setDiff(a: Record<string, true> | undefined | null, b: Record<string, true> | undefined | null): Record<string, true>; /** Option(T).or / Result(T,E).or — receiver when Some/Ok, else `other`. */
353
+ or(v: unknown, other: unknown): unknown; /** Result(T,E).map-err(fn) — maps the Err payload, passes Ok through unchanged. */
354
+ mapErr(r: unknown, fn: (e: unknown) => unknown): unknown; /** Polymorphic `.diff`: numeric magnitude (Time/Duration) or Set difference. */
355
+ diff(a: unknown, b: unknown): unknown;
356
+ };
357
+ declare const builtinEffects: {
358
+ storageRead(input: unknown): Promise<EffectResult>;
359
+ storageWrite(input: unknown): Promise<EffectResult>;
360
+ httpFetch(method: string, input: unknown, baseUrl: string): Promise<EffectResult>;
361
+ };
362
+ //#endregion
363
+ export { type Action, AppShape, CapabilityRegistry, EffectResult, type EffectScript, EffectSpec, EmitSpec, EventHandler, type Expect, RedirectEntry, ReducerSpec, RefinementCheck, RouteEntry, type Scenario, type ScenarioReport, type ScenarioStep, SlotMeta, type SmokeIssue, type SmokeOptions, type SmokePhase, type SmokeReport, type StepResult, Theme, ThemeValue, TileNode, TileProps, _stdlib, builtinEffects, mount, runScenario, smoke };