ilha 0.5.0 → 0.6.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.
- package/dist/index.d.ts +21 -7
- package/dist/index.js +391 -32
- package/dist/jsx-dev-runtime.d.ts +3 -0
- package/dist/jsx-dev-runtime.js +2 -0
- package/dist/jsx-runtime.d.ts +3 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/jsx-types-DSrM2NdI.d.ts +26 -0
- package/package.json +9 -1
package/dist/index.d.ts
CHANGED
|
@@ -51,10 +51,19 @@ interface IslandCall {
|
|
|
51
51
|
interface MarkedSignalAccessor<T> {
|
|
52
52
|
(): T;
|
|
53
53
|
(...args: [value: T]): void;
|
|
54
|
+
select<S>(selector: (state: T) => S): MarkedSignalAccessor<S>;
|
|
54
55
|
[SIGNAL_ACCESSOR]: true;
|
|
55
56
|
}
|
|
56
57
|
declare function ilhaRaw(value: string): RawHtml;
|
|
57
58
|
declare function ilhaCss(strings: TemplateStringsArray | string, ...values: (string | number)[]): string;
|
|
59
|
+
type JsxChild = unknown;
|
|
60
|
+
type JsxProps = Record<string, unknown> | null | undefined;
|
|
61
|
+
type JsxType = string | ((props: Record<string, unknown>) => unknown);
|
|
62
|
+
declare function ilhaJsx(type: JsxType, props: JsxProps, maybeKey?: JsxChild | string | number, ...restChildren: JsxChild[]): RawHtml;
|
|
63
|
+
declare function ilhaFragment(props: {
|
|
64
|
+
children?: JsxChild;
|
|
65
|
+
} | null, ...children: JsxChild[]): RawHtml;
|
|
66
|
+
declare function ilhaJsxDEV(type: JsxType, props: JsxProps, maybeKey?: string | number, _source?: unknown, _self?: unknown): RawHtml;
|
|
58
67
|
declare function ilhaHtml(strings: TemplateStringsArray, ...values: unknown[]): RawHtml;
|
|
59
68
|
type ContextSignal<T> = {
|
|
60
69
|
(): T;
|
|
@@ -72,7 +81,7 @@ declare function ilhaContext<T>(key: string, initial: T): ContextSignal<T>;
|
|
|
72
81
|
* or `.render()` automatically subscribes the surrounding reactive scope —
|
|
73
82
|
* so when the signal changes, dependents re-run as if it were local state.
|
|
74
83
|
*/
|
|
75
|
-
declare function ilhaSignal<T>(initial: T):
|
|
84
|
+
declare function ilhaSignal<T>(initial: T): SignalAccessor<T>;
|
|
76
85
|
/**
|
|
77
86
|
* Run `fn` with reactive tracking suspended. Reading signals inside `fn`
|
|
78
87
|
* returns their current value without subscribing the surrounding scope.
|
|
@@ -107,10 +116,7 @@ interface DerivedEntry<TInput, TStateMap extends Record<string, unknown>> {
|
|
|
107
116
|
fn: DerivedFn<TInput, TStateMap, unknown>;
|
|
108
117
|
}
|
|
109
118
|
type IslandDerived<TDerivedMap extends Record<string, unknown>> = { readonly [K in keyof TDerivedMap]: DerivedValue<TDerivedMap[K]> };
|
|
110
|
-
|
|
111
|
-
(): T;
|
|
112
|
-
(value: T): void;
|
|
113
|
-
}
|
|
119
|
+
type ExternalSignal<T = unknown> = SignalAccessor<T>;
|
|
114
120
|
type SignalAccessor<T> = MarkedSignalAccessor<T>;
|
|
115
121
|
type MergeState<TStateMap extends Record<string, unknown>, K extends string, V> = Omit<TStateMap, K> & Record<K, V>;
|
|
116
122
|
type IslandState<TStateMap extends Record<string, unknown>> = { readonly [K in keyof TStateMap]-?: SignalAccessor<TStateMap[K]> };
|
|
@@ -235,7 +241,7 @@ interface MountOptions {
|
|
|
235
241
|
lazy?: boolean;
|
|
236
242
|
}
|
|
237
243
|
interface MountResult {
|
|
238
|
-
unmount: () => void
|
|
244
|
+
unmount: () => void | Promise<void>;
|
|
239
245
|
}
|
|
240
246
|
interface BuilderConfig<TInput, TStateMap extends Record<string, unknown>, TDerivedMap extends Record<string, unknown>> {
|
|
241
247
|
schema: StandardSchemaV1 | null;
|
|
@@ -269,6 +275,10 @@ declare function mountAll(registry: IslandRegistry, options?: MountOptions): Mou
|
|
|
269
275
|
declare const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, Record<never, never>> & {
|
|
270
276
|
html: typeof ilhaHtml;
|
|
271
277
|
raw: typeof ilhaRaw;
|
|
278
|
+
jsx: typeof ilhaJsx;
|
|
279
|
+
jsxs: typeof ilhaJsx;
|
|
280
|
+
jsxDEV: typeof ilhaJsxDEV;
|
|
281
|
+
Fragment: typeof ilhaFragment;
|
|
272
282
|
mount: typeof mountAll;
|
|
273
283
|
from: typeof ilhaFrom;
|
|
274
284
|
context: typeof ilhaContext;
|
|
@@ -278,9 +288,13 @@ declare const ilha: IlhaBuilder<Record<string, unknown>, Record<never, never>, R
|
|
|
278
288
|
};
|
|
279
289
|
declare const html: typeof ilhaHtml;
|
|
280
290
|
declare const raw: typeof ilhaRaw;
|
|
291
|
+
declare const jsx: typeof ilhaJsx;
|
|
292
|
+
declare const jsxs: typeof ilhaJsx;
|
|
293
|
+
declare const jsxDEV: typeof ilhaJsxDEV;
|
|
294
|
+
declare const Fragment: typeof ilhaFragment;
|
|
281
295
|
declare const css: typeof ilhaCss;
|
|
282
296
|
declare const mount: typeof mountAll;
|
|
283
297
|
declare const from: typeof ilhaFrom;
|
|
284
298
|
declare const context: typeof ilhaContext;
|
|
285
299
|
//#endregion
|
|
286
|
-
export { DerivedValue, ErrorContext, ErrorSource, ExternalSignal, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, mount, raw, untrack };
|
|
300
|
+
export { DerivedValue, ErrorContext, ErrorSource, ExternalSignal, Fragment, HandlerContext, HandlerContextFor, HydratableOptions, ISLAND_MOUNT_INTERNAL, Island, IslandDerived, IslandState, KeyedIsland, MountOptions, MountResult, OnMountContext, RawHtml, SignalAccessor, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
|
package/dist/index.js
CHANGED
|
@@ -16,9 +16,76 @@ function shallowEqualInput(a, b) {
|
|
|
16
16
|
}
|
|
17
17
|
return true;
|
|
18
18
|
}
|
|
19
|
+
function extractDirectChildSlotIdsInOrder(html) {
|
|
20
|
+
if (typeof document === "undefined") return [];
|
|
21
|
+
const tpl = document.createElement("template");
|
|
22
|
+
tpl.innerHTML = html;
|
|
23
|
+
const root = tpl.content;
|
|
24
|
+
const ids = [];
|
|
25
|
+
for (const el of root.querySelectorAll("[data-ilha-slot]")) {
|
|
26
|
+
if (!(el instanceof Element)) continue;
|
|
27
|
+
let parent = el.parentElement ?? el.parentNode;
|
|
28
|
+
let nested = false;
|
|
29
|
+
while (parent && parent !== root) {
|
|
30
|
+
if (parent instanceof Element && parent.hasAttribute("data-ilha-slot")) {
|
|
31
|
+
nested = true;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
parent = parent instanceof Element ? parent.parentElement ?? parent.parentNode : parent.parentNode;
|
|
35
|
+
}
|
|
36
|
+
if (!nested) ids.push(el.getAttribute("data-ilha-slot"));
|
|
37
|
+
}
|
|
38
|
+
return ids;
|
|
39
|
+
}
|
|
40
|
+
function extractSlotPropsAttr(html, slotId) {
|
|
41
|
+
const escaped = slotId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
42
|
+
const withProps = new RegExp(`<div\\s[^>]*data-ilha-slot="${escaped}"[^>]*data-ilha-props='([^']*)'`);
|
|
43
|
+
const m = html.match(withProps);
|
|
44
|
+
if (m) return m[1];
|
|
45
|
+
const withoutProps = new RegExp(`<div\\s[^>]*data-ilha-slot="${escaped}"`);
|
|
46
|
+
return html.match(withoutProps) ? "" : null;
|
|
47
|
+
}
|
|
48
|
+
function isStableInlineSlotMount(initialHtml, renderedHtml, slotIds) {
|
|
49
|
+
const initialOrder = extractDirectChildSlotIdsInOrder(initialHtml);
|
|
50
|
+
const renderedOrder = extractDirectChildSlotIdsInOrder(renderedHtml);
|
|
51
|
+
if (initialOrder.length !== renderedOrder.length || initialOrder.some((id, i) => id !== renderedOrder[i])) return false;
|
|
52
|
+
for (const id of slotIds) {
|
|
53
|
+
const initialProps = extractSlotPropsAttr(initialHtml, id);
|
|
54
|
+
const renderedProps = extractSlotPropsAttr(renderedHtml, id);
|
|
55
|
+
if (initialProps === null || renderedProps === null) return false;
|
|
56
|
+
if (initialProps !== renderedProps) return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
function serializeSlotProps(props) {
|
|
61
|
+
return props === void 0 ? "" : JSON.stringify(props);
|
|
62
|
+
}
|
|
63
|
+
const MORPH_CONTROLLER_SLOT_SELECTOR = "[data-slot]";
|
|
64
|
+
const MORPH_CONTROLLER_ATTRS = new Set([
|
|
65
|
+
"data-checked",
|
|
66
|
+
"data-unchecked",
|
|
67
|
+
"data-indeterminate",
|
|
68
|
+
"aria-checked",
|
|
69
|
+
"data-open",
|
|
70
|
+
"data-closed",
|
|
71
|
+
"data-state",
|
|
72
|
+
"aria-expanded",
|
|
73
|
+
"aria-hidden",
|
|
74
|
+
"data-selected",
|
|
75
|
+
"data-panel-open"
|
|
76
|
+
]);
|
|
77
|
+
function shouldPreserveMorphAttr(el, name) {
|
|
78
|
+
return el.matches(MORPH_CONTROLLER_SLOT_SELECTOR) && MORPH_CONTROLLER_ATTRS.has(name);
|
|
79
|
+
}
|
|
19
80
|
function syncAttributes(from, to) {
|
|
20
|
-
for (const { name, value } of to.attributes)
|
|
21
|
-
|
|
81
|
+
for (const { name, value } of to.attributes) {
|
|
82
|
+
if (shouldPreserveMorphAttr(from, name)) continue;
|
|
83
|
+
if (from.getAttribute(name) !== value) from.setAttribute(name, value);
|
|
84
|
+
}
|
|
85
|
+
for (const { name } of Array.from(from.attributes)) {
|
|
86
|
+
if (shouldPreserveMorphAttr(from, name)) continue;
|
|
87
|
+
if (!to.hasAttribute(name)) from.removeAttribute(name);
|
|
88
|
+
}
|
|
22
89
|
}
|
|
23
90
|
function morphChildren(fromParent, toParent) {
|
|
24
91
|
const fromNodes = Array.from(fromParent.childNodes);
|
|
@@ -88,14 +155,14 @@ function dedentString(str) {
|
|
|
88
155
|
const indent = Math.min(...lines.filter((l) => l.trim() !== "").map((l) => l.match(/^(\s*)/)[1].length));
|
|
89
156
|
return lines.map((l) => l.slice(indent)).join("\n");
|
|
90
157
|
}
|
|
91
|
-
const RAW = Symbol("ilha.raw");
|
|
92
|
-
const SIGNAL_ACCESSOR = Symbol("ilha.signalAccessor");
|
|
93
|
-
const ISLAND = Symbol("ilha.island");
|
|
94
|
-
const ISLAND_CALL = Symbol("ilha.islandCall");
|
|
158
|
+
const RAW = Symbol.for("ilha.raw");
|
|
159
|
+
const SIGNAL_ACCESSOR = Symbol.for("ilha.signalAccessor");
|
|
160
|
+
const ISLAND = Symbol.for("ilha.island");
|
|
161
|
+
const ISLAND_CALL = Symbol.for("ilha.islandCall");
|
|
95
162
|
/** @internal Internal hook used by a parent's mountSlots to mount a child island and
|
|
96
163
|
* retain a handle to push updated props into it on subsequent parent
|
|
97
164
|
* re-renders. Not part of the public surface. */
|
|
98
|
-
const ISLAND_MOUNT_INTERNAL = Symbol("ilha.islandMountInternal");
|
|
165
|
+
const ISLAND_MOUNT_INTERNAL = Symbol.for("ilha.islandMountInternal");
|
|
99
166
|
const SLOT_ATTR = "data-ilha-slot";
|
|
100
167
|
const PROPS_ATTR = "data-ilha-props";
|
|
101
168
|
const STATE_ATTR = "data-ilha-state";
|
|
@@ -122,10 +189,19 @@ function currentRenderCtx() {
|
|
|
122
189
|
return renderCtxStack[renderCtxStack.length - 1];
|
|
123
190
|
}
|
|
124
191
|
function isIsland(v) {
|
|
125
|
-
|
|
192
|
+
if (typeof v !== "function") return false;
|
|
193
|
+
if (ISLAND in v) return true;
|
|
194
|
+
return Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.island");
|
|
126
195
|
}
|
|
127
196
|
function isIslandCall(v) {
|
|
128
|
-
|
|
197
|
+
if (v == null || typeof v !== "object" && typeof v !== "function") return false;
|
|
198
|
+
if (ISLAND_CALL in v) return true;
|
|
199
|
+
if (Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.islandCall")) return true;
|
|
200
|
+
if (typeof v === "object" && "island" in v) {
|
|
201
|
+
const island = v.island;
|
|
202
|
+
return typeof island === "function" && (ISLAND in island || Object.getOwnPropertySymbols(island).some((s) => s.description === "ilha.island"));
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
129
205
|
}
|
|
130
206
|
function emitIslandSlot(island, props, key) {
|
|
131
207
|
const ctx = currentRenderCtx();
|
|
@@ -171,10 +247,86 @@ async function resolveAsyncChildren(html, ctx) {
|
|
|
171
247
|
}
|
|
172
248
|
function markSignalAccessor(fn) {
|
|
173
249
|
fn[SIGNAL_ACCESSOR] = true;
|
|
174
|
-
|
|
250
|
+
const accessor = fn;
|
|
251
|
+
accessor.select = (selector) => createSelectAccessor(accessor, selector);
|
|
252
|
+
return accessor;
|
|
175
253
|
}
|
|
176
254
|
function isSignalAccessor(v) {
|
|
177
|
-
|
|
255
|
+
if (typeof v !== "function") return false;
|
|
256
|
+
if (SIGNAL_ACCESSOR in v) return true;
|
|
257
|
+
return Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.signalAccessor");
|
|
258
|
+
}
|
|
259
|
+
function getAtPath(obj, path) {
|
|
260
|
+
let cur = obj;
|
|
261
|
+
for (const seg of path) {
|
|
262
|
+
if (cur == null) return void 0;
|
|
263
|
+
cur = cur[seg];
|
|
264
|
+
}
|
|
265
|
+
return cur;
|
|
266
|
+
}
|
|
267
|
+
function setAtPath(obj, path, value) {
|
|
268
|
+
if (path.length === 0) return value;
|
|
269
|
+
const [head, ...rest] = path;
|
|
270
|
+
if (Array.isArray(obj)) {
|
|
271
|
+
const idx = head;
|
|
272
|
+
if (idx < 0 || idx >= obj.length) return obj;
|
|
273
|
+
const next = obj[idx];
|
|
274
|
+
const updated = rest.length === 0 ? value : setAtPath(next, rest, value);
|
|
275
|
+
if (Object.is(next, updated)) return obj;
|
|
276
|
+
const copy = obj.slice();
|
|
277
|
+
copy[idx] = updated;
|
|
278
|
+
return copy;
|
|
279
|
+
}
|
|
280
|
+
if (obj !== null && typeof obj === "object") {
|
|
281
|
+
const record = obj;
|
|
282
|
+
const key = String(head);
|
|
283
|
+
const next = record[key];
|
|
284
|
+
const updated = rest.length === 0 ? value : setAtPath(next, rest, value);
|
|
285
|
+
if (rest.length === 0) {
|
|
286
|
+
if (Object.is(next, value)) return obj;
|
|
287
|
+
} else if (Object.is(next, updated)) return obj;
|
|
288
|
+
return {
|
|
289
|
+
...record,
|
|
290
|
+
[key]: updated
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return rest.length === 0 ? value : setAtPath(void 0, rest, value);
|
|
294
|
+
}
|
|
295
|
+
function toPathSegment(prop) {
|
|
296
|
+
if (typeof prop === "symbol") return null;
|
|
297
|
+
if (prop === "length") return null;
|
|
298
|
+
if (/^\d+$/.test(prop)) return Number(prop);
|
|
299
|
+
return prop;
|
|
300
|
+
}
|
|
301
|
+
function trackSelectPath(rootState, selector) {
|
|
302
|
+
const path = [];
|
|
303
|
+
const track = (value) => {
|
|
304
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") return value;
|
|
305
|
+
return new Proxy(value, { get(target, prop, receiver) {
|
|
306
|
+
const seg = toPathSegment(prop);
|
|
307
|
+
if (seg != null) path.push(seg);
|
|
308
|
+
const next = Reflect.get(target, prop, receiver);
|
|
309
|
+
return seg != null && next !== null && typeof next === "object" ? track(next) : next;
|
|
310
|
+
} });
|
|
311
|
+
};
|
|
312
|
+
selector(track(rootState));
|
|
313
|
+
return path;
|
|
314
|
+
}
|
|
315
|
+
function createSelectAccessor(root, selector) {
|
|
316
|
+
const path = trackSelectPath(root(), selector);
|
|
317
|
+
const selected = selector(root());
|
|
318
|
+
const resolved = path.length === 0 ? root() : getAtPath(root(), path);
|
|
319
|
+
if (!Object.is(selected, resolved)) {
|
|
320
|
+
const msg = "select(): selector must only traverse nested properties or array indexes — derived or transformed values are not supported.";
|
|
321
|
+
if (__DEV__) warn(msg);
|
|
322
|
+
throw new Error(msg);
|
|
323
|
+
}
|
|
324
|
+
if (__DEV__ && path.length === 0) warn("select(): selector did not traverse nested state — bind writes may replace the entire root value.");
|
|
325
|
+
return markSignalAccessor((...args) => {
|
|
326
|
+
if (args.length === 0) return path.length === 0 ? selector(root()) : getAtPath(root(), path);
|
|
327
|
+
const next = path.length === 0 ? args[0] : setAtPath(root(), path, args[0]);
|
|
328
|
+
if (!Object.is(root(), next)) root(next);
|
|
329
|
+
});
|
|
178
330
|
}
|
|
179
331
|
function ilhaRaw(value) {
|
|
180
332
|
return {
|
|
@@ -191,10 +343,143 @@ function ilhaCss(strings, ...values) {
|
|
|
191
343
|
}
|
|
192
344
|
return result;
|
|
193
345
|
}
|
|
346
|
+
const SAFE_NAME_RE = /^[A-Za-z_:][A-Za-z0-9:._-]*$/;
|
|
347
|
+
const SAFE_BIND_LOCAL_RE = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
348
|
+
const VOID_ELEMENTS = new Set([
|
|
349
|
+
"area",
|
|
350
|
+
"base",
|
|
351
|
+
"br",
|
|
352
|
+
"col",
|
|
353
|
+
"embed",
|
|
354
|
+
"hr",
|
|
355
|
+
"img",
|
|
356
|
+
"input",
|
|
357
|
+
"link",
|
|
358
|
+
"meta",
|
|
359
|
+
"param",
|
|
360
|
+
"source",
|
|
361
|
+
"track",
|
|
362
|
+
"wbr"
|
|
363
|
+
]);
|
|
364
|
+
const SAFE_CSS_PROP_RE = /^(-{2}[a-zA-Z][a-zA-Z0-9-]*|-?[a-zA-Z][a-zA-Z0-9-]*)$/;
|
|
365
|
+
const URL_ATTRS = new Set([
|
|
366
|
+
"href",
|
|
367
|
+
"src",
|
|
368
|
+
"action",
|
|
369
|
+
"formaction",
|
|
370
|
+
"cite",
|
|
371
|
+
"data",
|
|
372
|
+
"poster"
|
|
373
|
+
]);
|
|
374
|
+
const SAFE_URL_RE = /^(?!javascript:|data:text\/html|data:text\/xml|data:application\/xhtml\+xml|data:image\/svg|vbscript:)/i;
|
|
375
|
+
function normalizeClass(value) {
|
|
376
|
+
if (Array.isArray(value)) return value.filter(Boolean).join(" ");
|
|
377
|
+
if (value && typeof value === "object") return Object.entries(value).filter(([, enabled]) => Boolean(enabled)).map(([name]) => name).join(" ");
|
|
378
|
+
return String(value);
|
|
379
|
+
}
|
|
380
|
+
function normalizeJsxChildren(props, children) {
|
|
381
|
+
const propChildren = props && "children" in props ? props.children : void 0;
|
|
382
|
+
return (children.length > 0 ? children : propChildren === void 0 ? [] : [propChildren]).flat(1);
|
|
383
|
+
}
|
|
384
|
+
function normalizeJsxSlotKey(raw) {
|
|
385
|
+
const key = String(raw);
|
|
386
|
+
if (key.trim().length === 0) throw new Error("jsx key requires a non-empty string.");
|
|
387
|
+
if (key.includes(":")) throw new Error(`jsx key cannot contain the slot separator ":" (got "${key}").`);
|
|
388
|
+
return key;
|
|
389
|
+
}
|
|
390
|
+
/** React-style `key` (props or 3rd jsx arg) on island components → `k:{key}` slot ids. */
|
|
391
|
+
function extractJsxSlotKey(props, keyArg) {
|
|
392
|
+
const fromProps = props?.key;
|
|
393
|
+
const raw = keyArg ?? (typeof fromProps === "string" || typeof fromProps === "number" ? fromProps : void 0);
|
|
394
|
+
if (raw == null) return void 0;
|
|
395
|
+
return normalizeJsxSlotKey(raw);
|
|
396
|
+
}
|
|
397
|
+
function serializeStyle(value) {
|
|
398
|
+
return Object.entries(value).map(([k, v]) => {
|
|
399
|
+
if (!SAFE_CSS_PROP_RE.test(k)) return "";
|
|
400
|
+
const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
401
|
+
let safeV = String(v).replace(/["<>{};]/g, "");
|
|
402
|
+
if (/expression\(/i.test(safeV) || /^javascript:/i.test(safeV)) return "";
|
|
403
|
+
return `${prop}:${safeV}`;
|
|
404
|
+
}).filter(Boolean).join(";");
|
|
405
|
+
}
|
|
406
|
+
function pushJsxAttr(chunks, values, name, value) {
|
|
407
|
+
if (value == null || value === false || name === "children" || name === "key") return;
|
|
408
|
+
if (name === "__proto__" || name === "constructor" || name === "prototype") return;
|
|
409
|
+
if (name.startsWith("bind:")) {
|
|
410
|
+
const [prefix, localName, ...rest] = name.split(":");
|
|
411
|
+
if (prefix !== "bind" || rest.length > 0 || !localName || !SAFE_BIND_LOCAL_RE.test(localName)) return;
|
|
412
|
+
if (!isSignalAccessor(value)) return;
|
|
413
|
+
const safeName = `${prefix}:${localName}`;
|
|
414
|
+
chunks[chunks.length - 1] += ` ${safeName}=`;
|
|
415
|
+
values.push(value);
|
|
416
|
+
chunks.push("");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (!SAFE_NAME_RE.test(name)) return;
|
|
420
|
+
let safeName = name;
|
|
421
|
+
if (safeName === "className") safeName = "class";
|
|
422
|
+
if (safeName === "htmlFor") safeName = "for";
|
|
423
|
+
if (safeName.startsWith("on")) return;
|
|
424
|
+
if (safeName === "class") value = normalizeClass(value);
|
|
425
|
+
if (safeName === "style" && value && typeof value === "object") value = serializeStyle(value);
|
|
426
|
+
if ((URL_ATTRS.has(safeName) || /:(href|src|action|formaction|cite|data|poster)$/.test(safeName)) && typeof value === "string" && !SAFE_URL_RE.test(value.trim())) return;
|
|
427
|
+
if (value === true) {
|
|
428
|
+
chunks[chunks.length - 1] += ` ${safeName}`;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
chunks[chunks.length - 1] += ` ${safeName}="`;
|
|
432
|
+
values.push(value);
|
|
433
|
+
chunks.push("\"");
|
|
434
|
+
}
|
|
435
|
+
function renderJsxElement(type, props, children) {
|
|
436
|
+
const chunks = [`<${type}`];
|
|
437
|
+
const values = [];
|
|
438
|
+
if (props) for (const [name, value] of Object.entries(props)) pushJsxAttr(chunks, values, name, value);
|
|
439
|
+
chunks[chunks.length - 1] += ">";
|
|
440
|
+
if (!VOID_ELEMENTS.has(type)) {
|
|
441
|
+
for (const child of children) {
|
|
442
|
+
values.push(child);
|
|
443
|
+
chunks.push("");
|
|
444
|
+
}
|
|
445
|
+
chunks[chunks.length - 1] += `</${type}>`;
|
|
446
|
+
} else if (children.length > 0 && __DEV__) warn(`Void element <${type}> should not have children. They will be ignored.`);
|
|
447
|
+
return ilhaHtml(chunks, ...values);
|
|
448
|
+
}
|
|
449
|
+
function ilhaJsx(type, props, maybeKey, ...restChildren) {
|
|
450
|
+
const hasKeyArg = typeof maybeKey === "string" || typeof maybeKey === "number";
|
|
451
|
+
const keyFromArg = hasKeyArg ? maybeKey : void 0;
|
|
452
|
+
const normalizedChildren = normalizeJsxChildren(props, hasKeyArg || maybeKey === void 0 ? restChildren : [maybeKey, ...restChildren]);
|
|
453
|
+
if (typeof type === "function") {
|
|
454
|
+
const slotKey = extractJsxSlotKey(props, typeof keyFromArg === "string" || typeof keyFromArg === "number" ? keyFromArg : void 0);
|
|
455
|
+
const componentProps = {
|
|
456
|
+
...props ?? {},
|
|
457
|
+
...normalizedChildren.length > 0 ? { children: normalizedChildren } : {}
|
|
458
|
+
};
|
|
459
|
+
delete componentProps["key"];
|
|
460
|
+
const out = type(Object.keys(componentProps).length ? componentProps : {});
|
|
461
|
+
if (isIslandCall(out)) {
|
|
462
|
+
if (slotKey !== void 0) out.key = slotKey;
|
|
463
|
+
return ilhaHtml`${out}`;
|
|
464
|
+
}
|
|
465
|
+
if (isRawHtml(out)) return out;
|
|
466
|
+
if (typeof out === "string" && isIsland(type)) return ilhaRaw(emitIslandSlot(type, componentProps, slotKey));
|
|
467
|
+
if (typeof out === "string") return ilhaHtml`${out}`;
|
|
468
|
+
return ilhaHtml`${out}`;
|
|
469
|
+
}
|
|
470
|
+
return renderJsxElement(type, props, normalizedChildren);
|
|
471
|
+
}
|
|
472
|
+
function ilhaFragment(props, ...children) {
|
|
473
|
+
const normalizedChildren = normalizeJsxChildren(props, children);
|
|
474
|
+
return ilhaHtml(["", ...normalizedChildren.map(() => "")], ...normalizedChildren);
|
|
475
|
+
}
|
|
476
|
+
function ilhaJsxDEV(type, props, maybeKey, _source, _self) {
|
|
477
|
+
return ilhaJsx(type, props, maybeKey);
|
|
478
|
+
}
|
|
194
479
|
function interpolateValue(v) {
|
|
195
|
-
if (v == null) return "";
|
|
480
|
+
if (v == null || v === true || v === false) return "";
|
|
196
481
|
if (Array.isArray(v)) return v.map(interpolateValue).join("");
|
|
197
|
-
if (
|
|
482
|
+
if (isRawHtml(v)) return v.value;
|
|
198
483
|
if (isIslandCall(v)) {
|
|
199
484
|
const call = typeof v === "function" ? v() : v;
|
|
200
485
|
return emitIslandSlot(call.island, call.props, call.key);
|
|
@@ -216,7 +501,11 @@ const BIND_VALID_KINDS = new Set([
|
|
|
216
501
|
]);
|
|
217
502
|
const BIND_PREFIX_RE = /\bbind:([a-zA-Z]+)\s*=\s*("|')?$/;
|
|
218
503
|
function formatDateForInput(d) {
|
|
219
|
-
if (d instanceof Date
|
|
504
|
+
if (d instanceof Date) {
|
|
505
|
+
if (isNaN(d.getTime())) {
|
|
506
|
+
if (__DEV__) warn("bind:valueAsDate received an invalid Date object — value attribute will be empty.");
|
|
507
|
+
return "";
|
|
508
|
+
}
|
|
220
509
|
const pad = (n) => String(n).padStart(2, "0");
|
|
221
510
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
222
511
|
}
|
|
@@ -316,8 +605,15 @@ function ilhaHtml(strings, ...values) {
|
|
|
316
605
|
value: dedentString(result)
|
|
317
606
|
};
|
|
318
607
|
}
|
|
608
|
+
/** Duck-type RawHtml — works across duplicate `ilha` bundles (distinct RAW symbols). */
|
|
609
|
+
function isRawHtml(v) {
|
|
610
|
+
if (typeof v !== "object" || v === null || !("value" in v)) return false;
|
|
611
|
+
if (typeof v.value !== "string") return false;
|
|
612
|
+
if (RAW in v) return true;
|
|
613
|
+
return Object.getOwnPropertySymbols(v).some((s) => s.description === "ilha.raw");
|
|
614
|
+
}
|
|
319
615
|
function unwrapHtml(v) {
|
|
320
|
-
return
|
|
616
|
+
return isRawHtml(v) ? v.value : v;
|
|
321
617
|
}
|
|
322
618
|
const contextRegistry = /* @__PURE__ */ new Map();
|
|
323
619
|
function ilhaContext(key, initial) {
|
|
@@ -1045,10 +1341,38 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1045
1341
|
}
|
|
1046
1342
|
}
|
|
1047
1343
|
const mountedSlots = /* @__PURE__ */ new Map();
|
|
1344
|
+
const leavingSlots = /* @__PURE__ */ new Map();
|
|
1345
|
+
function teardownMountedSlot(id, entry) {
|
|
1346
|
+
mountedSlots.delete(id);
|
|
1347
|
+
const token = Symbol();
|
|
1348
|
+
const result = entry.unmount();
|
|
1349
|
+
const remove = (stub) => {
|
|
1350
|
+
const leaving = leavingSlots.get(id);
|
|
1351
|
+
if (!leaving || leaving.token !== token) return;
|
|
1352
|
+
leaving.el.remove();
|
|
1353
|
+
stub?.remove();
|
|
1354
|
+
leavingSlots.delete(id);
|
|
1355
|
+
};
|
|
1356
|
+
if (result instanceof Promise) {
|
|
1357
|
+
let stub;
|
|
1358
|
+
if (entry.el.isConnected) {
|
|
1359
|
+
stub = document.createElement("div");
|
|
1360
|
+
stub.setAttribute(SLOT_ATTR, id);
|
|
1361
|
+
entry.el.replaceWith(stub);
|
|
1362
|
+
}
|
|
1363
|
+
leavingSlots.set(id, {
|
|
1364
|
+
el: entry.el,
|
|
1365
|
+
token
|
|
1366
|
+
});
|
|
1367
|
+
result.finally(() => remove(stub));
|
|
1368
|
+
} else entry.el.remove();
|
|
1369
|
+
}
|
|
1048
1370
|
function findSlot(id) {
|
|
1371
|
+
const leavingEls = new Set([...leavingSlots.values()].map((l) => l.el));
|
|
1049
1372
|
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
|
|
1050
1373
|
const candidates = host.querySelectorAll(`[${SLOT_ATTR}="${escaped}"]`);
|
|
1051
1374
|
for (const candidate of candidates) {
|
|
1375
|
+
if (leavingEls.has(candidate)) continue;
|
|
1052
1376
|
let el = candidate;
|
|
1053
1377
|
while (el && el !== host) {
|
|
1054
1378
|
if (el.hasAttribute("data-ilha")) break;
|
|
@@ -1059,10 +1383,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1059
1383
|
return null;
|
|
1060
1384
|
}
|
|
1061
1385
|
function mountSlots(slotMap) {
|
|
1062
|
-
for (const [id, entry] of mountedSlots) if (!slotMap.has(id))
|
|
1063
|
-
entry.unmount();
|
|
1064
|
-
mountedSlots.delete(id);
|
|
1065
|
-
}
|
|
1386
|
+
for (const [id, entry] of mountedSlots) if (!slotMap.has(id)) teardownMountedSlot(id, entry);
|
|
1066
1387
|
for (const [id, { island: childIsland, props }] of slotMap) {
|
|
1067
1388
|
const existing = mountedSlots.get(id);
|
|
1068
1389
|
if (existing) {
|
|
@@ -1113,6 +1434,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1113
1434
|
listeners.splice(0, listeners.length, ...listeners.filter((l) => l.entry !== entry));
|
|
1114
1435
|
}
|
|
1115
1436
|
const eventTarget = event.target instanceof Element ? event.target : listenerTarget;
|
|
1437
|
+
const handlerTarget = entry.selector === "" ? eventTarget : listenerTarget;
|
|
1116
1438
|
let handlerSignal;
|
|
1117
1439
|
if (entry.abortable) {
|
|
1118
1440
|
let entryMap = invocationControllers.get(entry);
|
|
@@ -1134,7 +1456,7 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1134
1456
|
derived,
|
|
1135
1457
|
input,
|
|
1136
1458
|
host,
|
|
1137
|
-
target:
|
|
1459
|
+
target: handlerTarget,
|
|
1138
1460
|
event,
|
|
1139
1461
|
signal: handlerSignal
|
|
1140
1462
|
});
|
|
@@ -1175,11 +1497,11 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1175
1497
|
input
|
|
1176
1498
|
}), hasExistingContent ? host : void 0);
|
|
1177
1499
|
if (!hasExistingContent) host.innerHTML = stylePrefix + initial.html;
|
|
1178
|
-
attachListeners();
|
|
1179
1500
|
let stopBindings = applyTemplateBindings(host, initial.binds);
|
|
1180
1501
|
cleanups.push(() => stopBindings());
|
|
1181
1502
|
mountSlots(initial.slots);
|
|
1182
1503
|
cleanups.push(() => mountedSlots.forEach((entry) => entry.unmount()));
|
|
1504
|
+
attachListeners();
|
|
1183
1505
|
if (!shouldSkipOnMount) for (const entry of onMounts) {
|
|
1184
1506
|
const prevSub = setActiveSub(void 0);
|
|
1185
1507
|
let userCleanup;
|
|
@@ -1249,19 +1571,34 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1249
1571
|
if (!initialized) {
|
|
1250
1572
|
initialized = true;
|
|
1251
1573
|
if (rendered === initialRenderedHtml) return;
|
|
1574
|
+
if (mountedSlots.size > 0 && mountedSlots.size === newSlotMap.size && [...newSlotMap.keys()].every((id) => mountedSlots.has(id)) && isStableInlineSlotMount(initialRenderedHtml, rendered, newSlotMap.keys())) return;
|
|
1252
1575
|
}
|
|
1253
1576
|
detachListeners();
|
|
1254
1577
|
stopBindings();
|
|
1578
|
+
const prevMountedCount = mountedSlots.size;
|
|
1579
|
+
const removedSlotIds = [];
|
|
1255
1580
|
for (const [id, entry] of mountedSlots) if (!newSlotMap.has(id)) {
|
|
1256
|
-
entry
|
|
1257
|
-
|
|
1581
|
+
teardownMountedSlot(id, entry);
|
|
1582
|
+
removedSlotIds.push(id);
|
|
1583
|
+
}
|
|
1584
|
+
for (const id of removedSlotIds) {
|
|
1585
|
+
if (leavingSlots.has(id)) continue;
|
|
1586
|
+
const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, "\\$&");
|
|
1587
|
+
for (const el of host.querySelectorAll(`[${SLOT_ATTR}="${escaped}"]`)) el.remove();
|
|
1258
1588
|
}
|
|
1259
1589
|
const preserved = /* @__PURE__ */ new Map();
|
|
1590
|
+
const listShrunk = newSlotMap.size < prevMountedCount;
|
|
1260
1591
|
for (const [id, entry] of mountedSlots) {
|
|
1261
1592
|
if (!newSlotMap.has(id)) continue;
|
|
1262
1593
|
if (!entry.el.isConnected) continue;
|
|
1594
|
+
if (listShrunk && id.startsWith("p:")) {
|
|
1595
|
+
teardownMountedSlot(id, entry);
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1263
1598
|
const stub = document.createElement("div");
|
|
1264
1599
|
stub.setAttribute(SLOT_ATTR, id);
|
|
1600
|
+
const incomingProps = serializeSlotProps(newSlotMap.get(id)?.props);
|
|
1601
|
+
if (incomingProps !== "") stub.setAttribute(PROPS_ATTR, incomingProps);
|
|
1265
1602
|
entry.el.replaceWith(stub);
|
|
1266
1603
|
preserved.set(id, entry.el);
|
|
1267
1604
|
}
|
|
@@ -1284,16 +1621,23 @@ Element: ${host.outerHTML.slice(0, 120)}`);
|
|
|
1284
1621
|
if (__DEV__ && _mountedHosts) _mountedHosts.delete(host);
|
|
1285
1622
|
stopRender();
|
|
1286
1623
|
detachListeners();
|
|
1624
|
+
const pending = [];
|
|
1625
|
+
for (const [, entry] of mountedSlots) {
|
|
1626
|
+
const result = entry.unmount();
|
|
1627
|
+
if (result instanceof Promise) pending.push(result);
|
|
1628
|
+
}
|
|
1287
1629
|
if (transition?.leave) {
|
|
1288
1630
|
const result = transition.leave(host);
|
|
1289
|
-
if (result instanceof Promise)
|
|
1290
|
-
result.then(() => {
|
|
1291
|
-
for (const c of cleanups) c();
|
|
1292
|
-
}).catch(console.error);
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1631
|
+
if (result instanceof Promise) pending.push(result);
|
|
1295
1632
|
}
|
|
1296
|
-
|
|
1633
|
+
const finish = () => {
|
|
1634
|
+
for (const c of cleanups) c();
|
|
1635
|
+
};
|
|
1636
|
+
if (pending.length > 0) return Promise.all(pending).then(finish).catch((err) => {
|
|
1637
|
+
console.error(err);
|
|
1638
|
+
finish();
|
|
1639
|
+
});
|
|
1640
|
+
finish();
|
|
1297
1641
|
};
|
|
1298
1642
|
const updateProps = (nextProps) => {
|
|
1299
1643
|
if (tornDown) return;
|
|
@@ -1453,7 +1797,14 @@ function mountAll(registry, options = {}) {
|
|
|
1453
1797
|
io.disconnect();
|
|
1454
1798
|
});
|
|
1455
1799
|
} else els.forEach(activateEl);
|
|
1456
|
-
return { unmount: () =>
|
|
1800
|
+
return { unmount: () => {
|
|
1801
|
+
const pending = [];
|
|
1802
|
+
for (const u of unmounts) {
|
|
1803
|
+
const result = u();
|
|
1804
|
+
if (result instanceof Promise) pending.push(result);
|
|
1805
|
+
}
|
|
1806
|
+
if (pending.length > 0) return Promise.all(pending).then(() => {});
|
|
1807
|
+
} };
|
|
1457
1808
|
}
|
|
1458
1809
|
const rootBuilder = new IlhaBuilder({
|
|
1459
1810
|
schema: null,
|
|
@@ -1469,6 +1820,10 @@ const rootBuilder = new IlhaBuilder({
|
|
|
1469
1820
|
const ilha = Object.assign(rootBuilder, {
|
|
1470
1821
|
html: ilhaHtml,
|
|
1471
1822
|
raw: ilhaRaw,
|
|
1823
|
+
jsx: ilhaJsx,
|
|
1824
|
+
jsxs: ilhaJsx,
|
|
1825
|
+
jsxDEV: ilhaJsxDEV,
|
|
1826
|
+
Fragment: ilhaFragment,
|
|
1472
1827
|
mount: mountAll,
|
|
1473
1828
|
from: ilhaFrom,
|
|
1474
1829
|
context: ilhaContext,
|
|
@@ -1478,9 +1833,13 @@ const ilha = Object.assign(rootBuilder, {
|
|
|
1478
1833
|
});
|
|
1479
1834
|
const html = ilhaHtml;
|
|
1480
1835
|
const raw = ilhaRaw;
|
|
1836
|
+
const jsx = ilhaJsx;
|
|
1837
|
+
const jsxs = ilhaJsx;
|
|
1838
|
+
const jsxDEV = ilhaJsxDEV;
|
|
1839
|
+
const Fragment = ilhaFragment;
|
|
1481
1840
|
const css = ilhaCss;
|
|
1482
1841
|
const mount = mountAll;
|
|
1483
1842
|
const from = ilhaFrom;
|
|
1484
1843
|
const context = ilhaContext;
|
|
1485
1844
|
//#endregion
|
|
1486
|
-
export { ISLAND_MOUNT_INTERNAL, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, mount, raw, untrack };
|
|
1845
|
+
export { Fragment, ISLAND_MOUNT_INTERNAL, batch, context, css, ilha as default, from, html, ilhaSignal, ilhaSignal as signal, jsx, jsxDEV, jsxs, mount, raw, untrack };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RawHtml } from "./index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/jsx-types.d.ts
|
|
4
|
+
declare namespace JSX {
|
|
5
|
+
type Child = unknown;
|
|
6
|
+
type ElementType = string | ((props: any) => any);
|
|
7
|
+
interface Element extends RawHtml {}
|
|
8
|
+
interface ElementChildrenAttribute {
|
|
9
|
+
children: {};
|
|
10
|
+
}
|
|
11
|
+
interface IntrinsicAttributes {
|
|
12
|
+
key?: string | number;
|
|
13
|
+
}
|
|
14
|
+
type IntrinsicElementProps = {
|
|
15
|
+
children?: Child;
|
|
16
|
+
class?: string | unknown[] | Record<string, boolean>;
|
|
17
|
+
className?: string | unknown[] | Record<string, boolean>;
|
|
18
|
+
htmlFor?: string;
|
|
19
|
+
[name: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
interface IntrinsicElements {
|
|
22
|
+
[name: string]: IntrinsicElementProps;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { JSX as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ilha",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A tiny, framework-free island architecture library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryuz <ryuzer@proton.me>",
|
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
".": {
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./jsx-runtime": {
|
|
24
|
+
"types": "./dist/jsx-runtime.d.ts",
|
|
25
|
+
"import": "./dist/jsx-runtime.js"
|
|
26
|
+
},
|
|
27
|
+
"./jsx-dev-runtime": {
|
|
28
|
+
"types": "./dist/jsx-dev-runtime.d.ts",
|
|
29
|
+
"import": "./dist/jsx-dev-runtime.js"
|
|
22
30
|
}
|
|
23
31
|
},
|
|
24
32
|
"scripts": {
|