@kumikijs/runtime 0.2.1 → 0.3.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/dist/index.d.ts +24 -3
- package/dist/index.js +108 -6
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -86,6 +86,19 @@ declare function smoke(app: AppShape, root: HTMLElement, opts?: SmokeOptions): P
|
|
|
86
86
|
//#region src/index.d.ts
|
|
87
87
|
type RefinementCheck = (v: unknown) => boolean;
|
|
88
88
|
type EventHandler = (el: Record<string, unknown>) => void;
|
|
89
|
+
/**
|
|
90
|
+
* A controlled panic — Kumiki's "stop the program" signal (spec/stdlib.md §2.2:
|
|
91
|
+
* `panic(message)`; `Option/Result.get` on the empty case; `Result.get-err` on
|
|
92
|
+
* `Ok`). On the live path a panic is caught — the dispatch episode is rolled
|
|
93
|
+
* back (no partial slot writes) and an error boundary / top-level fallback is
|
|
94
|
+
* shown — instead of escaping the DOM event handler / render uncaught. The
|
|
95
|
+
* reducer-test harness already catches it to power `expect = {panic: ...}`.
|
|
96
|
+
*/
|
|
97
|
+
declare class KumikiPanic extends Error {
|
|
98
|
+
readonly isKumikiPanic: true;
|
|
99
|
+
readonly location: string | undefined;
|
|
100
|
+
constructor(message: string, location?: string);
|
|
101
|
+
}
|
|
89
102
|
type TileNode = {
|
|
90
103
|
kind: "page" | "column" | "row" | "card" | "box";
|
|
91
104
|
children: TileNode[];
|
|
@@ -372,7 +385,15 @@ declare const _stdlib: {
|
|
|
372
385
|
freshId(): string;
|
|
373
386
|
now(): number;
|
|
374
387
|
recordCopy(rec: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
|
|
375
|
-
|
|
388
|
+
/**
|
|
389
|
+
* `.get` — the polymorphic unwrap for Option AND Result. Per spec/stdlib.md
|
|
390
|
+
* §2.2 it PANICS on the empty case (`None` / `Err`); `Some(v)` / `Ok(v)`
|
|
391
|
+
* unwrap to `v`. A plain (non-variant) value passes through unchanged. (Before
|
|
392
|
+
* v0.3 this returned the value unchanged on the empty case, so `.get` and
|
|
393
|
+
* `.get-err` behaved oppositely — #24.)
|
|
394
|
+
*/
|
|
395
|
+
unwrap(opt: unknown): unknown; /** `panic(message)` — raise Kumiki's controlled stop-the-program signal. */
|
|
396
|
+
panic(message: unknown): never;
|
|
376
397
|
optionGetOr(opt: unknown, def: unknown): unknown;
|
|
377
398
|
Some(v: unknown): {
|
|
378
399
|
_tag: "Some";
|
|
@@ -407,7 +428,7 @@ declare const _stdlib: {
|
|
|
407
428
|
listHead(xs: unknown[] | undefined | null): unknown; /** List(T).tail → List(T) (all but the first; empty list stays empty). */
|
|
408
429
|
listTail(xs: unknown[] | undefined | null): unknown[]; /** List(T).last → Option(T). */
|
|
409
430
|
listLast(xs: unknown[] | undefined | null): unknown; /** Set(T).to-list / Option(T).to-list → List(T). */
|
|
410
|
-
toList(v: unknown): unknown[]; /** Result(T,E).get-err → E; panics if the value is Ok. */
|
|
431
|
+
toList(v: unknown): unknown[]; /** Result(T,E).get-err → E; panics (KumikiPanic) if the value is Ok. */
|
|
411
432
|
getErr(r: unknown): unknown; /** Result(T,E).to-option → Option(T): Ok(v) → Some(v), Err(_) → None. */
|
|
412
433
|
toOption(r: unknown): unknown; /** Text.parse-int → Option(Int) (truncates; mirrors `Int.parse`). */
|
|
413
434
|
parseIntOpt(s: unknown): unknown; /** Text.parse-float → Option(Float) (mirrors `Float.parse`). */
|
|
@@ -419,4 +440,4 @@ declare const builtinEffects: {
|
|
|
419
440
|
httpFetch(method: string, input: unknown, baseUrl: string): Promise<EffectResult>;
|
|
420
441
|
};
|
|
421
442
|
//#endregion
|
|
422
|
-
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, TestResult, Theme, ThemeValue, TileNode, TileProps, _stdlib, builtinEffects, mount, runScenario, smoke };
|
|
443
|
+
export { type Action, AppShape, CapabilityRegistry, EffectResult, type EffectScript, EffectSpec, EmitSpec, EventHandler, type Expect, KumikiPanic, RedirectEntry, ReducerSpec, RefinementCheck, RouteEntry, type Scenario, type ScenarioReport, type ScenarioStep, SlotMeta, type SmokeIssue, type SmokeOptions, type SmokePhase, type SmokeReport, type StepResult, TestResult, Theme, ThemeValue, TileNode, TileProps, _stdlib, builtinEffects, mount, runScenario, smoke };
|
package/dist/index.js
CHANGED
|
@@ -361,6 +361,27 @@ function errStr(e) {
|
|
|
361
361
|
}
|
|
362
362
|
//#endregion
|
|
363
363
|
//#region src/index.ts
|
|
364
|
+
/**
|
|
365
|
+
* A controlled panic — Kumiki's "stop the program" signal (spec/stdlib.md §2.2:
|
|
366
|
+
* `panic(message)`; `Option/Result.get` on the empty case; `Result.get-err` on
|
|
367
|
+
* `Ok`). On the live path a panic is caught — the dispatch episode is rolled
|
|
368
|
+
* back (no partial slot writes) and an error boundary / top-level fallback is
|
|
369
|
+
* shown — instead of escaping the DOM event handler / render uncaught. The
|
|
370
|
+
* reducer-test harness already catches it to power `expect = {panic: ...}`.
|
|
371
|
+
*/
|
|
372
|
+
var KumikiPanic = class extends Error {
|
|
373
|
+
isKumikiPanic = true;
|
|
374
|
+
location;
|
|
375
|
+
constructor(message, location) {
|
|
376
|
+
super(message);
|
|
377
|
+
this.name = "KumikiPanic";
|
|
378
|
+
this.location = location;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
/** True for a KumikiPanic — also matches across realms where `instanceof` fails. */
|
|
382
|
+
function isPanic(e) {
|
|
383
|
+
return e instanceof KumikiPanic || typeof e === "object" && e !== null && e.isKumikiPanic === true;
|
|
384
|
+
}
|
|
364
385
|
function parseLocation(routes, loc) {
|
|
365
386
|
const path = loc.pathname || "/";
|
|
366
387
|
const query = {};
|
|
@@ -448,7 +469,13 @@ function mount(app, target) {
|
|
|
448
469
|
};
|
|
449
470
|
}
|
|
450
471
|
maybeReapplyTheme(app);
|
|
451
|
-
|
|
472
|
+
let dom;
|
|
473
|
+
try {
|
|
474
|
+
dom = renderTile(pickRootTile(app));
|
|
475
|
+
} catch (e) {
|
|
476
|
+
reportPanic("render", e);
|
|
477
|
+
dom = renderPanicFallback(e);
|
|
478
|
+
}
|
|
452
479
|
if (currentRoot) target.replaceChild(dom, currentRoot);
|
|
453
480
|
else target.appendChild(dom);
|
|
454
481
|
currentRoot = dom;
|
|
@@ -475,9 +502,38 @@ function mount(app, target) {
|
|
|
475
502
|
text: "(no root)"
|
|
476
503
|
};
|
|
477
504
|
}
|
|
505
|
+
let inPanicHandler = false;
|
|
506
|
+
/**
|
|
507
|
+
* Handle a caught live panic per spec/lifecycle.md §7.2: the dispatch episode
|
|
508
|
+
* is already rolled back (the caller never applied the failed result), so we
|
|
509
|
+
* surface it (console.error → smoke/scenario see it) and fire the `app.error`
|
|
510
|
+
* reducer(s) with `$event = PanicInfo`, exactly as §7.2.3 specifies.
|
|
511
|
+
*/
|
|
512
|
+
function handleLivePanic(location, e) {
|
|
513
|
+
reportPanic(location, e);
|
|
514
|
+
if (inPanicHandler) return;
|
|
515
|
+
const handlers = app.reducers.filter((h) => h.event.kind === "lifecycle" && h.event.name === "app.error");
|
|
516
|
+
if (handlers.length === 0) return;
|
|
517
|
+
const info = {
|
|
518
|
+
message: panicInfo(e).message,
|
|
519
|
+
location
|
|
520
|
+
};
|
|
521
|
+
inPanicHandler = true;
|
|
522
|
+
try {
|
|
523
|
+
for (const h of handlers) applyReducer(h, { $event: info });
|
|
524
|
+
} finally {
|
|
525
|
+
inPanicHandler = false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
478
528
|
function applyReducer(r, payload) {
|
|
479
529
|
if (disposed) return;
|
|
480
|
-
|
|
530
|
+
let result;
|
|
531
|
+
try {
|
|
532
|
+
result = r.apply(slotValues, payload);
|
|
533
|
+
} catch (e) {
|
|
534
|
+
handleLivePanic(`reducer "${r.name}"`, e);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
481
537
|
for (const [k, v] of Object.entries(result.slots)) {
|
|
482
538
|
const meta = app.slots[k];
|
|
483
539
|
if (meta?.refine && !meta.refine(v)) continue;
|
|
@@ -747,6 +803,39 @@ function _setPathHelper(obj, path, value) {
|
|
|
747
803
|
[head]: _setPathHelper(cur[head], rest, value)
|
|
748
804
|
};
|
|
749
805
|
}
|
|
806
|
+
/** Message + optional source location for a caught throw (panic or otherwise). */
|
|
807
|
+
function panicInfo(e) {
|
|
808
|
+
if (isPanic(e)) return {
|
|
809
|
+
message: e.message,
|
|
810
|
+
location: e.location
|
|
811
|
+
};
|
|
812
|
+
if (e instanceof Error) return {
|
|
813
|
+
message: e.message,
|
|
814
|
+
location: void 0
|
|
815
|
+
};
|
|
816
|
+
return {
|
|
817
|
+
message: String(e),
|
|
818
|
+
location: void 0
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Surface a caught live panic so the verification tiers still see it: smoke()
|
|
823
|
+
* and runScenario() both patch console.error into their issue/error buffers, so
|
|
824
|
+
* a controlled panic is reported as a failure rather than silently swallowed.
|
|
825
|
+
*/
|
|
826
|
+
function reportPanic(where, e) {
|
|
827
|
+
const { message } = panicInfo(e);
|
|
828
|
+
console.error(`[kumiki] ${isPanic(e) ? "panic" : "error"} in ${where}: ${message}`);
|
|
829
|
+
}
|
|
830
|
+
/** A minimal top-level fallback for a render panic with no enclosing boundary. */
|
|
831
|
+
function renderPanicFallback(e) {
|
|
832
|
+
const { message, location } = panicInfo(e);
|
|
833
|
+
const div = document.createElement("div");
|
|
834
|
+
div.dataset.kumikiPanic = location ?? "";
|
|
835
|
+
div.setAttribute("role", "alert");
|
|
836
|
+
div.textContent = `Something went wrong: ${message}`;
|
|
837
|
+
return div;
|
|
838
|
+
}
|
|
750
839
|
function renderTile(node) {
|
|
751
840
|
const el = renderTileNode(node);
|
|
752
841
|
applyMotion(el, node.props);
|
|
@@ -1726,13 +1815,26 @@ const _stdlib = {
|
|
|
1726
1815
|
...patch
|
|
1727
1816
|
};
|
|
1728
1817
|
},
|
|
1818
|
+
/**
|
|
1819
|
+
* `.get` — the polymorphic unwrap for Option AND Result. Per spec/stdlib.md
|
|
1820
|
+
* §2.2 it PANICS on the empty case (`None` / `Err`); `Some(v)` / `Ok(v)`
|
|
1821
|
+
* unwrap to `v`. A plain (non-variant) value passes through unchanged. (Before
|
|
1822
|
+
* v0.3 this returned the value unchanged on the empty case, so `.get` and
|
|
1823
|
+
* `.get-err` behaved oppositely — #24.)
|
|
1824
|
+
*/
|
|
1729
1825
|
unwrap(opt) {
|
|
1730
1826
|
if (opt && typeof opt === "object" && "_tag" in opt) {
|
|
1731
1827
|
const o = opt;
|
|
1732
|
-
if (o._tag === "Some") return o._0;
|
|
1828
|
+
if (o._tag === "Some" || o._tag === "Ok") return o._0;
|
|
1829
|
+
if (o._tag === "None") throw new KumikiPanic("get called on None");
|
|
1830
|
+
if (o._tag === "Err") throw new KumikiPanic("get called on an Err value");
|
|
1733
1831
|
}
|
|
1734
1832
|
return opt;
|
|
1735
1833
|
},
|
|
1834
|
+
/** `panic(message)` — raise Kumiki's controlled stop-the-program signal. */
|
|
1835
|
+
panic(message) {
|
|
1836
|
+
throw new KumikiPanic(String(message));
|
|
1837
|
+
},
|
|
1736
1838
|
optionGetOr(opt, def) {
|
|
1737
1839
|
if (opt && typeof opt === "object" && "_tag" in opt) {
|
|
1738
1840
|
const o = opt;
|
|
@@ -1873,13 +1975,13 @@ const _stdlib = {
|
|
|
1873
1975
|
if (v && typeof v === "object") return Object.keys(v);
|
|
1874
1976
|
return [];
|
|
1875
1977
|
},
|
|
1876
|
-
/** Result(T,E).get-err → E; panics if the value is Ok. */
|
|
1978
|
+
/** Result(T,E).get-err → E; panics (KumikiPanic) if the value is Ok. */
|
|
1877
1979
|
getErr(r) {
|
|
1878
1980
|
if (r && typeof r === "object" && "_tag" in r) {
|
|
1879
1981
|
const t = r;
|
|
1880
1982
|
if (t._tag === "Err") return t._0;
|
|
1881
1983
|
}
|
|
1882
|
-
throw new
|
|
1984
|
+
throw new KumikiPanic("get-err called on a non-Err value");
|
|
1883
1985
|
},
|
|
1884
1986
|
/** Result(T,E).to-option → Option(T): Ok(v) → Some(v), Err(_) → None. */
|
|
1885
1987
|
toOption(r) {
|
|
@@ -1992,4 +2094,4 @@ const builtinEffects = {
|
|
|
1992
2094
|
}
|
|
1993
2095
|
};
|
|
1994
2096
|
//#endregion
|
|
1995
|
-
export { _stdlib, builtinEffects, mount, runScenario, smoke };
|
|
2097
|
+
export { KumikiPanic, _stdlib, builtinEffects, mount, runScenario, smoke };
|
package/package.json
CHANGED