@kumikijs/runtime 0.4.0 → 0.5.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 +19 -0
- package/dist/index.js +104 -15
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,14 @@ type KumikiElementOptions = {
|
|
|
23
23
|
* apply, matching a standalone Kumiki page).
|
|
24
24
|
*/
|
|
25
25
|
shadow?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Routing source forwarded to mount (#36). An embedded element rarely owns the
|
|
28
|
+
* top-level URL, so a routed Kumiki app inside it should usually use
|
|
29
|
+
* `"memory"` (an in-memory path) rather than the default `"history"`, which
|
|
30
|
+
* reads/writes the host page's location/history.
|
|
31
|
+
*/
|
|
32
|
+
router?: "history" | "memory"; /** Initial path for the memory router (default `"/"`). */
|
|
33
|
+
initialPath?: string;
|
|
26
34
|
};
|
|
27
35
|
/**
|
|
28
36
|
* Register `app` as the custom element `tagName`. Idempotent: if the tag is
|
|
@@ -89,6 +97,8 @@ type ScenarioReport = {
|
|
|
89
97
|
};
|
|
90
98
|
declare function runScenario(app: AppShape, root: HTMLElement, scenario: Scenario, opts?: {
|
|
91
99
|
settleMs?: number;
|
|
100
|
+
router?: "history" | "memory";
|
|
101
|
+
initialPath?: string;
|
|
92
102
|
}): Promise<ScenarioReport>;
|
|
93
103
|
//#endregion
|
|
94
104
|
//#region src/smoke.d.ts
|
|
@@ -324,6 +334,15 @@ type MountOptions = {
|
|
|
324
334
|
* passes its in-shadow container so theming stays encapsulated.
|
|
325
335
|
*/
|
|
326
336
|
styleHost?: HTMLElement;
|
|
337
|
+
/**
|
|
338
|
+
* Routing source (#36). `"history"` (default) reads/writes the ambient
|
|
339
|
+
* document `location` / `history`. `"memory"` holds the current path in
|
|
340
|
+
* memory and never touches `history.*` — for embedded / sandboxed hosts (the
|
|
341
|
+
* docs playground `srcdoc`, a Web Component) where the Kumiki app does not own
|
|
342
|
+
* the top-level URL and `history.pushState` throws in an opaque origin.
|
|
343
|
+
*/
|
|
344
|
+
router?: "history" | "memory"; /** Initial path for the memory router (default `"/"`). Ignored in history mode. */
|
|
345
|
+
initialPath?: string;
|
|
327
346
|
};
|
|
328
347
|
type RouteEntry = {
|
|
329
348
|
pattern: string; /** Returns the TileNode for this route given the current state. */
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,8 @@ function defineKumikiElement(tagName, app, options = {}) {
|
|
|
36
36
|
this.app = makeApp();
|
|
37
37
|
let target = this;
|
|
38
38
|
const mountOpts = { providers: buildProviders(this) };
|
|
39
|
+
if (options.router) mountOpts.router = options.router;
|
|
40
|
+
if (options.initialPath !== void 0) mountOpts.initialPath = options.initialPath;
|
|
39
41
|
if (options.shadow) {
|
|
40
42
|
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
41
43
|
root.replaceChildren();
|
|
@@ -127,9 +129,12 @@ async function runScenario(app, root, scenario, opts = {}) {
|
|
|
127
129
|
};
|
|
128
130
|
};
|
|
129
131
|
const dispatchable = app;
|
|
132
|
+
const mountOpts = {};
|
|
133
|
+
if (opts.router) mountOpts.router = opts.router;
|
|
134
|
+
if (opts.initialPath !== void 0) mountOpts.initialPath = opts.initialPath;
|
|
130
135
|
try {
|
|
131
136
|
try {
|
|
132
|
-
mount(app, root);
|
|
137
|
+
mount(app, root, mountOpts);
|
|
133
138
|
} catch (e) {
|
|
134
139
|
steps.push(mkStep(void 0, "mount", [`mount threw: ${errStr$1(e)}`], [], app, root, []));
|
|
135
140
|
return finish();
|
|
@@ -464,6 +469,69 @@ var KumikiPanic = class extends Error {
|
|
|
464
469
|
function isPanic(e) {
|
|
465
470
|
return e instanceof KumikiPanic || typeof e === "object" && e !== null && e.isKumikiPanic === true;
|
|
466
471
|
}
|
|
472
|
+
function historyRouter() {
|
|
473
|
+
return {
|
|
474
|
+
read: () => ({
|
|
475
|
+
pathname: location.pathname,
|
|
476
|
+
search: location.search,
|
|
477
|
+
hash: location.hash
|
|
478
|
+
}),
|
|
479
|
+
push: (p) => history.pushState(null, "", p),
|
|
480
|
+
replace: (p) => history.replaceState(null, "", p),
|
|
481
|
+
back: () => history.back(),
|
|
482
|
+
subscribe: (cb) => {
|
|
483
|
+
const h = () => cb();
|
|
484
|
+
window.addEventListener("popstate", h);
|
|
485
|
+
return () => window.removeEventListener("popstate", h);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/** Split a raw path into the `{ pathname, search, hash }` parseLocation reads. */
|
|
490
|
+
function splitPath(p) {
|
|
491
|
+
let rest = p || "/";
|
|
492
|
+
let hash = "";
|
|
493
|
+
const hi = rest.indexOf("#");
|
|
494
|
+
if (hi !== -1) {
|
|
495
|
+
hash = rest.slice(hi);
|
|
496
|
+
rest = rest.slice(0, hi);
|
|
497
|
+
}
|
|
498
|
+
let search = "";
|
|
499
|
+
const qi = rest.indexOf("?");
|
|
500
|
+
if (qi !== -1) {
|
|
501
|
+
search = rest.slice(qi);
|
|
502
|
+
rest = rest.slice(0, qi);
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
pathname: rest || "/",
|
|
506
|
+
search,
|
|
507
|
+
hash
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function memoryRouter(initialPath = "/") {
|
|
511
|
+
const stack = [initialPath || "/"];
|
|
512
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
513
|
+
return {
|
|
514
|
+
read: () => splitPath(stack[stack.length - 1] ?? "/"),
|
|
515
|
+
push: (p) => {
|
|
516
|
+
stack.push(p);
|
|
517
|
+
},
|
|
518
|
+
replace: (p) => {
|
|
519
|
+
stack[stack.length - 1] = p;
|
|
520
|
+
},
|
|
521
|
+
back: () => {
|
|
522
|
+
if (stack.length > 1) {
|
|
523
|
+
stack.pop();
|
|
524
|
+
for (const l of listeners) l();
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
subscribe: (cb) => {
|
|
528
|
+
listeners.add(cb);
|
|
529
|
+
return () => {
|
|
530
|
+
listeners.delete(cb);
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
467
535
|
function parseLocation(routes, loc) {
|
|
468
536
|
const path = loc.pathname || "/";
|
|
469
537
|
const query = {};
|
|
@@ -532,6 +600,8 @@ function mount(app, target, options = {}) {
|
|
|
532
600
|
stateStylesEl = null;
|
|
533
601
|
ensureMotionStyles(app);
|
|
534
602
|
const slotValues = app.live;
|
|
603
|
+
const router = options.router === "memory" ? memoryRouter(options.initialPath) : historyRouter();
|
|
604
|
+
let routerUnsub;
|
|
535
605
|
const dispatcher = makeEffectDispatcher(app, makeCapabilityRegistry(app.caps, options.providers), (effect, outcome, value, key) => {
|
|
536
606
|
handleEffectResult(effect, outcome, value, key);
|
|
537
607
|
});
|
|
@@ -635,19 +705,24 @@ function mount(app, target, options = {}) {
|
|
|
635
705
|
render();
|
|
636
706
|
}
|
|
637
707
|
function handleEffectResult(effect, outcome, value, key) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
708
|
+
let matched = 0;
|
|
709
|
+
for (const r of app.reducers) if (r.event.kind === "effect" && r.event.effect === effect && r.event.outcome === outcome) {
|
|
710
|
+
applyReducer(r, {
|
|
711
|
+
$1: value,
|
|
712
|
+
$2: key
|
|
713
|
+
});
|
|
714
|
+
matched++;
|
|
715
|
+
}
|
|
716
|
+
if (outcome === "err" && matched === 0) reportUnhandledEffectError(effect, value);
|
|
642
717
|
}
|
|
643
718
|
function updateRoute(newPath, replace) {
|
|
644
|
-
if (replace)
|
|
645
|
-
else
|
|
719
|
+
if (replace) router.replace(newPath);
|
|
720
|
+
else router.push(newPath);
|
|
646
721
|
syncRouteFromLocation();
|
|
647
722
|
}
|
|
648
723
|
function syncRouteFromLocation() {
|
|
649
724
|
const oldRoute = slotValues.route;
|
|
650
|
-
const newRoute = parseLocation(app.routes,
|
|
725
|
+
const newRoute = parseLocation(app.routes, router.read());
|
|
651
726
|
slotValues.route = newRoute;
|
|
652
727
|
if (oldRoute && oldRoute.pattern !== newRoute.pattern) {
|
|
653
728
|
for (const r of app.reducers) if (r.event.kind === "lifecycle" && r.event.name === `route.leave(${JSON.stringify(oldRoute.pattern)})`) applyReducer(r, { $route: oldRoute });
|
|
@@ -655,17 +730,17 @@ function mount(app, target, options = {}) {
|
|
|
655
730
|
for (const r of app.reducers) if (r.event.kind === "lifecycle" && r.event.name === `route.enter(${JSON.stringify(newRoute.pattern)})`) applyReducer(r, { $route: newRoute });
|
|
656
731
|
render();
|
|
657
732
|
}
|
|
658
|
-
registerBuiltinEffects(app, updateRoute, () => slotValues, () => render());
|
|
733
|
+
registerBuiltinEffects(app, updateRoute, () => router.back(), () => slotValues, () => render());
|
|
659
734
|
lastAppliedThemeName = null;
|
|
660
735
|
applyThemeDefaults(app);
|
|
661
736
|
lastAppliedThemeName = app.live?.[app.themeName ?? ""] ?? app.themeName ?? null;
|
|
662
737
|
if (app.routes && app.routes.length > 0) {
|
|
663
|
-
for (const r of app.routes) if ("redirectTo" in r && matchPattern(r.pattern,
|
|
664
|
-
|
|
738
|
+
for (const r of app.routes) if ("redirectTo" in r && matchPattern(r.pattern, router.read().pathname)) {
|
|
739
|
+
router.replace(r.redirectTo);
|
|
665
740
|
break;
|
|
666
741
|
}
|
|
667
|
-
slotValues.route = parseLocation(app.routes,
|
|
668
|
-
|
|
742
|
+
slotValues.route = parseLocation(app.routes, router.read());
|
|
743
|
+
routerUnsub = router.subscribe(syncRouteFromLocation);
|
|
669
744
|
}
|
|
670
745
|
app._rerender = render;
|
|
671
746
|
app._dispatch = (reducerName, el) => {
|
|
@@ -702,6 +777,7 @@ function mount(app, target, options = {}) {
|
|
|
702
777
|
for (const h of anonTimers) clearInterval(h);
|
|
703
778
|
for (const h of namedTimers.values()) clearInterval(h);
|
|
704
779
|
namedTimers.clear();
|
|
780
|
+
routerUnsub?.();
|
|
705
781
|
target.replaceChildren();
|
|
706
782
|
dispatcher.dispose();
|
|
707
783
|
} };
|
|
@@ -787,7 +863,7 @@ function makeEffectDispatcher(app, caps, onResult) {
|
|
|
787
863
|
}
|
|
788
864
|
};
|
|
789
865
|
}
|
|
790
|
-
function registerBuiltinEffects(app, navigate, getLive, rerender) {
|
|
866
|
+
function registerBuiltinEffects(app, navigate, back, getLive, rerender) {
|
|
791
867
|
const overridable = (cap, fn) => {
|
|
792
868
|
return async (input, caps) => {
|
|
793
869
|
const p = caps.provider(cap);
|
|
@@ -821,7 +897,7 @@ function registerBuiltinEffects(app, navigate, getLive, rerender) {
|
|
|
821
897
|
name: "navigate-back",
|
|
822
898
|
cap: "nav.back",
|
|
823
899
|
invoke: overridable("nav.back", async () => {
|
|
824
|
-
|
|
900
|
+
back();
|
|
825
901
|
return {
|
|
826
902
|
kind: "ok",
|
|
827
903
|
value: null
|
|
@@ -922,6 +998,19 @@ function reportPanic(where, e) {
|
|
|
922
998
|
const { message } = panicInfo(e);
|
|
923
999
|
console.error(`[kumiki] ${isPanic(e) ? "panic" : "error"} in ${where}: ${message}`);
|
|
924
1000
|
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Surface an effect `err` result that no `.err` reducer consumes (#37). A failed
|
|
1003
|
+
* capability must never fail silently — the storage-unavailable case (sandbox /
|
|
1004
|
+
* private mode) otherwise looks like the app does nothing. Reported via
|
|
1005
|
+
* console.error so the verification tiers (smoke / runScenario, which patch
|
|
1006
|
+
* console.error) flag it, consistent with the v0.3 panic model. Production noise
|
|
1007
|
+
* is the app's own choice: wire an `.err` reducer to handle (or deliberately
|
|
1008
|
+
* ignore) the error.
|
|
1009
|
+
*/
|
|
1010
|
+
function reportUnhandledEffectError(effect, value) {
|
|
1011
|
+
const message = value && typeof value === "object" && "message" in value ? String(value.message) : String(value);
|
|
1012
|
+
console.error(`[kumiki] effect "${effect}" returned an error with no .err reducer: ${message}`);
|
|
1013
|
+
}
|
|
925
1014
|
/** A minimal top-level fallback for a render panic with no enclosing boundary. */
|
|
926
1015
|
function renderPanicFallback(e) {
|
|
927
1016
|
const { message, location } = panicInfo(e);
|
package/package.json
CHANGED