@moku-labs/web 1.17.0 → 2.0.1

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.
@@ -29,17 +29,17 @@ function renderVNode(vnode, region) {
29
29
  (0, preact.render)(vnode, region);
30
30
  }
31
31
  /**
32
- * Commit a component's VNode into its host as a DIFF — the in-interaction render
32
+ * Commit an island's VNode into its host as a DIFF — the in-interaction render
33
33
  * path. Unlike {@link renderVNode} (which RESETS the region: unmount → clear → mount,
34
34
  * correct for a navigation swap of static SSR markup), this is a plain Preact
35
35
  * `render(vnode, host)` that diffs against Preact's retained vdom, preserving focus,
36
36
  * scroll, and uncontrolled input state across re-renders triggered by `ctx.set`.
37
37
  *
38
- * Reached ONLY through the lazy `await import("./render")` gate (the component render
39
- * scheduler in `components.ts`), so an app whose islands never return a VNode never
38
+ * Reached ONLY through the lazy `await import("./render")` gate (the island render
39
+ * scheduler in `islands.ts`), so an app whose islands never return a VNode never
40
40
  * pulls Preact's `render` into its main bundle.
41
41
  *
42
- * @param vnode - The VNode produced by a component's `render(state, ctx)`.
42
+ * @param vnode - The VNode produced by an island's `render(state, ctx)`.
43
43
  * @param host - The island's host element to render into.
44
44
  * @example
45
45
  * ```ts
@@ -29,17 +29,17 @@ function renderVNode(vnode, region) {
29
29
  render(vnode, region);
30
30
  }
31
31
  /**
32
- * Commit a component's VNode into its host as a DIFF — the in-interaction render
32
+ * Commit an island's VNode into its host as a DIFF — the in-interaction render
33
33
  * path. Unlike {@link renderVNode} (which RESETS the region: unmount → clear → mount,
34
34
  * correct for a navigation swap of static SSR markup), this is a plain Preact
35
35
  * `render(vnode, host)` that diffs against Preact's retained vdom, preserving focus,
36
36
  * scroll, and uncontrolled input state across re-renders triggered by `ctx.set`.
37
37
  *
38
- * Reached ONLY through the lazy `await import("./render")` gate (the component render
39
- * scheduler in `components.ts`), so an app whose islands never return a VNode never
38
+ * Reached ONLY through the lazy `await import("./render")` gate (the island render
39
+ * scheduler in `islands.ts`), so an app whose islands never return a VNode never
40
40
  * pulls Preact's `render` into its main bundle.
41
41
  *
42
- * @param vnode - The VNode produced by a component's `render(state, ctx)`.
42
+ * @param vnode - The VNode produced by an island's `render(state, ctx)`.
43
43
  * @param host - The island's host element to render into.
44
44
  * @example
45
45
  * ```ts
@@ -1,6 +1,6 @@
1
1
  //#region src/plugins/spa/types.d.ts
2
2
  /**
3
- * What a component's `render` may return:
3
+ * What a island's `render` may return:
4
4
  * - a Preact `VNode` — committed into the host through the lazy Preact gate (`commitVNode`);
5
5
  * - a `Node` — replaces the host's children;
6
6
  * - a `string` — set as the host's `innerHTML`;
@@ -12,30 +12,33 @@ type RenderResult = AnyVNode | Node | string | void;
12
12
  * `VNode<SomeProps>`; the props generic is invariant under `exactOptionalPropertyTypes`,
13
13
  * so the only supertype that accepts every concrete `VNode<P>` is `VNode<any>`.
14
14
  */
15
+ /**
16
+ *
17
+ */
15
18
  type AnyVNode = import("preact").VNode<any>;
16
19
  /**
17
- * Factory that builds a component's typed per-instance state (mirrors a plugin's
20
+ * Factory that builds a island's typed per-instance state (mirrors a plugin's
18
21
  * `createState`). Called ONCE at mount; the returned object is stored on the
19
- * {@link ComponentInstance} and exposed read-only as `ctx.state`.
22
+ * {@link IslandInstance} and exposed read-only as `ctx.state`.
20
23
  *
21
- * @param ctx - The component context for this instance (state is not yet set).
24
+ * @param ctx - The island context for this instance (state is not yet set).
22
25
  * @returns The initial per-instance state.
23
26
  * @example
24
27
  * state: (ctx): BoardState => ({ boardId: ctx.params.id ?? "", cards: [] })
25
28
  */
26
- type ComponentStateFactory<S extends object> = (ctx: ComponentContext<S>) => S;
29
+ type IslandStateFactory<S extends object> = (ctx: IslandContext<S>) => S;
27
30
  /**
28
31
  * Pure render of `(state, ctx)` → {@link RenderResult}. Called after mount-state-init
29
32
  * and again (microtask-batched) after every `ctx.set`. Must be free of side effects
30
33
  * beyond producing its result.
31
34
  *
32
35
  * @param state - The current per-instance state (read-only).
33
- * @param ctx - The component context for this instance.
36
+ * @param ctx - The island context for this instance.
34
37
  * @returns The render result to commit into the host.
35
38
  * @example
36
39
  * render: (state) => h(BoardView, { snapshot: state.snapshot })
37
40
  */
38
- type ComponentRender<S extends object> = (state: Readonly<S>, ctx: ComponentContext<S>) => RenderResult;
41
+ type IslandRender<S extends object> = (state: Readonly<S>, ctx: IslandContext<S>) => RenderResult;
39
42
  /**
40
43
  * A delegated DOM event handler. `target` is the element matched by the key's selector
41
44
  * (already resolved via `closest` — no `instanceof`/`closest` ceremony in the body).
@@ -43,14 +46,14 @@ type ComponentRender<S extends object> = (state: Readonly<S>, ctx: ComponentCont
43
46
  * Typed `void` for ergonomics (the void-return rule accepts async handlers returning
44
47
  * `Promise<void>` too); the kernel ignores any returned value.
45
48
  *
46
- * @param ctx - The component context (carries the live per-instance `state`).
49
+ * @param ctx - The island context (carries the live per-instance `state`).
47
50
  * @param event - The raw DOM event.
48
51
  * @param target - The element matched by the selector (the host when no selector).
49
52
  * @returns void (a returned promise is ignored by the kernel).
50
53
  * @example
51
54
  * (ctx, event, button) => { event.preventDefault(); ctx.set({ open: true }); }
52
55
  */
53
- type ComponentEventHandler<S extends object> = (ctx: ComponentContext<S>, event: Event, target: Element) => void;
56
+ type IslandEventHandler<S extends object> = (ctx: IslandContext<S>, event: Event, target: Element) => void;
54
57
  /**
55
58
  * Declarative delegated event map. Each key is `"<type> <selector>"` (the selector is
56
59
  * optional → a host-level listener). ONE real listener per event TYPE is attached to
@@ -63,9 +66,9 @@ type ComponentEventHandler<S extends object> = (ctx: ComponentContext<S>, event:
63
66
  * "submit [data-add]": (ctx, e) => { e.preventDefault(); add(ctx); }
64
67
  * }
65
68
  */
66
- type ComponentEvents<S extends object> = Record<string, ComponentEventHandler<S>>;
69
+ type IslandEvents<S extends object> = Record<string, IslandEventHandler<S>>;
67
70
  /**
68
- * Context handed to every component lifecycle hook, render, and event handler — the
71
+ * Context handed to every island lifecycle hook, render, and event handler — the
69
72
  * bound element + page data, plus the matched route's `params`/`meta`/`locale` and a
70
73
  * link builder, so an island can read its route context (e.g. a `card` route's
71
74
  * `ctx.meta.focus` + `ctx.params.id`) directly, without the page bridging it through
@@ -73,11 +76,11 @@ type ComponentEvents<S extends object> = Record<string, ComponentEventHandler<S>
73
76
  *
74
77
  * Generic over the per-instance state `S` (default `undefined` so every existing
75
78
  * hooks-only island still type-checks). The additive members (`state`/`set`/`flush`/
76
- * `cleanup`/`component`) are ALWAYS-PRESENT functions — never optional keys — so they
79
+ * `cleanup`/`island`) are ALWAYS-PRESENT functions — never optional keys — so they
77
80
  * never trip `exactOptionalPropertyTypes`.
78
81
  */
79
- interface ComponentContext<S = undefined> {
80
- /** The element the component instance is bound to. */
82
+ interface IslandContext<S = undefined> {
83
+ /** The element the island instance is bound to. */
81
84
  el: Element;
82
85
  /** Page data extracted from the `script#__DATA__` payload. */
83
86
  data: PageData;
@@ -126,107 +129,107 @@ interface ComponentContext<S = undefined> {
126
129
  * Resolve another island's registered `api` by name. Returns `undefined` when no
127
130
  * provider is registered (optional-dependency semantics, mirroring `ctx.has`).
128
131
  *
129
- * @param name - The provider island's component name.
132
+ * @param name - The provider island's island name.
130
133
  * @returns The provider's api, or `undefined`.
131
134
  * @example
132
- * ctx.component<LightboxApi>("lightbox")?.open(slides, index);
135
+ * ctx.island<LightboxApi>("lightbox")?.open(slides, index);
133
136
  */
134
- component<T = unknown>(name: string): T | undefined;
137
+ island<T = unknown>(name: string): T | undefined;
135
138
  }
136
- /** Lifecycle hooks a component may implement. Generic over the per-instance state `S`. */
137
- interface ComponentHooks<S = undefined> {
139
+ /** Lifecycle hooks a island may implement. Generic over the per-instance state `S`. */
140
+ interface IslandHooks<S = undefined> {
138
141
  /**
139
142
  * Called once when the instance is created (before DOM attach).
140
143
  *
141
- * @param ctx - The component context for this instance.
144
+ * @param ctx - The island context for this instance.
142
145
  * @returns void
143
146
  * @example
144
147
  * onCreate({ el }) { el.dataset.ready = "1"; }
145
148
  */
146
- onCreate?(ctx: ComponentContext<S>): void;
149
+ onCreate?(ctx: IslandContext<S>): void;
147
150
  /**
148
151
  * Called after the instance is attached to its element.
149
152
  *
150
- * @param ctx - The component context for this instance.
153
+ * @param ctx - The island context for this instance.
151
154
  * @returns void
152
155
  * @example
153
156
  * onMount({ el }) { el.textContent = "0"; }
154
157
  * @example
155
158
  * async onMount(ctx) { ctx.set({ items: await load() }); } // async is allowed; the harness awaits it via settle()
156
159
  */
157
- onMount?(ctx: ComponentContext<S>): void;
160
+ onMount?(ctx: IslandContext<S>): void;
158
161
  /**
159
162
  * Called when a navigation begins while this instance is mounted.
160
163
  *
161
- * @param ctx - The component context for this instance.
164
+ * @param ctx - The island context for this instance.
162
165
  * @returns void
163
166
  * @example
164
167
  * onNavStart({ el }) { el.dataset.loading = ""; }
165
168
  */
166
- onNavStart?(ctx: ComponentContext<S>): void;
169
+ onNavStart?(ctx: IslandContext<S>): void;
167
170
  /**
168
171
  * Called when a navigation completes while this instance is mounted.
169
172
  *
170
- * @param ctx - The component context for this instance.
173
+ * @param ctx - The island context for this instance.
171
174
  * @returns void
172
175
  * @example
173
176
  * onNavEnd({ el }) { delete el.dataset.loading; }
174
177
  */
175
- onNavEnd?(ctx: ComponentContext<S>): void;
178
+ onNavEnd?(ctx: IslandContext<S>): void;
176
179
  /**
177
180
  * Called before the instance is detached from its element.
178
181
  *
179
- * @param ctx - The component context for this instance.
182
+ * @param ctx - The island context for this instance.
180
183
  * @returns void
181
184
  * @example
182
185
  * onUnMount({ el }) { el.replaceChildren(); }
183
186
  */
184
- onUnMount?(ctx: ComponentContext<S>): void;
187
+ onUnMount?(ctx: IslandContext<S>): void;
185
188
  /**
186
189
  * Called once when the instance is destroyed (after detach).
187
190
  *
188
- * @param ctx - The component context for this instance.
191
+ * @param ctx - The island context for this instance.
189
192
  * @returns void
190
193
  * @example
191
194
  * onDestroy({ el }) { delete el.dataset.ready; }
192
195
  */
193
- onDestroy?(ctx: ComponentContext<S>): void;
196
+ onDestroy?(ctx: IslandContext<S>): void;
194
197
  }
195
198
  /**
196
- * The spec extras carried on a {@link ComponentDef}, type-erased to `object` state
197
- * (authors keep full `S` inference at the `createComponent` call site; the registry
199
+ * The spec extras carried on a {@link IslandDef}, type-erased to `object` state
200
+ * (authors keep full `S` inference at the `createIsland` call site; the registry
198
201
  * stores the runtime-only erased form). Absent for legacy `(name, hooks)` defs.
199
202
  */
200
- interface ComponentSpecExtras {
203
+ interface IslandSpecExtras {
201
204
  /** Per-instance state factory. */
202
- state?: ComponentStateFactory<object>;
205
+ state?: IslandStateFactory<object>;
203
206
  /** Render called on mount + after every `ctx.set`. */
204
- render?: ComponentRender<object>;
207
+ render?: IslandRender<object>;
205
208
  /** Declarative delegated events. */
206
- events?: ComponentEvents<object>;
207
- /** Public api factory registered under the component name. */
208
- api?: (ctx: ComponentContext<object>) => unknown;
209
+ events?: IslandEvents<object>;
210
+ /** Public api factory registered under the island name. */
211
+ api?: (ctx: IslandContext<object>) => unknown;
209
212
  }
210
- /** A registered component definition (an opaque token; author inference lives on `createComponent`). */
211
- interface ComponentDef {
212
- /** Unique component name (matched against `data-component`). */
213
+ /** A registered island definition (an opaque token; author inference lives on `createIsland`). */
214
+ interface IslandDef {
215
+ /** Unique island name (matched against `data-island`). */
213
216
  name: string;
214
217
  /** Lifecycle hooks (the subset shared with the legacy form). */
215
- hooks: ComponentHooks<object>;
218
+ hooks: IslandHooks<object>;
216
219
  /** Plugin-mirror extras (state/render/events/api). Absent for legacy `(name, hooks)` defs. */
217
- spec?: ComponentSpecExtras;
220
+ spec?: IslandSpecExtras;
218
221
  }
219
222
  /** The matched-route slice carried on a live instance (params/meta/locale + link builder). */
220
- type ComponentRouteSlice = Pick<ComponentContext, "params" | "meta" | "locale" | "url">;
223
+ type IslandRouteSlice = Pick<IslandContext, "params" | "meta" | "locale" | "url">;
221
224
  /** Page data payload parsed from the inline `script#__DATA__` element. */
222
225
  type PageData = Record<string, unknown>;
223
226
  //#endregion
224
- //#region src/plugins/spa/components.d.ts
225
- /** The matched-route slice merged onto the component context (params/meta/locale + link builder). */
226
- type RouteSlice = ComponentRouteSlice;
227
+ //#region src/plugins/spa/islands.d.ts
228
+ /** The matched-route slice merged onto the island context (params/meta/locale + link builder). */
229
+ type RouteSlice = IslandRouteSlice;
227
230
  //#endregion
228
231
  //#region src/testing.d.ts
229
- /** One captured spa emit (the kernel's `spa:component-mount` / `-unmount`). */
232
+ /** One captured spa emit (the kernel's `spa:island-mount` / `-unmount`). */
230
233
  interface CapturedEmit {
231
234
  /** The event name. */
232
235
  readonly event: string;
@@ -301,7 +304,7 @@ interface IslandHandle<S extends object = object, A = unknown> {
301
304
  * handle.unmount();
302
305
  */
303
306
  unmount(): void;
304
- /** Captured `spa:component-mount` / `-unmount` emits, in order. */
307
+ /** Captured `spa:island-mount` / `-unmount` emits, in order. */
305
308
  readonly emitted: ReadonlyArray<CapturedEmit>;
306
309
  }
307
310
  /** Options for {@link mountIsland}. All optional; sensible headless defaults. */
@@ -322,14 +325,14 @@ interface MountIslandOptions {
322
325
  url?: (name: string, params?: Record<string, string>) => string;
323
326
  /** Mount OUTSIDE the swap area so the instance is persistent (gets `onNavEnd`). */
324
327
  persistent?: boolean;
325
- /** Stubbed sibling-island apis resolved by `ctx.component(name)`. */
326
- components?: Record<string, unknown>;
328
+ /** Stubbed sibling-island apis resolved by `ctx.island(name)`. */
329
+ islands?: Record<string, unknown>;
327
330
  }
328
331
  /**
329
332
  * Mount ONE island headlessly through the REAL spa kernel internals under a DOM. The
330
333
  * unit + light-integration tier: no `createApp`, no router, no network.
331
334
  *
332
- * @param definition - The component definition under test (from `createComponent`).
335
+ * @param definition - The island definition under test (from `createIsland`).
333
336
  * @param options - Host HTML/element, route slice, page data, persistence, stub apis.
334
337
  * @returns A handle exposing the instance's `state`/`api` + event/nav/flush drivers.
335
338
  * @example
@@ -337,7 +340,7 @@ interface MountIslandOptions {
337
340
  * h.navEnd({ locale: "en" });
338
341
  * expect(h.el.querySelector("[aria-current]")).toBeTruthy();
339
342
  */
340
- declare function mountIsland<S extends object = object, A = unknown>(definition: ComponentDef, options?: MountIslandOptions): IslandHandle<S, A>;
343
+ declare function mountIsland<S extends object = object, A = unknown>(definition: IslandDef, options?: MountIslandOptions): IslandHandle<S, A>;
341
344
  /** The result of {@link renderIsland} — a host plus query/flush/teardown helpers. */
342
345
  interface RenderIslandResult {
343
346
  /** The host element the view rendered into. */
@@ -381,9 +384,9 @@ interface RenderIslandResult {
381
384
  * const r = renderIsland(render, { state: { boards: [{ id: "1", title: "Alpha" }] } });
382
385
  * expect(r.find("[data-board]")).toBeTruthy();
383
386
  */
384
- declare function renderIsland<S extends object>(render: ComponentRender<S>, input: {
387
+ declare function renderIsland<S extends object>(render: IslandRender<S>, input: {
385
388
  state: S;
386
- ctx?: Partial<ComponentContext<S>>;
389
+ ctx?: Partial<IslandContext<S>>;
387
390
  }): RenderIslandResult;
388
391
  //#endregion
389
392
  export { CapturedEmit, IslandHandle, MountIslandOptions, RenderIslandResult, mountIsland, renderIsland };
package/dist/testing.mjs CHANGED
@@ -2,7 +2,7 @@ import { render } from "preact";
2
2
  import { act } from "preact/test-utils";
3
3
  //#region src/plugins/spa/types.ts
4
4
  /** Allowed hook names — single source of truth for fail-fast validation. */
5
- const COMPONENT_HOOK_NAMES = [
5
+ const ISLAND_HOOK_NAMES = [
6
6
  "onCreate",
7
7
  "onMount",
8
8
  "onNavStart",
@@ -11,17 +11,17 @@ const COMPONENT_HOOK_NAMES = [
11
11
  "onDestroy"
12
12
  ];
13
13
  //#endregion
14
- //#region src/plugins/spa/components.ts
14
+ //#region src/plugins/spa/islands.ts
15
15
  /**
16
- * @file spa plugin — component lifecycle, mounting, the plugin-mirror authoring
17
- * surface (`createComponent` with a typed `{ state, render, events, api }` spec),
16
+ * @file spa plugin — island lifecycle, mounting, the plugin-mirror authoring
17
+ * surface (`createIsland` with a typed `{ state, render, events, api }` spec),
18
18
  * the per-instance state + microtask-batched render scheduler, declarative
19
19
  * delegated events, and the cross-island api registry.
20
20
  * @see README.md
21
21
  */
22
22
  /** Error prefix for spa fail-fast failures (spec/11 Part-3). */
23
23
  const ERROR_PREFIX = "[web]";
24
- new Set(COMPONENT_HOOK_NAMES);
24
+ new Set(ISLAND_HOOK_NAMES);
25
25
  /** Synchronous re-entrancy cap for the render scheduler (a render that calls `ctx.flush`). */
26
26
  const MAX_RENDER_DEPTH = 25;
27
27
  /**
@@ -54,7 +54,7 @@ let renderChunk;
54
54
  let commitVNodeFunction;
55
55
  /**
56
56
  * Load the lazy `./render` chunk (once) and cache its `commitVNode` for synchronous
57
- * use by later renders. Awaited by a component's `mountPromise` so the test harness's
57
+ * use by later renders. Awaited by a island's `mountPromise` so the test harness's
58
58
  * `settle()` can deterministically flush a VNode render.
59
59
  *
60
60
  * @returns A promise that resolves once `commitVNode` is available.
@@ -62,7 +62,7 @@ let commitVNodeFunction;
62
62
  * await loadRenderChunk();
63
63
  */
64
64
  async function loadRenderChunk() {
65
- renderChunk ??= import("./render-UO4nimWr.mjs");
65
+ renderChunk ??= import("./render-yXHc9BWI.mjs");
66
66
  commitVNodeFunction = (await renderChunk).commitVNode;
67
67
  }
68
68
  /**
@@ -71,7 +71,7 @@ async function loadRenderChunk() {
71
71
  * a Preact `VNode` → committed through the lazy gate (loading it on demand if needed).
72
72
  *
73
73
  * @param host - The island host element to render into.
74
- * @param result - The value returned by the component's `render`.
74
+ * @param result - The value returned by the island's `render`.
75
75
  * @example
76
76
  * commitResult(host, h(View, { items }));
77
77
  */
@@ -93,7 +93,7 @@ function commitResult(host, result) {
93
93
  loadRenderChunk().then(() => commitVNodeFunction?.(vnode, host)).catch(() => {});
94
94
  }
95
95
  /**
96
- * Run a component's `render(state, ctx)` and commit the result now. Guards against
96
+ * Run a island's `render(state, ctx)` and commit the result now. Guards against
97
97
  * synchronous re-entrancy (a render that calls `ctx.flush`) with a depth cap.
98
98
  *
99
99
  * @param instance - The instance to render.
@@ -104,7 +104,7 @@ function commitResult(host, result) {
104
104
  function runRender(instance) {
105
105
  const render = instance.def.spec?.render;
106
106
  if (!render) return;
107
- if (instance.renderDepth > MAX_RENDER_DEPTH) throw new Error(`${ERROR_PREFIX} component "${instance.def.name}" render re-entered ${MAX_RENDER_DEPTH}+ times\n → a render must not synchronously trigger its own render (avoid ctx.flush() inside render)`);
107
+ if (instance.renderDepth > MAX_RENDER_DEPTH) throw new Error(`${ERROR_PREFIX} island "${instance.def.name}" render re-entered ${MAX_RENDER_DEPTH}+ times\n → a render must not synchronously trigger its own render (avoid ctx.flush() inside render)`);
108
108
  instance.renderDepth += 1;
109
109
  try {
110
110
  commitResult(instance.el, render(instance.state ?? {}, instance.ctx));
@@ -130,12 +130,12 @@ function scheduleRender(instance) {
130
130
  });
131
131
  }
132
132
  /**
133
- * Build the single per-instance {@link ComponentContext} reused by every hook, event
133
+ * Build the single per-instance {@link IslandContext} reused by every hook, event
134
134
  * handler, and render. Route fields (`params`/`meta`/`locale`/`url`) and `data` read
135
135
  * through the instance so a navigation update is reflected without rebuilding the ctx;
136
- * `state`/`set`/`flush`/`cleanup`/`component` are bound to the instance + plugin state.
136
+ * `state`/`set`/`flush`/`cleanup`/`island` are bound to the instance + plugin state.
137
137
  *
138
- * @param state - The plugin state (for the cross-island `component` resolver).
138
+ * @param state - The plugin state (for the cross-island `island` resolver).
139
139
  * @param instance - The instance the context is bound to.
140
140
  * @returns The instance-bound context.
141
141
  * @example
@@ -239,13 +239,13 @@ function buildContext(state, instance) {
239
239
  /**
240
240
  * Resolve another island's registered api by name (`undefined` when absent).
241
241
  *
242
- * @param name - The provider island's component name.
242
+ * @param name - The provider island's island name.
243
243
  * @returns The provider's api, or `undefined`.
244
244
  * @example
245
- * ctx.component("lightbox");
245
+ * ctx.island("lightbox");
246
246
  */
247
- component(name) {
248
- return state.componentApis.get(name);
247
+ island(name) {
248
+ return state.islandApis.get(name);
249
249
  }
250
250
  };
251
251
  }
@@ -269,7 +269,7 @@ function matchTarget(host, event, selector) {
269
269
  return matched && host.contains(matched) ? matched : void 0;
270
270
  }
271
271
  /**
272
- * Attach a component's declarative `events` map: one real listener per event TYPE on
272
+ * Attach a island's declarative `events` map: one real listener per event TYPE on
273
273
  * the host (dispatch walks `closest(selector)` for each registered selector), each
274
274
  * removed via the instance's cleanup registry on destroy.
275
275
  *
@@ -286,7 +286,7 @@ function attachEvents(instance, events) {
286
286
  const space = key.indexOf(" ");
287
287
  const type = (space === -1 ? key : key.slice(0, space)).trim();
288
288
  const selector = space === -1 ? "" : key.slice(space + 1).trim();
289
- if (type === "") throw new Error(`${ERROR_PREFIX} component "${instance.def.name}" event key must start with an event type: "${key}"\n → use "<type>" or "<type> <selector>" (e.g. "click [data-action]")`);
289
+ if (type === "") throw new Error(`${ERROR_PREFIX} island "${instance.def.name}" event key must start with an event type: "${key}"\n → use "<type>" or "<type> <selector>" (e.g. "click [data-action]")`);
290
290
  const list = byType.get(type) ?? [];
291
291
  list.push({
292
292
  selector,
@@ -360,30 +360,30 @@ function disposeInstance(state, instance) {
360
360
  } catch {}
361
361
  instance.cleanups.length = 0;
362
362
  instance.renderScheduled = false;
363
- if (instance.api !== void 0 && state.componentApis.get(instance.def.name) === instance.api) state.componentApis.delete(instance.def.name);
363
+ if (instance.api !== void 0 && state.islandApis.get(instance.def.name) === instance.api) state.islandApis.delete(instance.def.name);
364
364
  }
365
365
  /**
366
- * Mounts a single `data-component` element: classifies persistent vs page-specific,
366
+ * Mounts a single `data-island` element: classifies persistent vs page-specific,
367
367
  * builds the instance + its bound context, initializes per-instance `state`, registers
368
368
  * its `api`, attaches declarative `events`, fires `onCreate` then `onMount` (capturing
369
369
  * an async `onMount` + render-chunk load as `mountPromise`), schedules the initial
370
- * render, records it, and emits `spa:component-mount`. No-ops if the element is already
371
- * mounted, has no component name, or names an unregistered component.
370
+ * render, records it, and emits `spa:island-mount`. No-ops if the element is already
371
+ * mounted, has no island name, or names an unregistered island.
372
372
  *
373
- * @param state - The plugin state (registeredComponents + instances + componentApis).
374
- * @param emit - The event emitter for spa:component-mount.
373
+ * @param state - The plugin state (registeredIslands + instances + islandApis).
374
+ * @param emit - The event emitter for spa:island-mount.
375
375
  * @param swapArea - The swap-region element, or null when none was found.
376
376
  * @param data - The current page data payload.
377
- * @param element - The candidate element carrying a `data-component` attribute.
377
+ * @param element - The candidate element carrying a `data-island` attribute.
378
378
  * @param route - The matched-route slice for the current URL (params/meta/locale/url).
379
379
  * @example
380
380
  * mountElement(state, emit, swapArea, data, element, route);
381
381
  */
382
382
  function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE) {
383
383
  if (state.instances.has(element)) return;
384
- const name = element.dataset.component;
384
+ const name = element.dataset.island;
385
385
  if (!name) return;
386
- const definition = state.registeredComponents.get(name);
386
+ const definition = state.registeredIslands.get(name);
387
387
  if (!definition) return;
388
388
  const instance = {
389
389
  def: definition,
@@ -409,7 +409,7 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
409
409
  if (spec?.state) instance.state = spec.state(instance.ctx);
410
410
  if (spec?.api) {
411
411
  instance.api = spec.api(instance.ctx);
412
- state.componentApis.set(definition.name, instance.api);
412
+ state.islandApis.set(definition.name, instance.api);
413
413
  }
414
414
  if (spec?.events) attachEvents(instance, spec.events);
415
415
  runHook(instance, "onCreate");
@@ -420,20 +420,20 @@ function mountElement(state, emit, swapArea, data, element, route = EMPTY_ROUTE)
420
420
  if (onMountResult && typeof onMountResult.then === "function") pending.push(onMountResult);
421
421
  instance.mountPromise = pending.length > 0 ? Promise.all(pending).then(() => {}) : void 0;
422
422
  state.instances.set(element, instance);
423
- emit("spa:component-mount", {
423
+ emit("spa:island-mount", {
424
424
  name: definition.name,
425
425
  el: element
426
426
  });
427
427
  }
428
428
  /**
429
- * Scans the swap region, mounts components for matching `data-component` elements,
429
+ * Scans the swap region, mounts islands for matching `data-island` elements,
430
430
  * classifies persistent (outside swap area) vs page-specific (inside), runs
431
- * `onCreate`/`onMount` + initial render, and emits `spa:component-mount` per instance.
431
+ * `onCreate`/`onMount` + initial render, and emits `spa:island-mount` per instance.
432
432
  * Already-mounted elements are skipped.
433
433
  *
434
- * @param state - The plugin state (registeredComponents + instances + componentApis).
435
- * @param emit - The event emitter for spa:component-mount.
436
- * @param swapSelector - CSS selector bounding page-specific components.
434
+ * @param state - The plugin state (registeredIslands + instances + islandApis).
435
+ * @param emit - The event emitter for spa:island-mount.
436
+ * @param swapSelector - CSS selector bounding page-specific islands.
437
437
  * @param route - The matched-route slice for the current URL (params/meta/locale/url).
438
438
  * @example
439
439
  * scanAndMount(state, emit, "main > section", route);
@@ -442,16 +442,16 @@ function scanAndMount(state, emit, swapSelector, route = EMPTY_ROUTE) {
442
442
  if (typeof document === "undefined") return;
443
443
  const swapArea = document.querySelector(swapSelector);
444
444
  const data = extractPageData(document);
445
- for (const element of document.querySelectorAll("[data-component]")) mountElement(state, emit, swapArea, data, element, route);
445
+ for (const element of document.querySelectorAll("[data-island]")) mountElement(state, emit, swapArea, data, element, route);
446
446
  }
447
447
  /**
448
448
  * Unmounts page-specific instances inside the swap region (runs `onUnMount` then
449
449
  * `onDestroy`, then their cleanup disposers + api unregister), removes them from state,
450
- * and emits `spa:component-unmount`. Persistent instances (outside the swap area) are
450
+ * and emits `spa:island-unmount`. Persistent instances (outside the swap area) are
451
451
  * left in place.
452
452
  *
453
453
  * @param state - The plugin state holding live instances.
454
- * @param emit - The event emitter for spa:component-unmount.
454
+ * @param emit - The event emitter for spa:island-unmount.
455
455
  * @example
456
456
  * unmountPageSpecific(state, emit);
457
457
  */
@@ -464,7 +464,7 @@ function unmountPageSpecific(state, emit) {
464
464
  runHook(instance, "onDestroy");
465
465
  disposeInstance(state, instance);
466
466
  state.instances.delete(element);
467
- emit("spa:component-unmount", {
467
+ emit("spa:island-unmount", {
468
468
  name: instance.def.name,
469
469
  el: element
470
470
  });
@@ -473,11 +473,11 @@ function unmountPageSpecific(state, emit) {
473
473
  /**
474
474
  * Disposes ALL live instances (persistent and page-specific) on teardown: runs
475
475
  * `onUnMount` then `onDestroy`, then their cleanup disposers + api unregister, emits
476
- * `spa:component-unmount`, and clears the instance + api maps. Used by the kernel's
476
+ * `spa:island-unmount`, and clears the instance + api maps. Used by the kernel's
477
477
  * `dispose` on plugin stop.
478
478
  *
479
479
  * @param state - The plugin state holding live instances.
480
- * @param emit - The event emitter for spa:component-unmount.
480
+ * @param emit - The event emitter for spa:island-unmount.
481
481
  * @example
482
482
  * unmountAll(state, emit);
483
483
  */
@@ -488,13 +488,13 @@ function unmountAll(state, emit) {
488
488
  runHook(instance, "onUnMount");
489
489
  runHook(instance, "onDestroy");
490
490
  disposeInstance(state, instance);
491
- emit("spa:component-unmount", {
491
+ emit("spa:island-unmount", {
492
492
  name: instance.def.name,
493
493
  el: element
494
494
  });
495
495
  }
496
496
  state.instances.clear();
497
- state.componentApis.clear();
497
+ state.islandApis.clear();
498
498
  }
499
499
  /**
500
500
  * Fires `onNavStart` on every currently-mounted instance (persistent instances
@@ -546,9 +546,9 @@ function notifyNavEnd(state, route = EMPTY_ROUTE) {
546
546
  */
547
547
  function createState(_ctx) {
548
548
  return {
549
- registeredComponents: /* @__PURE__ */ new Map(),
549
+ registeredIslands: /* @__PURE__ */ new Map(),
550
550
  instances: /* @__PURE__ */ new Map(),
551
- componentApis: /* @__PURE__ */ new Map(),
551
+ islandApis: /* @__PURE__ */ new Map(),
552
552
  currentUrl: "",
553
553
  destroyRouter: null,
554
554
  started: false,
@@ -597,7 +597,7 @@ function parseEventSpec(spec) {
597
597
  * Mount ONE island headlessly through the REAL spa kernel internals under a DOM. The
598
598
  * unit + light-integration tier: no `createApp`, no router, no network.
599
599
  *
600
- * @param definition - The component definition under test (from `createComponent`).
600
+ * @param definition - The island definition under test (from `createIsland`).
601
601
  * @param options - Host HTML/element, route slice, page data, persistence, stub apis.
602
602
  * @returns A handle exposing the instance's `state`/`api` + event/nav/flush drivers.
603
603
  * @example
@@ -610,10 +610,10 @@ function mountIsland(definition, options = {}) {
610
610
  global: {},
611
611
  config: {}
612
612
  });
613
- state.registeredComponents.set(definition.name, definition);
614
- if (options.components) for (const [name, api] of Object.entries(options.components)) state.componentApis.set(name, api);
613
+ state.registeredIslands.set(definition.name, definition);
614
+ if (options.islands) for (const [name, api] of Object.entries(options.islands)) state.islandApis.set(name, api);
615
615
  const host = options.el ?? document.createElement("div");
616
- host.dataset.component = definition.name;
616
+ host.dataset.island = definition.name;
617
617
  if (options.html !== void 0) host.innerHTML = options.html;
618
618
  const dataScript = options.data ? `<script id="__DATA__" type="application/json">${JSON.stringify(options.data)}<\/script>` : "";
619
619
  document.body.innerHTML = `<main><section id="__moku_swap"></section></main>${dataScript}`;
@@ -811,7 +811,7 @@ function renderIsland(render$1, input) {
811
811
  set: noopStub,
812
812
  flush: noopStub,
813
813
  cleanup: noopStub,
814
- component: noopStub,
814
+ island: noopStub,
815
815
  ...input.ctx
816
816
  };
817
817
  commit(host, render$1(input.state, ctx));
package/package.json CHANGED
@@ -64,8 +64,8 @@
64
64
  "bun": ">=1.3.14"
65
65
  },
66
66
  "dependencies": {
67
- "@moku-labs/common": "0.2.0",
68
- "@moku-labs/core": "0.1.4",
67
+ "@moku-labs/common": "0.2.1",
68
+ "@moku-labs/core": "1.5.0",
69
69
  "@resvg/resvg-js": "2.6.2",
70
70
  "@shikijs/rehype": "3.22.0",
71
71
  "feed": "5.2.0",
@@ -132,5 +132,5 @@
132
132
  "test:build-e2e": "bun test src/plugins/build/__tests__/e2e/",
133
133
  "test:coverage": "vitest run --project unit --project integration --coverage"
134
134
  },
135
- "version": "1.17.0"
135
+ "version": "2.0.1"
136
136
  }