ilha 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/README.md +36 -7
- package/dist/index.d.ts +33 -31
- package/dist/index.js +199 -74
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -289,23 +289,51 @@ ilha.css(styles).render(() => `<div class="card">…</div>`);
|
|
|
289
289
|
|
|
290
290
|
---
|
|
291
291
|
|
|
292
|
-
###
|
|
292
|
+
### Composing Islands
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
Child islands are interpolated directly inside a parent's html`` template. During SSR the child's HTML is rendered inline; during client mount the child is activated independently inside its own host element.
|
|
295
295
|
|
|
296
296
|
```ts
|
|
297
297
|
const Icon = ilha.render(() => `<svg>…</svg>`);
|
|
298
298
|
|
|
299
|
-
const Card = ilha.
|
|
300
|
-
(
|
|
299
|
+
const Card = ilha.render(
|
|
300
|
+
() => html`
|
|
301
301
|
<div class="card">
|
|
302
|
-
${
|
|
302
|
+
${Icon}
|
|
303
303
|
<p>Card content</p>
|
|
304
304
|
</div>
|
|
305
305
|
`,
|
|
306
306
|
);
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
+
**Passing props** — call the child island with a props object:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const Badge = ilha
|
|
313
|
+
.input(z.object({ label: z.string(), color: z.string().default("teal") }))
|
|
314
|
+
.render(({ input }) => html`<span style="background:${input.color}">${input.label}</span>`);
|
|
315
|
+
|
|
316
|
+
const Card = ilha.render(
|
|
317
|
+
() => html`
|
|
318
|
+
<div>
|
|
319
|
+
${Badge({ label: "New", color: "coral" })}
|
|
320
|
+
<p>Content</p>
|
|
321
|
+
</div>
|
|
322
|
+
`,
|
|
323
|
+
);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Keyed children** — use `.key()` when a child may reorder or appear conditionally. Keys must be unique within a parent render:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
const List = ilha.render(
|
|
330
|
+
() =>
|
|
331
|
+
html`<ul>
|
|
332
|
+
${items.map((item) => html`<li>${Item.key(item.id)({ name: item.name })}</li>`)}
|
|
333
|
+
</ul>`,
|
|
334
|
+
);
|
|
335
|
+
```
|
|
336
|
+
|
|
309
337
|
---
|
|
310
338
|
|
|
311
339
|
### `.transition(opts)`
|
|
@@ -331,7 +359,7 @@ The `leave` transition is awaited before cleanup runs.
|
|
|
331
359
|
|
|
332
360
|
### `.render(fn)`
|
|
333
361
|
|
|
334
|
-
Finalises the builder and returns an `Island`. The render function receives `{ state, derived, input
|
|
362
|
+
Finalises the builder and returns an `Island`. The render function receives `{ state, derived, input }` and must return a string or `RawHtml`.
|
|
335
363
|
|
|
336
364
|
```ts
|
|
337
365
|
const MyIsland = ilha.state("x", 1).render(({ state, input }) => html`<p>${state.x}</p>`);
|
|
@@ -471,6 +499,7 @@ Interpolation rules:
|
|
|
471
499
|
| `raw(str)` | Inserted as-is (no escaping) |
|
|
472
500
|
| `html\`…\`` | Inserted as-is (already safe) |
|
|
473
501
|
| Signal accessor | Called and escaped |
|
|
502
|
+
| Island / Island call | Emitted as `data-ilha-slot` host element |
|
|
474
503
|
| Array | Each item processed recursively (no commas) |
|
|
475
504
|
|
|
476
505
|
**List rendering pattern:**
|
|
@@ -581,7 +610,7 @@ import type {
|
|
|
581
610
|
IslandState,
|
|
582
611
|
IslandDerived,
|
|
583
612
|
DerivedValue,
|
|
584
|
-
|
|
613
|
+
KeyedIsland,
|
|
585
614
|
HydratableOptions,
|
|
586
615
|
OnMountContext,
|
|
587
616
|
HandlerContext,
|
package/dist/index.d.ts
CHANGED
|
@@ -31,16 +31,18 @@ declare namespace StandardSchemaV1 {
|
|
|
31
31
|
type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
|
|
32
32
|
}
|
|
33
33
|
declare const RAW: unique symbol;
|
|
34
|
-
declare const SLOT_ACCESSOR: unique symbol;
|
|
35
34
|
declare const SIGNAL_ACCESSOR: unique symbol;
|
|
35
|
+
declare const ISLAND: unique symbol;
|
|
36
|
+
declare const ISLAND_CALL: unique symbol;
|
|
36
37
|
interface RawHtml {
|
|
37
38
|
[RAW]: true;
|
|
38
39
|
value: string;
|
|
39
40
|
}
|
|
40
|
-
interface
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
interface IslandCall {
|
|
42
|
+
[ISLAND_CALL]: true;
|
|
43
|
+
island: AnyIsland;
|
|
44
|
+
props: Record<string, unknown> | undefined;
|
|
45
|
+
key: string | undefined;
|
|
44
46
|
}
|
|
45
47
|
interface MarkedSignalAccessor<T> {
|
|
46
48
|
(): T;
|
|
@@ -96,16 +98,18 @@ interface Island<TInput = Record<string, unknown>, _TStateMap extends Record<str
|
|
|
96
98
|
toString(props?: Partial<TInput>): string;
|
|
97
99
|
mount(host: Element, props?: Partial<TInput>): () => void;
|
|
98
100
|
hydratable(props: Partial<TInput>, options: HydratableOptions): Promise<string>;
|
|
101
|
+
key(key: string): KeyedIsland<TInput>;
|
|
102
|
+
[ISLAND]: true;
|
|
103
|
+
}
|
|
104
|
+
interface KeyedIsland<TInput> {
|
|
105
|
+
(props?: Partial<TInput>): IslandCall;
|
|
106
|
+
[ISLAND_CALL]: true;
|
|
99
107
|
}
|
|
100
|
-
type InferIslandInput<T> = T extends Island<infer TInput, any> ? TInput : Record<string, unknown>;
|
|
101
108
|
type AnyIsland = Island<any, any>;
|
|
102
|
-
type
|
|
103
|
-
type SlotsProxy<TSlots extends SlotMap> = { readonly [K in keyof TSlots]: SlotAccessor<Partial<InferIslandInput<TSlots[K]>>> };
|
|
104
|
-
type RenderContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>, TSlots extends SlotMap> = {
|
|
109
|
+
type RenderContext<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> = {
|
|
105
110
|
state: IslandState<TStateMap>;
|
|
106
111
|
derived: IslandDerived<TDerivedMap>;
|
|
107
112
|
input: TInput;
|
|
108
|
-
slots: SlotsProxy<TSlots>;
|
|
109
113
|
};
|
|
110
114
|
type EffectContext<TInput, TStateMap extends Record<string, unknown>> = {
|
|
111
115
|
state: IslandState<TStateMap>;
|
|
@@ -167,40 +171,38 @@ interface MountOptions {
|
|
|
167
171
|
interface MountResult {
|
|
168
172
|
unmount: () => void;
|
|
169
173
|
}
|
|
170
|
-
interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown
|
|
174
|
+
interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
|
|
171
175
|
schema: StandardSchemaV1 | null;
|
|
172
176
|
states: StateEntry<TInput>[];
|
|
173
177
|
deriveds: DerivedEntry<TInput, TStateMap>[];
|
|
174
178
|
ons: OnEntry<TInput, TStateMap, TDerivedMap>[];
|
|
175
179
|
effects: EffectEntry<TInput, TStateMap>[];
|
|
176
180
|
onMounts: OnMountEntry<TInput, TStateMap, TDerivedMap>[];
|
|
177
|
-
slots: Record<string, AnyIsland>;
|
|
178
181
|
transition: TransitionOptions | null;
|
|
179
182
|
binds: BindEntry<TStateMap>[];
|
|
180
183
|
css: string | null;
|
|
181
184
|
}
|
|
182
|
-
declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<string, never
|
|
183
|
-
readonly _cfg: BuilderConfig<TInput, TStateMap, TDerivedMap
|
|
184
|
-
constructor(cfg: BuilderConfig<TInput, TStateMap, TDerivedMap
|
|
185
|
-
input<T extends Record<string, unknown>>(): IlhaBuilder<T, Record<string, never>, Record<string, never
|
|
186
|
-
input<S extends StandardSchemaV1>(schema: S): IlhaBuilder<StandardSchemaV1.InferOutput<S> & Record<string, unknown>, Record<string, never>, Record<string, never
|
|
187
|
-
state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap
|
|
188
|
-
derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V
|
|
189
|
-
bind(selector: string, stateKey: keyof TStateMap & string): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
190
|
-
bind<T>(selector: string, externalSignal: ExternalSignal<T>): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
191
|
-
on<S extends OnSelectorString>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
192
|
-
on(selectorOrCombined: string, handler: (ctx: HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
193
|
-
effect(fn: (ctx: EffectContext<TInput, TStateMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
194
|
-
onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
render(fn: (ctx: RenderContext<TInput, TStateMap, TDerivedMap, TSlots>) => string | RawHtml): Island<TInput, TStateMap>;
|
|
185
|
+
declare class IlhaBuilder<TInput extends Record<string, unknown>, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown> = Record<string, never>> {
|
|
186
|
+
readonly _cfg: BuilderConfig<TInput, TStateMap, TDerivedMap>;
|
|
187
|
+
constructor(cfg: BuilderConfig<TInput, TStateMap, TDerivedMap>);
|
|
188
|
+
input<T extends Record<string, unknown>>(): IlhaBuilder<T, Record<string, never>, Record<string, never>>;
|
|
189
|
+
input<S extends StandardSchemaV1>(schema: S): IlhaBuilder<StandardSchemaV1.InferOutput<S> & Record<string, unknown>, Record<string, never>, Record<string, never>>;
|
|
190
|
+
state<V = undefined, K extends string = string>(key: K, init?: StateInit<TInput, V> | undefined): IlhaBuilder<TInput, MergeState<TStateMap, K, V>, TDerivedMap>;
|
|
191
|
+
derived<K extends string, V>(key: K, fn: DerivedFn<TInput, TStateMap, V>): IlhaBuilder<TInput, TStateMap, TDerivedMap & Record<K, V>>;
|
|
192
|
+
bind(selector: string, stateKey: keyof TStateMap & string): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
193
|
+
bind<T>(selector: string, externalSignal: ExternalSignal<T>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
194
|
+
on<S extends OnSelectorString>(selectorOrCombined: S, handler: (ctx: S extends `${string}@${infer E}:${string}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : S extends `${string}@${infer E}` ? HandlerContextFor<TInput, TStateMap, E, TDerivedMap> : HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
195
|
+
on(selectorOrCombined: string, handler: (ctx: HandlerContext<TInput, TStateMap, TDerivedMap>) => void | Promise<void>): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
196
|
+
effect(fn: (ctx: EffectContext<TInput, TStateMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
197
|
+
onMount(fn: (ctx: OnMountContext<TInput, TStateMap, TDerivedMap>) => (() => void) | void): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
198
|
+
transition(opts: TransitionOptions): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
199
|
+
css(strings: TemplateStringsArray | string, ...values: (string | number)[]): IlhaBuilder<TInput, TStateMap, TDerivedMap>;
|
|
200
|
+
render(fn: (ctx: RenderContext<TInput, TStateMap, TDerivedMap>) => string | RawHtml): Island<TInput, TStateMap>;
|
|
199
201
|
}
|
|
200
202
|
declare function ilhaFrom<TInput, TStateMap extends Record<string, unknown>>(selector: string | Element, island: Island<TInput, TStateMap>, props?: Partial<TInput>): (() => void) | null;
|
|
201
203
|
type IslandRegistry = Record<string, AnyIsland>;
|
|
202
204
|
declare function mountAll(registry: IslandRegistry, options?: MountOptions): MountResult;
|
|
203
|
-
declare const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never
|
|
205
|
+
declare const ilha: IlhaBuilder<Record<string, unknown>, Record<string, never>, Record<string, never>> & {
|
|
204
206
|
html: typeof ilhaHtml;
|
|
205
207
|
raw: typeof ilhaRaw;
|
|
206
208
|
mount: typeof mountAll;
|
|
@@ -214,4 +216,4 @@ declare const mount: typeof mountAll;
|
|
|
214
216
|
declare const from: typeof ilhaFrom;
|
|
215
217
|
declare const context: typeof ilhaContext;
|
|
216
218
|
//#endregion
|
|
217
|
-
export { DerivedValue, HandlerContext, HandlerContextFor, HydratableOptions, Island, IslandDerived, IslandState, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor,
|
|
219
|
+
export { DerivedValue, HandlerContext, HandlerContextFor, HydratableOptions, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, context, css, ilha as default, from, html, mount, raw };
|
package/dist/index.js
CHANGED
|
@@ -77,8 +77,9 @@ function dedentString(str) {
|
|
|
77
77
|
return lines.map((l) => l.slice(indent)).join("\n");
|
|
78
78
|
}
|
|
79
79
|
const RAW = Symbol("ilha.raw");
|
|
80
|
-
const SLOT_ACCESSOR = Symbol("ilha.slotAccessor");
|
|
81
80
|
const SIGNAL_ACCESSOR = Symbol("ilha.signalAccessor");
|
|
81
|
+
const ISLAND = Symbol("ilha.island");
|
|
82
|
+
const ISLAND_CALL = Symbol("ilha.islandCall");
|
|
82
83
|
const SLOT_ATTR = "data-ilha-slot";
|
|
83
84
|
const PROPS_ATTR = "data-ilha-props";
|
|
84
85
|
const STATE_ATTR = "data-ilha-state";
|
|
@@ -86,17 +87,70 @@ const CSS_ATTR = "data-ilha-css";
|
|
|
86
87
|
function buildScopedStyle(css) {
|
|
87
88
|
return `<style ${CSS_ATTR}>@scope (:scope) to ([data-ilha]){${css}}</style>`;
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const renderCtxStack = [];
|
|
91
|
+
function pushRenderCtx(liveHost, asyncChildren) {
|
|
92
|
+
const ctx = {
|
|
93
|
+
slots: /* @__PURE__ */ new Map(),
|
|
94
|
+
positional: 0,
|
|
95
|
+
liveHost,
|
|
96
|
+
pending: asyncChildren ? /* @__PURE__ */ new Map() : void 0
|
|
97
|
+
};
|
|
98
|
+
renderCtxStack.push(ctx);
|
|
99
|
+
return ctx;
|
|
100
|
+
}
|
|
101
|
+
function popRenderCtx() {
|
|
102
|
+
renderCtxStack.pop();
|
|
103
|
+
}
|
|
104
|
+
function currentRenderCtx() {
|
|
105
|
+
return renderCtxStack[renderCtxStack.length - 1];
|
|
106
|
+
}
|
|
107
|
+
function isIsland(v) {
|
|
108
|
+
return typeof v === "function" && ISLAND in v;
|
|
109
|
+
}
|
|
110
|
+
function isIslandCall(v) {
|
|
111
|
+
return (typeof v === "object" || typeof v === "function") && v !== null && ISLAND_CALL in v;
|
|
112
|
+
}
|
|
113
|
+
function emitIslandSlot(island, props, key) {
|
|
114
|
+
const ctx = currentRenderCtx();
|
|
115
|
+
let id;
|
|
116
|
+
if (key !== void 0) {
|
|
117
|
+
id = `k:${key}`;
|
|
118
|
+
if (ctx && __DEV__ && ctx.slots.has(id)) warn(`Duplicate slot key "${key}" — two children with the same key in a single render will collide. Each .key() call must be unique.`);
|
|
119
|
+
} else id = ctx ? `p:${ctx.positional++}` : "p:0";
|
|
120
|
+
if (ctx) ctx.slots.set(id, {
|
|
121
|
+
island,
|
|
122
|
+
props
|
|
93
123
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
const propsAttr = props ? ` ${PROPS_ATTR}='${escapeHtml(JSON.stringify(props))}'` : "";
|
|
125
|
+
if (ctx?.liveHost) return `<div ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}></div>`;
|
|
126
|
+
if (ctx?.pending) {
|
|
127
|
+
popRenderCtx();
|
|
128
|
+
try {
|
|
129
|
+
const result = island(props);
|
|
130
|
+
if (result instanceof Promise) {
|
|
131
|
+
ctx.pending.set(id, result.then(String));
|
|
132
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}></div>`;
|
|
133
|
+
}
|
|
134
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}>${result}</div>`;
|
|
135
|
+
} finally {
|
|
136
|
+
renderCtxStack.push(ctx);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const inner = island.toString(props);
|
|
140
|
+
return `<div ${SLOT_ATTR}="${escapeHtml(id)}"${propsAttr}>${inner}</div>`;
|
|
97
141
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
142
|
+
async function resolveAsyncChildren(html, ctx) {
|
|
143
|
+
for (const [id, promise] of ctx.pending) {
|
|
144
|
+
const inner = await promise;
|
|
145
|
+
const escaped = escapeHtml(id);
|
|
146
|
+
if (escaped.length > 500) throw new Error(`Slot id exceeds safe length for regex replacement (${escaped.length} > 500).`);
|
|
147
|
+
const attrPattern = escaped.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
148
|
+
const placeholder = new RegExp(`<div\\s[^>]*${SLOT_ATTR}="${attrPattern}"[^>]*></div>`, "g");
|
|
149
|
+
html = html.replace(placeholder, (match) => {
|
|
150
|
+
return match.slice(0, -6) + `>${inner}</div>`;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return html;
|
|
100
154
|
}
|
|
101
155
|
function markSignalAccessor(fn) {
|
|
102
156
|
fn[SIGNAL_ACCESSOR] = true;
|
|
@@ -124,7 +178,11 @@ function interpolateValue(v) {
|
|
|
124
178
|
if (v == null) return "";
|
|
125
179
|
if (Array.isArray(v)) return v.map(interpolateValue).join("");
|
|
126
180
|
if (typeof v === "object" && RAW in v) return v.value;
|
|
127
|
-
if (
|
|
181
|
+
if (isIslandCall(v)) {
|
|
182
|
+
const call = typeof v === "function" ? v() : v;
|
|
183
|
+
return emitIslandSlot(call.island, call.props, call.key);
|
|
184
|
+
}
|
|
185
|
+
if (isIsland(v)) return emitIslandSlot(v, void 0, void 0);
|
|
128
186
|
if (isSignalAccessor(v)) return escapeHtml(v());
|
|
129
187
|
if (typeof v === "function") return escapeHtml(v());
|
|
130
188
|
return escapeHtml(v);
|
|
@@ -321,7 +379,6 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
321
379
|
ons: [],
|
|
322
380
|
effects: [],
|
|
323
381
|
onMounts: [],
|
|
324
|
-
slots: {},
|
|
325
382
|
transition: null,
|
|
326
383
|
binds: [],
|
|
327
384
|
css: null
|
|
@@ -380,15 +437,6 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
380
437
|
onMounts: [...this._cfg.onMounts, { fn }]
|
|
381
438
|
});
|
|
382
439
|
}
|
|
383
|
-
slot(name, island) {
|
|
384
|
-
return new IlhaBuilder({
|
|
385
|
-
...this._cfg,
|
|
386
|
-
slots: {
|
|
387
|
-
...this._cfg.slots,
|
|
388
|
-
[name]: island
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
440
|
transition(opts) {
|
|
393
441
|
return new IlhaBuilder({
|
|
394
442
|
...this._cfg,
|
|
@@ -413,28 +461,38 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
413
461
|
});
|
|
414
462
|
}
|
|
415
463
|
render(fn) {
|
|
416
|
-
const { schema, states, deriveds, ons, effects, onMounts,
|
|
464
|
+
const { schema, states, deriveds, ons, effects, onMounts, transition, binds, css: cssSource } = this._cfg;
|
|
417
465
|
const stylePrefix = cssSource != null ? buildScopedStyle(cssSource) : "";
|
|
418
466
|
function resolveInput(props) {
|
|
419
467
|
const value = props ?? {};
|
|
420
468
|
if (!schema) return value;
|
|
421
469
|
return validateSchema(schema, value);
|
|
422
470
|
}
|
|
423
|
-
function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
471
|
+
function renderWithCtx(fn, liveHost, asyncChildren) {
|
|
472
|
+
const ctx = pushRenderCtx(liveHost, asyncChildren);
|
|
473
|
+
let isAsync = false;
|
|
474
|
+
try {
|
|
475
|
+
const html = unwrapHtml(fn());
|
|
476
|
+
if (ctx.pending && ctx.pending.size > 0) {
|
|
477
|
+
isAsync = true;
|
|
478
|
+
return (async () => {
|
|
479
|
+
try {
|
|
480
|
+
return {
|
|
481
|
+
html: await resolveAsyncChildren(html, ctx),
|
|
482
|
+
slots: ctx.slots
|
|
483
|
+
};
|
|
484
|
+
} finally {
|
|
485
|
+
popRenderCtx();
|
|
486
|
+
}
|
|
487
|
+
})();
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
html,
|
|
491
|
+
slots: ctx.slots
|
|
492
|
+
};
|
|
493
|
+
} finally {
|
|
494
|
+
if (!isAsync) popRenderCtx();
|
|
495
|
+
}
|
|
438
496
|
}
|
|
439
497
|
function buildPlainState(input) {
|
|
440
498
|
const state = {};
|
|
@@ -462,7 +520,6 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
462
520
|
function renderToString(props, sync = false) {
|
|
463
521
|
const input = resolveInput(props);
|
|
464
522
|
const state = buildPlainState(input);
|
|
465
|
-
const slots = makeSlotsProxy(true);
|
|
466
523
|
const results = deriveds.map((entry) => {
|
|
467
524
|
try {
|
|
468
525
|
return {
|
|
@@ -492,12 +549,12 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
492
549
|
value: r.result,
|
|
493
550
|
error: void 0
|
|
494
551
|
};
|
|
495
|
-
|
|
552
|
+
const { html } = renderWithCtx(() => fn({
|
|
496
553
|
state,
|
|
497
554
|
derived,
|
|
498
|
-
input
|
|
499
|
-
slots
|
|
555
|
+
input
|
|
500
556
|
}));
|
|
557
|
+
return stylePrefix + html;
|
|
501
558
|
}
|
|
502
559
|
return Promise.all(results.map(async (r) => {
|
|
503
560
|
try {
|
|
@@ -519,15 +576,15 @@ var IlhaBuilder = class IlhaBuilder {
|
|
|
519
576
|
}
|
|
520
577
|
};
|
|
521
578
|
}
|
|
522
|
-
})).then((resolved) => {
|
|
579
|
+
})).then(async (resolved) => {
|
|
523
580
|
const derived = {};
|
|
524
581
|
for (const r of resolved) derived[r.key] = r.envelope;
|
|
525
|
-
|
|
582
|
+
const { html } = await renderWithCtx(() => fn({
|
|
526
583
|
state,
|
|
527
584
|
derived,
|
|
528
|
-
input
|
|
529
|
-
|
|
530
|
-
|
|
585
|
+
input
|
|
586
|
+
}), void 0, true);
|
|
587
|
+
return stylePrefix + html;
|
|
531
588
|
});
|
|
532
589
|
}
|
|
533
590
|
function mountIsland(host, props) {
|
|
@@ -576,28 +633,49 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
576
633
|
}
|
|
577
634
|
const { proxy: derived, stop: stopDerived } = buildDerivedSignals(deriveds, state, input, derivedSnapshot);
|
|
578
635
|
cleanups.push(stopDerived);
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
636
|
+
const mountedSlots = /* @__PURE__ */ new Map();
|
|
637
|
+
function findSlot(id) {
|
|
638
|
+
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
|
|
639
|
+
const candidates = host.querySelectorAll(`[${SLOT_ATTR}="${escaped}"]`);
|
|
640
|
+
for (const candidate of candidates) {
|
|
641
|
+
let el = candidate;
|
|
642
|
+
while (el && el !== host) {
|
|
643
|
+
if (el.hasAttribute("data-ilha")) break;
|
|
644
|
+
el = el.parentElement;
|
|
645
|
+
}
|
|
646
|
+
if (el === host) return candidate;
|
|
647
|
+
}
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
function mountSlots(slotMap) {
|
|
651
|
+
for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) {
|
|
652
|
+
entry.unmount();
|
|
653
|
+
mountedSlots.delete(id);
|
|
654
|
+
}
|
|
655
|
+
for (const [id, { island: childIsland, props }] of slotMap) {
|
|
656
|
+
if (mountedSlots.has(id)) continue;
|
|
657
|
+
const slotEl = findSlot(id);
|
|
584
658
|
if (!slotEl) continue;
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
warn(`Failed to parse props on [${SLOT_ATTR}="${name}"] — invalid JSON ignored.`);
|
|
659
|
+
let slotProps = props;
|
|
660
|
+
if (slotProps === void 0) {
|
|
661
|
+
const rawProps = slotEl.getAttribute(PROPS_ATTR) ?? slotEl.getAttribute("data-props");
|
|
662
|
+
if (rawProps) try {
|
|
663
|
+
slotProps = JSON.parse(rawProps);
|
|
664
|
+
} catch {
|
|
665
|
+
warn(`Failed to parse props on [${SLOT_ATTR}="${id}"] — invalid JSON ignored.`);
|
|
666
|
+
}
|
|
594
667
|
}
|
|
595
668
|
const prevSub = setActiveSub(void 0);
|
|
669
|
+
let unmount;
|
|
596
670
|
try {
|
|
597
|
-
|
|
671
|
+
unmount = childIsland.mount(slotEl, slotProps);
|
|
598
672
|
} finally {
|
|
599
673
|
setActiveSub(prevSub);
|
|
600
674
|
}
|
|
675
|
+
mountedSlots.set(id, {
|
|
676
|
+
el: slotEl,
|
|
677
|
+
unmount
|
|
678
|
+
});
|
|
601
679
|
}
|
|
602
680
|
}
|
|
603
681
|
const listeners = [];
|
|
@@ -644,18 +722,18 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
644
722
|
for (const l of listeners) l.target.removeEventListener(l.type, l.fn, l.options);
|
|
645
723
|
listeners.length = 0;
|
|
646
724
|
}
|
|
647
|
-
const
|
|
648
|
-
|
|
725
|
+
const hasExistingContent = hydrated && host.childNodes.length > 0;
|
|
726
|
+
const initial = renderWithCtx(() => fn({
|
|
649
727
|
state,
|
|
650
728
|
derived,
|
|
651
|
-
input
|
|
652
|
-
|
|
653
|
-
|
|
729
|
+
input
|
|
730
|
+
}), hasExistingContent ? host : void 0);
|
|
731
|
+
if (!hasExistingContent) host.innerHTML = stylePrefix + initial.html;
|
|
654
732
|
attachListeners();
|
|
655
733
|
let stopBindings = applyBindings(host, binds, state);
|
|
656
734
|
cleanups.push(() => stopBindings());
|
|
657
|
-
mountSlots();
|
|
658
|
-
cleanups.push(() =>
|
|
735
|
+
mountSlots(initial.slots);
|
|
736
|
+
cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
|
|
659
737
|
if (!shouldSkipOnMount) for (const entry of onMounts) {
|
|
660
738
|
const prevSub = setActiveSub(void 0);
|
|
661
739
|
let userCleanup;
|
|
@@ -674,24 +752,42 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
674
752
|
}
|
|
675
753
|
let initialized = false;
|
|
676
754
|
const stopRender = effect(() => {
|
|
677
|
-
const html =
|
|
755
|
+
const { html: rendered, slots: newSlotMap } = renderWithCtx(() => fn({
|
|
678
756
|
state,
|
|
679
757
|
derived,
|
|
680
|
-
input
|
|
681
|
-
|
|
682
|
-
|
|
758
|
+
input
|
|
759
|
+
}), host);
|
|
760
|
+
const html = stylePrefix + rendered;
|
|
683
761
|
if (!initialized) {
|
|
684
762
|
initialized = true;
|
|
685
763
|
return;
|
|
686
764
|
}
|
|
687
765
|
detachListeners();
|
|
688
766
|
stopBindings();
|
|
767
|
+
for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
|
|
768
|
+
entry.unmount();
|
|
769
|
+
mountedSlots.delete(id);
|
|
770
|
+
}
|
|
771
|
+
const preserved = /* @__PURE__ */ new Map();
|
|
772
|
+
for (const [id, entry] of mountedSlots) {
|
|
773
|
+
if (!newSlotMap.has(id)) continue;
|
|
774
|
+
if (!entry.el.isConnected) continue;
|
|
775
|
+
const stub = document.createElement("div");
|
|
776
|
+
stub.setAttribute(SLOT_ATTR, id);
|
|
777
|
+
entry.el.replaceWith(stub);
|
|
778
|
+
preserved.set(id, entry.el);
|
|
779
|
+
}
|
|
689
780
|
const tpl = document.createElement("template");
|
|
690
781
|
tpl.innerHTML = `<div>${html}</div>`;
|
|
691
782
|
morphInner(host, tpl.content.firstElementChild);
|
|
783
|
+
for (const [id, liveEl] of preserved) {
|
|
784
|
+
const stub = findSlot(id);
|
|
785
|
+
if (!stub) continue;
|
|
786
|
+
stub.replaceWith(liveEl);
|
|
787
|
+
}
|
|
692
788
|
attachListeners();
|
|
693
789
|
stopBindings = applyBindings(host, binds, state);
|
|
694
|
-
mountSlots();
|
|
790
|
+
mountSlots(newSlotMap);
|
|
695
791
|
});
|
|
696
792
|
cleanups.push(stopRender);
|
|
697
793
|
cleanups.push(detachListeners);
|
|
@@ -728,11 +824,41 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
728
824
|
cleanups.forEach((c) => c());
|
|
729
825
|
};
|
|
730
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Dual-mode island invocation:
|
|
829
|
+
* - When called inside an `html` interpolation (i.e. {@link currentRenderCtx}
|
|
830
|
+
* is active), returns an {@link IslandCall} carrying props to be emitted
|
|
831
|
+
* as a child slot marker; {@link interpolateValue} consumes this marker.
|
|
832
|
+
* - When called at the top level with no active render context, returns
|
|
833
|
+
* SSR HTML via {@link renderToString} (backward-compatible public API).
|
|
834
|
+
*
|
|
835
|
+
* The declared return type omits {@link IslandCall} because TypeScript
|
|
836
|
+
* cannot narrow based on runtime stack state.
|
|
837
|
+
*/
|
|
731
838
|
const island = function(props) {
|
|
839
|
+
if (currentRenderCtx()) return {
|
|
840
|
+
[ISLAND_CALL]: true,
|
|
841
|
+
island,
|
|
842
|
+
props,
|
|
843
|
+
key: void 0
|
|
844
|
+
};
|
|
732
845
|
return renderToString(props);
|
|
733
846
|
};
|
|
734
847
|
island.toString = (props) => renderToString(props, true);
|
|
735
848
|
island.mount = (host, props) => mountIsland(host, props);
|
|
849
|
+
island.key = (key) => {
|
|
850
|
+
if (typeof key !== "string" || key.trim().length === 0) throw new Error("island.key() requires a non-empty string.");
|
|
851
|
+
if (key.includes(":")) throw new Error(`island.key() key cannot contain the slot separator ":" (got "${key}").`);
|
|
852
|
+
const keyed = ((props) => ({
|
|
853
|
+
[ISLAND_CALL]: true,
|
|
854
|
+
island,
|
|
855
|
+
props,
|
|
856
|
+
key
|
|
857
|
+
}));
|
|
858
|
+
keyed[ISLAND_CALL] = true;
|
|
859
|
+
return keyed;
|
|
860
|
+
};
|
|
861
|
+
island[ISLAND] = true;
|
|
736
862
|
island.hydratable = async (props, opts) => {
|
|
737
863
|
const { name, as: tag = "div", snapshot = false, skipOnMount: explicitSkipOnMount } = opts;
|
|
738
864
|
const resolvedProps = props ?? {};
|
|
@@ -831,7 +957,6 @@ const rootBuilder = new IlhaBuilder({
|
|
|
831
957
|
ons: [],
|
|
832
958
|
effects: [],
|
|
833
959
|
onMounts: [],
|
|
834
|
-
slots: {},
|
|
835
960
|
transition: null,
|
|
836
961
|
binds: [],
|
|
837
962
|
css: null
|