@llui/dom 0.0.22 → 0.0.24
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 +26 -21
- package/dist/dom-env.d.ts +67 -0
- package/dist/dom-env.d.ts.map +1 -0
- package/dist/dom-env.js +61 -0
- package/dist/dom-env.js.map +1 -0
- package/dist/el-split.d.ts.map +1 -1
- package/dist/el-split.js +4 -4
- package/dist/el-split.js.map +1 -1
- package/dist/el-template.d.ts +14 -1
- package/dist/el-template.d.ts.map +1 -1
- package/dist/el-template.js +41 -7
- package/dist/el-template.js.map +1 -1
- package/dist/elements.js +2 -2
- package/dist/elements.js.map +1 -1
- package/dist/hmr.d.ts.map +1 -1
- package/dist/hmr.js +1 -0
- package/dist/hmr.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mathml-elements.js +2 -2
- package/dist/mathml-elements.js.map +1 -1
- package/dist/mount.d.ts +8 -0
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +14 -9
- package/dist/mount.js.map +1 -1
- package/dist/primitives/branch.js +1 -1
- package/dist/primitives/branch.js.map +1 -1
- package/dist/primitives/child.d.ts.map +1 -1
- package/dist/primitives/child.js +5 -2
- package/dist/primitives/child.js.map +1 -1
- package/dist/primitives/client-only.d.ts +108 -0
- package/dist/primitives/client-only.d.ts.map +1 -0
- package/dist/primitives/client-only.js +110 -0
- package/dist/primitives/client-only.js.map +1 -0
- package/dist/primitives/each.d.ts.map +1 -1
- package/dist/primitives/each.js +19 -9
- package/dist/primitives/each.js.map +1 -1
- package/dist/primitives/foreign.d.ts.map +1 -1
- package/dist/primitives/foreign.js +83 -44
- package/dist/primitives/foreign.js.map +1 -1
- package/dist/primitives/lazy.js +2 -2
- package/dist/primitives/lazy.js.map +1 -1
- package/dist/primitives/selector.js +1 -1
- package/dist/primitives/selector.js.map +1 -1
- package/dist/primitives/slice.d.ts.map +1 -1
- package/dist/primitives/slice.js +14 -0
- package/dist/primitives/slice.js.map +1 -1
- package/dist/primitives/text.js +3 -3
- package/dist/primitives/text.js.map +1 -1
- package/dist/primitives/unsafe-html.js +11 -13
- package/dist/primitives/unsafe-html.js.map +1 -1
- package/dist/primitives/virtual-each.d.ts.map +1 -1
- package/dist/primitives/virtual-each.js +5 -3
- package/dist/primitives/virtual-each.js.map +1 -1
- package/dist/render-context.d.ts +9 -0
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js.map +1 -1
- package/dist/ssr/jsdom.d.ts +18 -0
- package/dist/ssr/jsdom.d.ts.map +1 -0
- package/dist/ssr/jsdom.js +38 -0
- package/dist/ssr/jsdom.js.map +1 -0
- package/dist/ssr/legacy.d.ts +25 -0
- package/dist/ssr/legacy.d.ts.map +1 -0
- package/dist/ssr/legacy.js +61 -0
- package/dist/ssr/legacy.js.map +1 -0
- package/dist/ssr/linkedom.d.ts +17 -0
- package/dist/ssr/linkedom.d.ts.map +1 -0
- package/dist/ssr/linkedom.js +36 -0
- package/dist/ssr/linkedom.js.map +1 -0
- package/dist/ssr-dom.d.ts +11 -7
- package/dist/ssr-dom.d.ts.map +1 -1
- package/dist/ssr-dom.js +10 -31
- package/dist/ssr-dom.js.map +1 -1
- package/dist/ssr.d.ts +10 -5
- package/dist/ssr.d.ts.map +1 -1
- package/dist/ssr.js +11 -7
- package/dist/ssr.js.map +1 -1
- package/dist/svg-elements.js +2 -2
- package/dist/svg-elements.js.map +1 -1
- package/dist/types.d.ts +27 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/update-loop.d.ts +3 -1
- package/dist/update-loop.d.ts.map +1 -1
- package/dist/update-loop.js +15 -1
- package/dist/update-loop.js.map +1 -1
- package/dist/view-helpers.d.ts +7 -0
- package/dist/view-helpers.d.ts.map +1 -1
- package/dist/view-helpers.js +2 -0
- package/dist/view-helpers.js.map +1 -1
- package/package.json +25 -1
- package/dist/scope.d.ts +0 -39
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -250
- package/dist/scope.js.map +0 -1
package/dist/primitives/slice.js
CHANGED
|
@@ -7,6 +7,7 @@ import { unsafeHtml as _unsafeHtml } from './unsafe-html.js';
|
|
|
7
7
|
import { memo as _memo } from './memo.js';
|
|
8
8
|
import { sample as _sample } from './sample.js';
|
|
9
9
|
import { selector as _selector } from './selector.js';
|
|
10
|
+
import { clientOnly as _clientOnly } from './client-only.js';
|
|
10
11
|
import { useContext } from './context.js';
|
|
11
12
|
/**
|
|
12
13
|
* Build a `View<Sub, M>` that composes a selector into every state-bound
|
|
@@ -67,6 +68,15 @@ export function slice(h, lift) {
|
|
|
67
68
|
each: (opts) => _each({
|
|
68
69
|
...opts,
|
|
69
70
|
items: (r) => opts.items(lift(r)),
|
|
71
|
+
// The render bag carries a View<S, M> — for the inner Root-typed
|
|
72
|
+
// each we need to translate Root → Sub by re-slicing h.
|
|
73
|
+
render: (rootBag) => {
|
|
74
|
+
const subBag = {
|
|
75
|
+
...rootBag,
|
|
76
|
+
h: slice(rootBag.h, lift),
|
|
77
|
+
};
|
|
78
|
+
return opts.render(subBag);
|
|
79
|
+
},
|
|
70
80
|
}),
|
|
71
81
|
text: (accessor, mask) => {
|
|
72
82
|
if (typeof accessor === 'string')
|
|
@@ -88,6 +98,10 @@ export function slice(h, lift) {
|
|
|
88
98
|
return (s) => root(s);
|
|
89
99
|
},
|
|
90
100
|
sample: (selector) => _sample((r) => selector(lift(r))),
|
|
101
|
+
clientOnly: (opts) => _clientOnly({
|
|
102
|
+
render: wrapCase(opts.render),
|
|
103
|
+
fallback: opts.fallback ? wrapCase(opts.fallback) : undefined,
|
|
104
|
+
}),
|
|
91
105
|
};
|
|
92
106
|
}
|
|
93
107
|
//# sourceMappingURL=slice.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slice.js","sourceRoot":"","sources":["../../src/primitives/slice.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,IAAI,KAAK,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,KAAK,IAAI,MAAM,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,IAAI,IAAI,KAAK,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,IAAI,IAAI,KAAK,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC5D,OAAO,EAAE,IAAI,IAAI,KAAK,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,QAAQ,IAAI,SAAS,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAA;AAEvD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,KAAK,CACnB,CAAoC,EACpC,IAAsB;IAEtB,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAA;IAEnB,wEAAwE;IACxE,yEAAyE;IACzE,0DAA0D;IAC1D,MAAM,QAAQ,GACZ,CAAC,EAA+B,EAAE,EAAE,CACpC,CAAC,KAAoB,EAAU,EAAE,CAC/B,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;IAE1B,MAAM,SAAS,GAAG,CAChB,KAAkD,EACJ,EAAE;QAChD,MAAM,GAAG,GAAiD,EAAE,CAAA;QAC5D,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACrC,GAAG,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAE,CAAC,CAAA;QAClC,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC,CAAA;IAED,OAAO;QACL,IAAI;QACJ,IAAI,EAAE,CAAC,IAAyB,EAAE,EAAE,CAClC,KAAK,CAAU;YACb,GAAG,IAAI;YACP,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/B,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;YAC7B,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;SAC9D,CAAC;QACJ,MAAM,EAAE,CAAmB,IAA8B,EAAE,EAAE;YAC3D,kEAAkE;YAClE,kEAAkE;YAClE,kEAAkE;YAClE,0BAA0B;YAC1B,MAAM,OAAO,GAAG,IAIf,CAAA;YACD,OAAO,OAAO,CAAU;gBACtB,GAAI,OAA6C;gBACjD,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC9B,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS;gBAC3D,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;aACvC,CAAC,CAAA;QAC9B,CAAC;QACD,KAAK,EAAE,CAAC,IAA0B,EAAE,EAAE,CACpC,MAAM,CAAU;YACd,GAAG,IAAI;YACP,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC3B,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;SAC9B,CAAC;QACJ,IAAI,EAAE,CAAI,IAA4B,EAAE,EAAE,CACxC,KAAK,CAAa;YAChB,GAAG,IAAI;YACP,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"slice.js","sourceRoot":"","sources":["../../src/primitives/slice.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Send, EachOptions, ShowOptions, BranchOptions, ScopeOptions } from '../types.js'\nimport type { View } from '../view-helpers.js'\nimport { show as _show } from './show.js'\nimport { branch as _branch } from './branch.js'\nimport { scope as _scope } from './scope.js'\nimport { each as _each } from './each.js'\nimport { text as _text } from './text.js'\nimport { unsafeHtml as _unsafeHtml } from './unsafe-html.js'\nimport { memo as _memo } from './memo.js'\nimport { sample as _sample } from './sample.js'\nimport { selector as _selector } from './selector.js'\nimport { clientOnly as _clientOnly, type ClientOnlyOptions } from './client-only.js'\nimport { useContext, type Context } from './context.js'\n\n/**\n * Build a `View<Sub, M>` that composes a selector into every state-bound\n * accessor. Used to write view-functions over a sub-slice of parent state:\n *\n * ```ts\n * import { slice } from '@llui/dom'\n *\n * view: (h) => {\n * const formView = slice(h, (s) => s.form)\n * return [...formView.show({ when: f => f.valid, render: (h) => [...] })]\n * }\n * ```\n *\n * Kept as a standalone function rather than a method on the View bundle so\n * apps that don't use it don't pay for its bundle cost — tree-shaken when\n * unused.\n */\nexport function slice<Root, Sub, M>(\n h: View<Root, M> | { send: Send<M> },\n lift: (r: Root) => Sub,\n): View<Sub, M> {\n const send = h.send\n\n // Wrap a Sub-typed case callback to work with the Root-typed primitive.\n // The inner callback receives a View<Root, M> from the primitive, and we\n // narrow it to View<Sub, M> via a recursive slice() call.\n const wrapCase =\n (fn: (h: View<Sub, M>) => Node[]) =>\n (rootH: View<Root, M>): Node[] =>\n fn(slice(rootH, lift))\n\n const wrapCases = (\n cases: Record<string, (h: View<Sub, M>) => Node[]>,\n ): Record<string, (h: View<Root, M>) => Node[]> => {\n const out: Record<string, (h: View<Root, M>) => Node[]> = {}\n for (const key of Object.keys(cases)) {\n out[key] = wrapCase(cases[key]!)\n }\n return out\n }\n\n return {\n send,\n show: (opts: ShowOptions<Sub, M>) =>\n _show<Root, M>({\n ...opts,\n when: (r) => opts.when(lift(r)),\n render: wrapCase(opts.render),\n fallback: opts.fallback ? wrapCase(opts.fallback) : undefined,\n }),\n branch: <K extends string>(opts: BranchOptions<Sub, M, K>) => {\n // Re-typed to the wide form internally because we're transforming\n // the options before handing them to the real `branch`. The inner\n // call loses exhaustiveness narrowing — it's been enforced at the\n // slice boundary already.\n const anyOpts = opts as unknown as {\n on: (s: Sub) => K\n cases?: Record<string, (h: View<Sub, M>) => Node[]>\n default?: (h: View<Sub, M>) => Node[]\n }\n return _branch<Root, M>({\n ...(anyOpts as unknown as BranchOptions<Root, M>),\n on: (r) => anyOpts.on(lift(r)),\n cases: anyOpts.cases ? wrapCases(anyOpts.cases) : undefined,\n default: anyOpts.default ? wrapCase(anyOpts.default) : undefined,\n } as BranchOptions<Root, M>)\n },\n scope: (opts: ScopeOptions<Sub, M>) =>\n _scope<Root, M>({\n ...opts,\n on: (r) => opts.on(lift(r)),\n render: wrapCase(opts.render),\n }),\n each: <T>(opts: EachOptions<Sub, T, M>) =>\n _each<Root, T, M>({\n ...opts,\n items: (r) => opts.items(lift(r)),\n // The render bag carries a View<S, M> — for the inner Root-typed\n // each we need to translate Root → Sub by re-slicing h.\n render: (rootBag) => {\n const subBag = {\n ...rootBag,\n h: slice(rootBag.h, lift),\n } as unknown as Parameters<(typeof opts)['render']>[0]\n return opts.render(subBag)\n },\n }),\n text: (accessor, mask) => {\n if (typeof accessor === 'string') return _text(accessor)\n return _text<Root>((r) => accessor(lift(r)), mask)\n },\n unsafeHtml: (accessor, mask) => {\n if (typeof accessor === 'string') return _unsafeHtml(accessor)\n return _unsafeHtml<Root>((r) => accessor(lift(r)), mask)\n },\n memo: <T>(accessor: (s: Sub) => T) => {\n const m = _memo<Root, T>((r) => accessor(lift(r)))\n return (s: Sub) => m(s as unknown as Root)\n },\n selector: <V>(field: (s: Sub) => V) => _selector<Root, V>((r) => field(lift(r))),\n ctx: <T>(c: Context<T>) => {\n const root = useContext<Root, T>(c)\n return (s: Sub) => root(s as unknown as Root)\n },\n sample: <R>(selector: (s: Sub) => R) => _sample<Root, R>((r) => selector(lift(r))),\n clientOnly: (opts: ClientOnlyOptions<Sub, M>) =>\n _clientOnly<Root, M>({\n render: wrapCase(opts.render),\n fallback: opts.fallback ? wrapCase(opts.fallback) : undefined,\n }),\n }\n}\n"]}
|
package/dist/primitives/text.js
CHANGED
|
@@ -3,11 +3,11 @@ import { createBinding } from '../binding.js';
|
|
|
3
3
|
import { addCheckedItemUpdater } from '../lifetime.js';
|
|
4
4
|
import { FULL_MASK } from '../update-loop.js';
|
|
5
5
|
export function text(accessor, mask) {
|
|
6
|
+
const ctx = getRenderContext('text');
|
|
6
7
|
if (typeof accessor === 'string') {
|
|
7
|
-
return
|
|
8
|
+
return ctx.dom.createTextNode(accessor);
|
|
8
9
|
}
|
|
9
|
-
const
|
|
10
|
-
const node = document.createTextNode('');
|
|
10
|
+
const node = ctx.dom.createTextNode('');
|
|
11
11
|
// Per-item accessor from each() — zero-arg function (length === 0)
|
|
12
12
|
// Register as direct updater, bypassing Phase 2 binding scan
|
|
13
13
|
if (accessor.length === 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/primitives/text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAE7C,MAAM,UAAU,IAAI,CAClB,QAAsD,EACtD,IAAa;IAEb,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/primitives/text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAE7C,MAAM,UAAU,IAAI,CAClB,QAAsD,EACtD,IAAa;IAEb,MAAM,GAAG,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACpC,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAA;IACzC,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC,CAAA;IAEvC,mEAAmE;IACnE,6DAA6D;IAC7D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,QAAwB,CAAA;QACpC,MAAM,OAAO,GAAG,qBAAqB,CACnC,GAAG,CAAC,YAAY,EAChB,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,EACnB,CAAC,CAAC,EAAE,EAAE;YACJ,IAAI,CAAC,SAAS,GAAG,CAAC,CAAA;QACpB,CAAC,CACF,CAAA;QACD,IAAI,CAAC,SAAS,GAAG,OAAO,CAAA;QACxB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,iCAAiC;IACjC,MAAM,WAAW,GAAG,IAAI,IAAI,SAAS,CAAA;IACrC,MAAM,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,YAAY,EAAE;QAC9C,IAAI,EAAE,WAAW;QACjB,QAAQ,EAAE,QAAqC;QAC/C,IAAI,EAAE,MAAM;QACZ,IAAI;QACJ,OAAO,EAAE,KAAK;KACf,CAAC,CAAA;IACF,MAAM,YAAY,GAAI,QAA6B,CAAC,GAAG,CAAC,KAAU,CAAC,CAAA;IACnE,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,CAAA;IACrC,OAAO,CAAC,SAAS,GAAG,YAAY,CAAA;IAEhC,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import { getRenderContext } from '../render-context.js'\nimport { createBinding } from '../binding.js'\nimport { addCheckedItemUpdater } from '../lifetime.js'\nimport { FULL_MASK } from '../update-loop.js'\n\nexport function text<S>(\n accessor: ((s: S) => string) | (() => string) | string,\n mask?: number,\n): Text {\n const ctx = getRenderContext('text')\n if (typeof accessor === 'string') {\n return ctx.dom.createTextNode(accessor)\n }\n\n const node = ctx.dom.createTextNode('')\n\n // Per-item accessor from each() — zero-arg function (length === 0)\n // Register as direct updater, bypassing Phase 2 binding scan\n if (accessor.length === 0) {\n const get = accessor as () => string\n const initial = addCheckedItemUpdater(\n ctx.rootLifetime,\n () => String(get()),\n (v) => {\n node.nodeValue = v\n },\n )\n node.nodeValue = initial\n return node\n }\n\n // Component-level state accessor\n const bindingMask = mask ?? FULL_MASK\n const binding = createBinding(ctx.rootLifetime, {\n mask: bindingMask,\n accessor: accessor as (state: never) => unknown,\n kind: 'text',\n node,\n perItem: false,\n })\n const initialValue = (accessor as (s: S) => string)(ctx.state as S)\n node.nodeValue = String(initialValue)\n binding.lastValue = initialValue\n\n return node\n}\n"]}
|
|
@@ -27,15 +27,15 @@ import { FULL_MASK } from '../update-loop.js';
|
|
|
27
27
|
* is set. Defaults to FULL_MASK for hand-written components.
|
|
28
28
|
*/
|
|
29
29
|
export function unsafeHtml(accessor, mask) {
|
|
30
|
+
const ctx = getRenderContext('unsafeHtml');
|
|
30
31
|
if (typeof accessor === 'string') {
|
|
31
|
-
return parseHtmlToNodes(accessor);
|
|
32
|
+
return parseHtmlToNodes(ctx, accessor);
|
|
32
33
|
}
|
|
33
|
-
const ctx = getRenderContext('unsafeHtml');
|
|
34
34
|
const blocks = ctx.structuralBlocks;
|
|
35
35
|
const blockMask = mask ?? FULL_MASK;
|
|
36
|
-
const anchor =
|
|
36
|
+
const anchor = ctx.dom.createComment('unsafeHtml');
|
|
37
37
|
let currentHtml = accessor(ctx.state);
|
|
38
|
-
let currentNodes = parseHtmlToNodes(currentHtml);
|
|
38
|
+
let currentNodes = parseHtmlToNodes(ctx, currentHtml);
|
|
39
39
|
const block = {
|
|
40
40
|
mask: blockMask,
|
|
41
41
|
reconcile(state) {
|
|
@@ -49,7 +49,7 @@ export function unsafeHtml(accessor, mask) {
|
|
|
49
49
|
if (!parent)
|
|
50
50
|
return;
|
|
51
51
|
const oldNodes = currentNodes;
|
|
52
|
-
currentNodes = parseHtmlToNodes(newHtml);
|
|
52
|
+
currentNodes = parseHtmlToNodes(ctx, newHtml);
|
|
53
53
|
currentHtml = newHtml;
|
|
54
54
|
// The anchor sits immediately before the current node range, so
|
|
55
55
|
// `anchor.nextSibling` is the first old node (or whatever follows
|
|
@@ -69,14 +69,12 @@ export function unsafeHtml(accessor, mask) {
|
|
|
69
69
|
blocks.push(block);
|
|
70
70
|
return [anchor, ...currentNodes];
|
|
71
71
|
}
|
|
72
|
-
function parseHtmlToNodes(html) {
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
template.innerHTML = html;
|
|
78
|
-
// Snapshot — moving nodes to a parent drains template.content, but
|
|
72
|
+
function parseHtmlToNodes(ctx, html) {
|
|
73
|
+
// Delegates to the env — jsdom/linkedom adapters may implement this
|
|
74
|
+
// via DOMParser instead of the browser `<template>` trick, but both
|
|
75
|
+
// return an inert DocumentFragment that doesn't execute scripts.
|
|
76
|
+
// Snapshot — moving nodes to a parent drains fragment.childNodes, but
|
|
79
77
|
// callers hold this array and we iterate it on reconcile.
|
|
80
|
-
return Array.from(
|
|
78
|
+
return Array.from(ctx.dom.parseHtmlFragment(html).childNodes);
|
|
81
79
|
}
|
|
82
80
|
//# sourceMappingURL=unsafe-html.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unsafe-html.js","sourceRoot":"","sources":["../../src/primitives/unsafe-html.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,UAAU,CAAI,QAAqC,EAAE,IAAa;IAChF,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"unsafe-html.js","sourceRoot":"","sources":["../../src/primitives/unsafe-html.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,UAAU,CAAI,QAAqC,EAAE,IAAa;IAChF,MAAM,GAAG,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAA;IAC1C,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,OAAO,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IACxC,CAAC;IAED,MAAM,MAAM,GAAG,GAAG,CAAC,gBAAgB,CAAA;IACnC,MAAM,SAAS,GAAG,IAAI,IAAI,SAAS,CAAA;IAEnC,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,YAAY,CAAC,CAAA;IAClD,IAAI,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAU,CAAC,CAAA;IAC1C,IAAI,YAAY,GAAW,gBAAgB,CAAC,GAAG,EAAE,WAAW,CAAC,CAAA;IAE7D,MAAM,KAAK,GAAoB;QAC7B,IAAI,EAAE,SAAS;QACf,SAAS,CAAC,KAAc;YACtB,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAU,CAAC,CAAA;YACpC,8DAA8D;YAC9D,gEAAgE;YAChE,sDAAsD;YACtD,IAAI,OAAO,KAAK,WAAW;gBAAE,OAAM;YAEnC,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAA;YAChC,IAAI,CAAC,MAAM;gBAAE,OAAM;YAEnB,MAAM,QAAQ,GAAG,YAAY,CAAA;YAC7B,YAAY,GAAG,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;YAC7C,WAAW,GAAG,OAAO,CAAA;YAErB,gEAAgE;YAChE,kEAAkE;YAClE,4DAA4D;YAC5D,kEAAkE;YAClE,qCAAqC;YACrC,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAA;YAC9B,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;gBAChC,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;YAChC,CAAC;YACD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;gBAC5B,IAAI,IAAI,CAAC,UAAU;oBAAE,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,CAAA;YACxD,CAAC;QACH,CAAC;KACF,CAAA;IACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAElB,OAAO,CAAC,MAAM,EAAE,GAAG,YAAY,CAAC,CAAA;AAClC,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAiD,EAAE,IAAY;IACvF,oEAAoE;IACpE,oEAAoE;IACpE,iEAAiE;IACjE,sEAAsE;IACtE,0DAA0D;IAC1D,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["import type { StructuralBlock } from '../structural.js'\nimport { getRenderContext } from '../render-context.js'\nimport { FULL_MASK } from '../update-loop.js'\n\n/**\n * Insert raw HTML into the DOM tree.\n *\n * **Security.** The caller is responsible for sanitizing `html`. Never\n * interpolate unsanitized user input — this is the XSS escape hatch and\n * the name carries the warning deliberately.\n *\n * **Opaque to the framework.** The parsed subtree is treated as a black\n * box by LLui — no bindings, events, `each`, `show`, or `child` inside\n * it will be discovered or driven by the framework. Use element helpers\n * for anything that needs to react to state or dispatch messages.\n *\n * **Static form.** `unsafeHtml('<b>hi</b>')` parses once at view time\n * and returns the parsed nodes. Zero reactive overhead.\n *\n * **Reactive form.** `unsafeHtml((s) => s.markdownHtml)` registers a\n * structural block that re-parses when the returned string differs\n * (strict `===`) from the previous value. Unchanged strings short-\n * circuit — existing DOM identity (focus, selection, listeners\n * attached outside LLui) is preserved.\n *\n * **Mask hint.** The second parameter mirrors `text()`: the compiler\n * passes a precise mask derived from which state fields the accessor\n * reads. Phase 2 / Phase 1 skip the reconcile when no interesting bit\n * is set. Defaults to FULL_MASK for hand-written components.\n */\nexport function unsafeHtml<S>(accessor: ((s: S) => string) | string, mask?: number): Node[] {\n const ctx = getRenderContext('unsafeHtml')\n if (typeof accessor === 'string') {\n return parseHtmlToNodes(ctx, accessor)\n }\n\n const blocks = ctx.structuralBlocks\n const blockMask = mask ?? FULL_MASK\n\n const anchor = ctx.dom.createComment('unsafeHtml')\n let currentHtml = accessor(ctx.state as S)\n let currentNodes: Node[] = parseHtmlToNodes(ctx, currentHtml)\n\n const block: StructuralBlock = {\n mask: blockMask,\n reconcile(state: unknown) {\n const newHtml = accessor(state as S)\n // Identity short-circuit — don't rebuild the subtree when the\n // author handed us the same string. Preserves focus, selection,\n // and any listeners outside LLui's view of the world.\n if (newHtml === currentHtml) return\n\n const parent = anchor.parentNode\n if (!parent) return\n\n const oldNodes = currentNodes\n currentNodes = parseHtmlToNodes(ctx, newHtml)\n currentHtml = newHtml\n\n // The anchor sits immediately before the current node range, so\n // `anchor.nextSibling` is the first old node (or whatever follows\n // when the old list is empty). Insert new nodes there, then\n // remove the old ones — matches `branch()`'s enter/leave order so\n // transitions can inspect both sets.\n const ref = anchor.nextSibling\n for (const node of currentNodes) {\n parent.insertBefore(node, ref)\n }\n for (const node of oldNodes) {\n if (node.parentNode) node.parentNode.removeChild(node)\n }\n },\n }\n blocks.push(block)\n\n return [anchor, ...currentNodes]\n}\n\nfunction parseHtmlToNodes(ctx: import('../render-context.js').RenderContext, html: string): Node[] {\n // Delegates to the env — jsdom/linkedom adapters may implement this\n // via DOMParser instead of the browser `<template>` trick, but both\n // return an inert DocumentFragment that doesn't execute scripts.\n // Snapshot — moving nodes to a parent drains fragment.childNodes, but\n // callers hold this array and we iterate it on reconcile.\n return Array.from(ctx.dom.parseHtmlFragment(html).childNodes)\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"virtual-each.d.ts","sourceRoot":"","sources":["../../src/primitives/virtual-each.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAY,IAAI,EAAE,MAAM,aAAa,CAAA;AAY/D,MAAM,WAAW,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,OAAO;IACnD,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAA;IACpB,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,CAAA;IACjC,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAA;IAClB,6CAA6C;IAC7C,eAAe,EAAE,MAAM,CAAA;IACvB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,CAAC,IAAI,EAAE;QACb,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;QACb,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,CAAA;QACrB,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,CAAA;QAC1C,KAAK,EAAE,MAAM,MAAM,CAAA;KACpB,KAAK,IAAI,EAAE,CAAA;CACb;
|
|
1
|
+
{"version":3,"file":"virtual-each.d.ts","sourceRoot":"","sources":["../../src/primitives/virtual-each.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAY,IAAI,EAAE,MAAM,aAAa,CAAA;AAY/D,MAAM,WAAW,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,OAAO;IACnD,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAA;IACpB,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,CAAA;IACjC,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAA;IAClB,6CAA6C;IAC7C,eAAe,EAAE,MAAM,CAAA;IACvB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,CAAC,IAAI,EAAE;QACb,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAA;QACb,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,CAAA;QACrB,GAAG,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,CAAA;QAC1C,KAAK,EAAE,MAAM,MAAM,CAAA;KACpB,KAAK,IAAI,EAAE,CAAA;CACb;AAkBD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,CAmMxF"}
|
|
@@ -7,6 +7,7 @@ const buildCtx = {
|
|
|
7
7
|
state: null,
|
|
8
8
|
allBindings: [],
|
|
9
9
|
structuralBlocks: [],
|
|
10
|
+
dom: null,
|
|
10
11
|
};
|
|
11
12
|
/**
|
|
12
13
|
* Virtualized list — renders only the rows visible in the scroll viewport.
|
|
@@ -38,7 +39,7 @@ export function virtualEach(opts) {
|
|
|
38
39
|
const send = ctx.send;
|
|
39
40
|
const overscan = opts.overscan ?? 3;
|
|
40
41
|
// Scroll container
|
|
41
|
-
const scroll =
|
|
42
|
+
const scroll = ctx.dom.createElement('div');
|
|
42
43
|
scroll.style.overflow = 'auto';
|
|
43
44
|
scroll.style.position = 'relative';
|
|
44
45
|
scroll.style.height = `${opts.containerHeight}px`;
|
|
@@ -46,7 +47,7 @@ export function virtualEach(opts) {
|
|
|
46
47
|
if (opts.class)
|
|
47
48
|
scroll.className = opts.class;
|
|
48
49
|
// Inner spacer sized to full content height
|
|
49
|
-
const spacer =
|
|
50
|
+
const spacer = ctx.dom.createElement('div');
|
|
50
51
|
spacer.style.position = 'relative';
|
|
51
52
|
spacer.style.width = '100%';
|
|
52
53
|
spacer.dataset.virtualSpacer = '';
|
|
@@ -65,7 +66,7 @@ export function virtualEach(opts) {
|
|
|
65
66
|
const buildEntry = (item, index, state) => {
|
|
66
67
|
const key = opts.key(item);
|
|
67
68
|
const scope = createLifetime(parentLifetime);
|
|
68
|
-
const wrapper =
|
|
69
|
+
const wrapper = ctx.dom.createElement('div');
|
|
69
70
|
wrapper.style.position = 'absolute';
|
|
70
71
|
wrapper.style.top = `${index * opts.itemHeight}px`;
|
|
71
72
|
wrapper.style.left = '0';
|
|
@@ -107,6 +108,7 @@ export function virtualEach(opts) {
|
|
|
107
108
|
buildCtx.state = state;
|
|
108
109
|
buildCtx.allBindings = ctx.allBindings;
|
|
109
110
|
buildCtx.structuralBlocks = ctx.structuralBlocks;
|
|
111
|
+
buildCtx.dom = ctx.dom;
|
|
110
112
|
buildCtx.instance = ctx.instance;
|
|
111
113
|
const prevFlat = getFlatBindings();
|
|
112
114
|
setFlatBindings(ctx.allBindings);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"virtual-each.js","sourceRoot":"","sources":["../../src/primitives/virtual-each.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ItemAccessor, Lifetime, Send } from '../types.js'\nimport {\n getRenderContext,\n setRenderContext,\n clearRenderContext,\n type RenderContext,\n} from '../render-context.js'\nimport { createLifetime, disposeLifetime, addDisposer } from '../lifetime.js'\nimport { getFlatBindings, setFlatBindings } from '../binding.js'\nimport { FULL_MASK } from '../update-loop.js'\nimport type { StructuralBlock } from '../structural.js'\n\nexport interface VirtualEachOptions<S, T, M = unknown> {\n items: (s: S) => T[]\n key: (item: T) => string | number\n /** Fixed pixel height per item. Required — dynamic heights are not supported yet. */\n itemHeight: number\n /** Scrollable container height in pixels. */\n containerHeight: number\n /** Extra rows to render above/below the viewport for smooth scrolling. Default: 3. */\n overscan?: number\n /** Optional class for the scroll container. */\n class?: string\n render: (opts: {\n send: Send<M>\n item: ItemAccessor<T>\n acc: <R>(selector: (t: T) => R) => () => R\n index: () => number\n }) => Node[]\n}\n\ninterface VirtualEntry<T> {\n key: string | number\n current: T\n index: number\n scope: Lifetime\n wrapper: HTMLElement\n}\n\nconst buildCtx: RenderContext = {\n rootLifetime: null as unknown as Lifetime,\n state: null,\n allBindings: [],\n structuralBlocks: [],\n}\n\n/**\n * Virtualized list — renders only the rows visible in the scroll viewport.\n * Use for lists with 1k+ items where a regular `each()` would be too slow.\n *\n * Current limitations:\n * - Fixed row height (`itemHeight`) — dynamic heights not supported\n * - No transitions / animations\n * - No cross-container reuse (items outside view are fully disposed)\n *\n * ```ts\n * view: ({ text }) => [\n * ...virtualEach({\n * items: (s) => s.rows,\n * key: (r) => r.id,\n * itemHeight: 40,\n * containerHeight: 600,\n * render: ({ item }) => [\n * div({ class: 'row' }, [text(item.label)]),\n * ],\n * }),\n * ]\n * ```\n */\nexport function virtualEach<S, T, M = unknown>(opts: VirtualEachOptions<S, T, M>): Node[] {\n const ctx = getRenderContext('virtualEach')\n const parentLifetime = ctx.rootLifetime\n const blocks = ctx.structuralBlocks\n const send = ctx.send as (msg: M) => void\n\n const overscan = opts.overscan ?? 3\n\n // Scroll container\n const scroll = document.createElement('div')\n scroll.style.overflow = 'auto'\n scroll.style.position = 'relative'\n scroll.style.height = `${opts.containerHeight}px`\n scroll.dataset.virtualContainer = ''\n if (opts.class) scroll.className = opts.class\n\n // Inner spacer sized to full content height\n const spacer = document.createElement('div')\n spacer.style.position = 'relative'\n spacer.style.width = '100%'\n spacer.dataset.virtualSpacer = ''\n scroll.appendChild(spacer)\n\n // Map of key → entry\n const entries = new Map<string | number, VirtualEntry<T>>()\n let lastItems: T[] = []\n let scrollTop = 0\n\n const computeRange = (length: number): [number, number] => {\n if (length === 0) return [0, 0]\n const start = Math.max(0, Math.floor(scrollTop / opts.itemHeight) - overscan)\n const end = Math.min(\n length,\n Math.ceil((scrollTop + opts.containerHeight) / opts.itemHeight) + overscan,\n )\n return [start, end]\n }\n\n const buildEntry = (item: T, index: number, state: S): VirtualEntry<T> => {\n const key = opts.key(item)\n const scope = createLifetime(parentLifetime)\n\n const wrapper = document.createElement('div')\n wrapper.style.position = 'absolute'\n wrapper.style.top = `${index * opts.itemHeight}px`\n wrapper.style.left = '0'\n wrapper.style.right = '0'\n wrapper.style.height = `${opts.itemHeight}px`\n wrapper.dataset.virtualItem = ''\n wrapper.dataset.virtualKey = String(key)\n\n const entry: VirtualEntry<T> = { key, current: item, index, scope, wrapper }\n\n // Item accessor: item(selector) and item.field\n const itemFn = <R>(selector: (t: T) => R): (() => R) => {\n const accessor = (): R => selector(entry.current)\n ;(accessor as unknown as { __perItem: true }).__perItem = true\n return accessor\n }\n\n let itemProxy: ItemAccessor<T> | null = null\n const getItemProxy = (): ItemAccessor<T> => {\n if (itemProxy) return itemProxy\n const fieldCache = new Map<string, () => unknown>()\n itemProxy = new Proxy(itemFn as object, {\n get(target, prop) {\n if (typeof prop === 'symbol' || prop === 'then' || prop === 'prototype') {\n return Reflect.get(target, prop)\n }\n const k = prop as string\n const cached = fieldCache.get(k)\n if (cached) return cached\n const accessor = (): unknown => (entry.current as Record<string, unknown>)[k]\n ;(accessor as unknown as { __perItem: true }).__perItem = true\n fieldCache.set(k, accessor)\n return accessor\n },\n }) as ItemAccessor<T>\n return itemProxy\n }\n\n const indexAccessor = (): number => entry.index\n\n buildCtx.rootLifetime = scope\n buildCtx.state = state\n buildCtx.allBindings = ctx.allBindings\n buildCtx.structuralBlocks = ctx.structuralBlocks\n buildCtx.instance = ctx.instance\n const prevFlat = getFlatBindings()\n setFlatBindings(ctx.allBindings)\n setRenderContext(buildCtx)\n\n const nodes = opts.render({\n send,\n item: getItemProxy(),\n acc: itemFn,\n index: indexAccessor,\n })\n\n clearRenderContext()\n setFlatBindings(prevFlat)\n setRenderContext(ctx)\n\n for (const node of nodes) wrapper.appendChild(node)\n return entry\n }\n\n const reconcile = (state: S): void => {\n const items = opts.items(state)\n lastItems = items\n\n // Update spacer total height\n spacer.style.height = `${items.length * opts.itemHeight}px`\n\n if (items.length === 0) {\n // Dispose all entries\n for (const entry of entries.values()) {\n entry.scope.disposalCause = 'each-remove'\n disposeLifetime(entry.scope)\n if (entry.wrapper.parentNode) entry.wrapper.parentNode.removeChild(entry.wrapper)\n }\n entries.clear()\n return\n }\n\n const [start, end] = computeRange(items.length)\n\n // Build a map of key → {item, index} for the visible range\n const visibleKeys = new Map<string | number, { item: T; index: number }>()\n for (let i = start; i < end; i++) {\n const item = items[i]!\n visibleKeys.set(opts.key(item), { item, index: i })\n }\n\n // Dispose entries no longer visible\n for (const [key, entry] of entries) {\n if (!visibleKeys.has(key)) {\n entry.scope.disposalCause = 'each-remove'\n disposeLifetime(entry.scope)\n if (entry.wrapper.parentNode) entry.wrapper.parentNode.removeChild(entry.wrapper)\n entries.delete(key)\n }\n }\n\n // Create new entries + update existing\n for (const [key, { item, index }] of visibleKeys) {\n const existing = entries.get(key)\n if (existing) {\n existing.current = item\n if (existing.index !== index) {\n existing.index = index\n existing.wrapper.style.top = `${index * opts.itemHeight}px`\n }\n } else {\n const entry = buildEntry(item, index, state)\n entries.set(key, entry)\n spacer.appendChild(entry.wrapper)\n }\n }\n }\n\n // Scroll handler — reconcile visible range without touching component state\n const onScroll = (): void => {\n scrollTop = scroll.scrollTop\n reconcile(ctx.state as S)\n }\n scroll.addEventListener('scroll', onScroll, { passive: true })\n\n // Register as a structural block BEFORE initial render so this block\n // precedes any nested blocks its rows register. See branch.ts for the\n // full rationale (Phase 1 iteration safety).\n const block: StructuralBlock = {\n mask: FULL_MASK,\n reconcile(state: unknown) {\n const newItems = opts.items(state as S)\n if (newItems === lastItems) return\n reconcile(state as S)\n },\n }\n blocks.push(block)\n\n // Initial render\n reconcile(ctx.state as S)\n\n // Cleanup on parent disposal\n addDisposer(parentLifetime, () => {\n scroll.removeEventListener('scroll', onScroll)\n for (const entry of entries.values()) {\n disposeLifetime(entry.scope)\n }\n entries.clear()\n })\n\n return [scroll]\n}\n"]}
|
|
1
|
+
{"version":3,"file":"virtual-each.js","sourceRoot":"","sources":["../../src/primitives/virtual-each.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ItemAccessor, Lifetime, Send } from '../types.js'\nimport {\n getRenderContext,\n setRenderContext,\n clearRenderContext,\n type RenderContext,\n} from '../render-context.js'\nimport { createLifetime, disposeLifetime, addDisposer } from '../lifetime.js'\nimport { getFlatBindings, setFlatBindings } from '../binding.js'\nimport { FULL_MASK } from '../update-loop.js'\nimport type { StructuralBlock } from '../structural.js'\n\nexport interface VirtualEachOptions<S, T, M = unknown> {\n items: (s: S) => T[]\n key: (item: T) => string | number\n /** Fixed pixel height per item. Required — dynamic heights are not supported yet. */\n itemHeight: number\n /** Scrollable container height in pixels. */\n containerHeight: number\n /** Extra rows to render above/below the viewport for smooth scrolling. Default: 3. */\n overscan?: number\n /** Optional class for the scroll container. */\n class?: string\n render: (opts: {\n send: Send<M>\n item: ItemAccessor<T>\n acc: <R>(selector: (t: T) => R) => () => R\n index: () => number\n }) => Node[]\n}\n\ninterface VirtualEntry<T> {\n key: string | number\n current: T\n index: number\n scope: Lifetime\n wrapper: HTMLElement\n}\n\nconst buildCtx: RenderContext = {\n rootLifetime: null as unknown as Lifetime,\n state: null,\n allBindings: [],\n structuralBlocks: [],\n dom: null as unknown as import('../dom-env.js').DomEnv,\n}\n\n/**\n * Virtualized list — renders only the rows visible in the scroll viewport.\n * Use for lists with 1k+ items where a regular `each()` would be too slow.\n *\n * Current limitations:\n * - Fixed row height (`itemHeight`) — dynamic heights not supported\n * - No transitions / animations\n * - No cross-container reuse (items outside view are fully disposed)\n *\n * ```ts\n * view: ({ text }) => [\n * ...virtualEach({\n * items: (s) => s.rows,\n * key: (r) => r.id,\n * itemHeight: 40,\n * containerHeight: 600,\n * render: ({ item }) => [\n * div({ class: 'row' }, [text(item.label)]),\n * ],\n * }),\n * ]\n * ```\n */\nexport function virtualEach<S, T, M = unknown>(opts: VirtualEachOptions<S, T, M>): Node[] {\n const ctx = getRenderContext('virtualEach')\n const parentLifetime = ctx.rootLifetime\n const blocks = ctx.structuralBlocks\n const send = ctx.send as (msg: M) => void\n\n const overscan = opts.overscan ?? 3\n\n // Scroll container\n const scroll = ctx.dom.createElement('div') as HTMLDivElement\n scroll.style.overflow = 'auto'\n scroll.style.position = 'relative'\n scroll.style.height = `${opts.containerHeight}px`\n scroll.dataset.virtualContainer = ''\n if (opts.class) scroll.className = opts.class\n\n // Inner spacer sized to full content height\n const spacer = ctx.dom.createElement('div') as HTMLDivElement\n spacer.style.position = 'relative'\n spacer.style.width = '100%'\n spacer.dataset.virtualSpacer = ''\n scroll.appendChild(spacer)\n\n // Map of key → entry\n const entries = new Map<string | number, VirtualEntry<T>>()\n let lastItems: T[] = []\n let scrollTop = 0\n\n const computeRange = (length: number): [number, number] => {\n if (length === 0) return [0, 0]\n const start = Math.max(0, Math.floor(scrollTop / opts.itemHeight) - overscan)\n const end = Math.min(\n length,\n Math.ceil((scrollTop + opts.containerHeight) / opts.itemHeight) + overscan,\n )\n return [start, end]\n }\n\n const buildEntry = (item: T, index: number, state: S): VirtualEntry<T> => {\n const key = opts.key(item)\n const scope = createLifetime(parentLifetime)\n\n const wrapper = ctx.dom.createElement('div') as HTMLDivElement\n wrapper.style.position = 'absolute'\n wrapper.style.top = `${index * opts.itemHeight}px`\n wrapper.style.left = '0'\n wrapper.style.right = '0'\n wrapper.style.height = `${opts.itemHeight}px`\n wrapper.dataset.virtualItem = ''\n wrapper.dataset.virtualKey = String(key)\n\n const entry: VirtualEntry<T> = { key, current: item, index, scope, wrapper }\n\n // Item accessor: item(selector) and item.field\n const itemFn = <R>(selector: (t: T) => R): (() => R) => {\n const accessor = (): R => selector(entry.current)\n ;(accessor as unknown as { __perItem: true }).__perItem = true\n return accessor\n }\n\n let itemProxy: ItemAccessor<T> | null = null\n const getItemProxy = (): ItemAccessor<T> => {\n if (itemProxy) return itemProxy\n const fieldCache = new Map<string, () => unknown>()\n itemProxy = new Proxy(itemFn as object, {\n get(target, prop) {\n if (typeof prop === 'symbol' || prop === 'then' || prop === 'prototype') {\n return Reflect.get(target, prop)\n }\n const k = prop as string\n const cached = fieldCache.get(k)\n if (cached) return cached\n const accessor = (): unknown => (entry.current as Record<string, unknown>)[k]\n ;(accessor as unknown as { __perItem: true }).__perItem = true\n fieldCache.set(k, accessor)\n return accessor\n },\n }) as ItemAccessor<T>\n return itemProxy\n }\n\n const indexAccessor = (): number => entry.index\n\n buildCtx.rootLifetime = scope\n buildCtx.state = state\n buildCtx.allBindings = ctx.allBindings\n buildCtx.structuralBlocks = ctx.structuralBlocks\n buildCtx.dom = ctx.dom\n buildCtx.instance = ctx.instance\n const prevFlat = getFlatBindings()\n setFlatBindings(ctx.allBindings)\n setRenderContext(buildCtx)\n\n const nodes = opts.render({\n send,\n item: getItemProxy(),\n acc: itemFn,\n index: indexAccessor,\n })\n\n clearRenderContext()\n setFlatBindings(prevFlat)\n setRenderContext(ctx)\n\n for (const node of nodes) wrapper.appendChild(node)\n return entry\n }\n\n const reconcile = (state: S): void => {\n const items = opts.items(state)\n lastItems = items\n\n // Update spacer total height\n spacer.style.height = `${items.length * opts.itemHeight}px`\n\n if (items.length === 0) {\n // Dispose all entries\n for (const entry of entries.values()) {\n entry.scope.disposalCause = 'each-remove'\n disposeLifetime(entry.scope)\n if (entry.wrapper.parentNode) entry.wrapper.parentNode.removeChild(entry.wrapper)\n }\n entries.clear()\n return\n }\n\n const [start, end] = computeRange(items.length)\n\n // Build a map of key → {item, index} for the visible range\n const visibleKeys = new Map<string | number, { item: T; index: number }>()\n for (let i = start; i < end; i++) {\n const item = items[i]!\n visibleKeys.set(opts.key(item), { item, index: i })\n }\n\n // Dispose entries no longer visible\n for (const [key, entry] of entries) {\n if (!visibleKeys.has(key)) {\n entry.scope.disposalCause = 'each-remove'\n disposeLifetime(entry.scope)\n if (entry.wrapper.parentNode) entry.wrapper.parentNode.removeChild(entry.wrapper)\n entries.delete(key)\n }\n }\n\n // Create new entries + update existing\n for (const [key, { item, index }] of visibleKeys) {\n const existing = entries.get(key)\n if (existing) {\n existing.current = item\n if (existing.index !== index) {\n existing.index = index\n existing.wrapper.style.top = `${index * opts.itemHeight}px`\n }\n } else {\n const entry = buildEntry(item, index, state)\n entries.set(key, entry)\n spacer.appendChild(entry.wrapper)\n }\n }\n }\n\n // Scroll handler — reconcile visible range without touching component state\n const onScroll = (): void => {\n scrollTop = scroll.scrollTop\n reconcile(ctx.state as S)\n }\n scroll.addEventListener('scroll', onScroll, { passive: true })\n\n // Register as a structural block BEFORE initial render so this block\n // precedes any nested blocks its rows register. See branch.ts for the\n // full rationale (Phase 1 iteration safety).\n const block: StructuralBlock = {\n mask: FULL_MASK,\n reconcile(state: unknown) {\n const newItems = opts.items(state as S)\n if (newItems === lastItems) return\n reconcile(state as S)\n },\n }\n blocks.push(block)\n\n // Initial render\n reconcile(ctx.state as S)\n\n // Cleanup on parent disposal\n addDisposer(parentLifetime, () => {\n scroll.removeEventListener('scroll', onScroll)\n for (const entry of entries.values()) {\n disposeLifetime(entry.scope)\n }\n entries.clear()\n })\n\n return [scroll]\n}\n"]}
|
package/dist/render-context.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Lifetime, Binding } from './types.js';
|
|
2
2
|
import type { StructuralBlock } from './structural.js';
|
|
3
|
+
import type { DomEnv } from './dom-env.js';
|
|
3
4
|
export interface RenderContext {
|
|
4
5
|
rootLifetime: Lifetime;
|
|
5
6
|
state: unknown;
|
|
@@ -7,6 +8,14 @@ export interface RenderContext {
|
|
|
7
8
|
structuralBlocks: StructuralBlock[];
|
|
8
9
|
container?: Element;
|
|
9
10
|
send?: (msg: unknown) => void;
|
|
11
|
+
/**
|
|
12
|
+
* The DOM implementation backing this render pass. Seeded by
|
|
13
|
+
* `mountApp` / `hydrateApp` / `renderToString` and threaded through
|
|
14
|
+
* every nested primitive via context spread. Primitives construct
|
|
15
|
+
* DOM through `ctx.dom.createElement(...)` etc. instead of reaching
|
|
16
|
+
* for `globalThis.document`.
|
|
17
|
+
*/
|
|
18
|
+
dom: DomEnv;
|
|
10
19
|
}
|
|
11
20
|
export declare function setRenderContext(ctx: RenderContext): void;
|
|
12
21
|
export declare function clearRenderContext(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,QAAQ,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,OAAO,EAAE,CAAA;IACtB,gBAAgB,EAAE,eAAe,EAAE,CAAA;IACnC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAA;CAMZ;AAID,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAEzD;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED,wBAAgB,gBAAgB,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,aAAa,CAgBtE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AA2BA,IAAI,cAAc,GAAyB,IAAI,CAAA;AAE/C,MAAM,UAAU,gBAAgB,CAAC,GAAkB;IACjD,cAAc,GAAG,GAAG,CAAA;AACtB,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,cAAc,GAAG,IAAI,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,aAAsB;IACrD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,IAAI,CAAC,CAAC,CAAC,YAAY,CAAA;QAChE,MAAM,IAAI,KAAK,CACb,UAAU,IAAI,4DAA4D;YACxE,0DAA0D;YAC1D,sEAAsE;YACtE,yEAAyE;YACzE,0EAA0E;YAC1E,0EAA0E;YAC1E,gCAAgC;YAChC,yEAAyE;YACzE,sEAAsE,CACzE,CAAA;IACH,CAAC;IACD,OAAO,cAAc,CAAA;AACvB,CAAC","sourcesContent":["import type { Lifetime, Binding } from './types.js'\nimport type { StructuralBlock } from './structural.js'\nimport type { ComponentInstance } from './update-loop.js'\nimport type { DomEnv } from './dom-env.js'\n\nexport interface RenderContext {\n rootLifetime: Lifetime\n state: unknown\n allBindings: Binding[]\n structuralBlocks: StructuralBlock[]\n container?: Element\n send?: (msg: unknown) => void\n /**\n * The DOM implementation backing this render pass. Seeded by\n * `mountApp` / `hydrateApp` / `renderToString` and threaded through\n * every nested primitive via context spread. Primitives construct\n * DOM through `ctx.dom.createElement(...)` etc. instead of reaching\n * for `globalThis.document`.\n */\n dom: DomEnv\n /** @internal dev-only — the owning ComponentInstance. Set by mount /\n * hydrate / child to let primitives (currently `each`) emit tracker\n * data to `inst._eachDiffLog`. Nested contexts pass through via\n * spread (e.g. `{ ...ctx, rootLifetime }`). Undefined outside dev. */\n instance?: ComponentInstance\n}\n\nlet currentContext: RenderContext | null = null\n\nexport function setRenderContext(ctx: RenderContext): void {\n currentContext = ctx\n}\n\nexport function clearRenderContext(): void {\n currentContext = null\n}\n\nexport function getRenderContext(primitiveName?: string): RenderContext {\n if (!currentContext) {\n const name = primitiveName ? `${primitiveName}()` : 'primitives'\n throw new Error(\n `[LLui] ${name} can only be called inside a component's view() function. ` +\n `It was called outside a render context. Common causes:\\n` +\n ` 1. Calling a primitive at module scope instead of inside view().\\n` +\n ` 2. Calling an overlay helper (dialog.overlay, popover.overlay, …) at ` +\n `module scope — these internally use show()/branch() and must be invoked ` +\n `from inside the component's view callback so their result can be spread ` +\n `into the returned node tree.\\n` +\n ` 3. Calling a primitive from a setTimeout / Promise / event handler — ` +\n `the render context only persists during the synchronous view() call.`,\n )\n }\n return currentContext\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@llui/dom/ssr/jsdom` — jsdom-backed `DomEnv` factory.
|
|
3
|
+
*
|
|
4
|
+
* Only imports jsdom. Consumers who need a different DOM (linkedom,
|
|
5
|
+
* happy-dom, custom) use the corresponding sub-entry or build their
|
|
6
|
+
* own `DomEnv` by hand — neither route pulls jsdom into the bundle.
|
|
7
|
+
*/
|
|
8
|
+
import type { DomEnv } from '../dom-env.js';
|
|
9
|
+
/**
|
|
10
|
+
* Construct a `DomEnv` backed by a fresh jsdom instance. Each call
|
|
11
|
+
* returns a new env — no process-level state, safe under concurrency.
|
|
12
|
+
*
|
|
13
|
+
* Requires `jsdom` as an installed dependency. If you don't want the
|
|
14
|
+
* jsdom bundle (e.g. on Cloudflare Workers), use `linkedomEnv()` from
|
|
15
|
+
* `@llui/dom/ssr/linkedom` instead.
|
|
16
|
+
*/
|
|
17
|
+
export declare function jsdomEnv(): Promise<DomEnv>;
|
|
18
|
+
//# sourceMappingURL=jsdom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsdom.d.ts","sourceRoot":"","sources":["../../src/ssr/jsdom.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAwB3C;;;;;;;GAOG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CA6BhD"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct a `DomEnv` backed by a fresh jsdom instance. Each call
|
|
3
|
+
* returns a new env — no process-level state, safe under concurrency.
|
|
4
|
+
*
|
|
5
|
+
* Requires `jsdom` as an installed dependency. If you don't want the
|
|
6
|
+
* jsdom bundle (e.g. on Cloudflare Workers), use `linkedomEnv()` from
|
|
7
|
+
* `@llui/dom/ssr/linkedom` instead.
|
|
8
|
+
*/
|
|
9
|
+
export async function jsdomEnv() {
|
|
10
|
+
// @ts-expect-error — jsdom is an optional peer dependency, not typed
|
|
11
|
+
const jsdomMod = await import('jsdom');
|
|
12
|
+
const { JSDOM } = jsdomMod;
|
|
13
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
|
14
|
+
const w = dom.window;
|
|
15
|
+
return {
|
|
16
|
+
createElement: (tag) => w.document.createElement(tag),
|
|
17
|
+
createElementNS: (ns, tag) => w.document.createElementNS(ns, tag),
|
|
18
|
+
createTextNode: (text) => w.document.createTextNode(text),
|
|
19
|
+
createComment: (text) => w.document.createComment(text),
|
|
20
|
+
createDocumentFragment: () => w.document.createDocumentFragment(),
|
|
21
|
+
createRange: () => w.document.createRange(),
|
|
22
|
+
Element: w.Element,
|
|
23
|
+
Node: w.Node,
|
|
24
|
+
Text: w.Text,
|
|
25
|
+
Comment: w.Comment,
|
|
26
|
+
DocumentFragment: w.DocumentFragment,
|
|
27
|
+
HTMLElement: w.HTMLElement,
|
|
28
|
+
HTMLTemplateElement: w.HTMLTemplateElement,
|
|
29
|
+
ShadowRoot: w.ShadowRoot,
|
|
30
|
+
MouseEvent: w.MouseEvent,
|
|
31
|
+
parseHtmlFragment: (html) => {
|
|
32
|
+
const template = w.document.createElement('template');
|
|
33
|
+
template.innerHTML = html;
|
|
34
|
+
return template.content;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=jsdom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsdom.js","sourceRoot":"","sources":["../../src/ssr/jsdom.ts"],"names":[],"mappings":"AA+BA;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,qEAAqE;IACrE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,CAAA;IACtC,MAAM,EAAE,KAAK,EAAE,GAAG,QAAoE,CAAA;IACtF,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAA;IAClE,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAA;IAEpB,OAAO;QACL,aAAa,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC;QACrD,eAAe,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,EAAE,GAAG,CAAC;QACjE,cAAc,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC;QACzD,aAAa,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC;QACvD,sBAAsB,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,sBAAsB,EAAE;QACjE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE;QAC3C,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,gBAAgB,EAAE,CAAC,CAAC,gBAAgB;QACpC,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,mBAAmB,EAAE,CAAC,CAAC,mBAAmB;QAC1C,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,iBAAiB,EAAE,CAAC,IAAI,EAAE,EAAE;YAC1B,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAwB,CAAA;YAC5E,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAA;YACzB,OAAO,QAAQ,CAAC,OAAO,CAAA;QACzB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * `@llui/dom/ssr/jsdom` — jsdom-backed `DomEnv` factory.\n *\n * Only imports jsdom. Consumers who need a different DOM (linkedom,\n * happy-dom, custom) use the corresponding sub-entry or build their\n * own `DomEnv` by hand — neither route pulls jsdom into the bundle.\n */\nimport type { DomEnv } from '../dom-env.js'\n\n// jsdom is an optional peer dependency. The typing below is just\n// enough to construct the env; callers don't see the jsdom surface.\ninterface JsdomWindow {\n document: {\n createElement: (tag: string) => Element\n createElementNS: (ns: string, tag: string) => Element\n createTextNode: (text: string) => Text\n createComment: (text: string) => Comment\n createDocumentFragment: () => DocumentFragment\n createRange: () => Range\n }\n Element: typeof Element\n Node: typeof Node\n Text: typeof Text\n Comment: typeof Comment\n DocumentFragment: typeof DocumentFragment\n HTMLElement: typeof HTMLElement\n HTMLTemplateElement: typeof HTMLTemplateElement\n ShadowRoot: typeof ShadowRoot\n MouseEvent: typeof MouseEvent\n}\n\n/**\n * Construct a `DomEnv` backed by a fresh jsdom instance. Each call\n * returns a new env — no process-level state, safe under concurrency.\n *\n * Requires `jsdom` as an installed dependency. If you don't want the\n * jsdom bundle (e.g. on Cloudflare Workers), use `linkedomEnv()` from\n * `@llui/dom/ssr/linkedom` instead.\n */\nexport async function jsdomEnv(): Promise<DomEnv> {\n // @ts-expect-error — jsdom is an optional peer dependency, not typed\n const jsdomMod = await import('jsdom')\n const { JSDOM } = jsdomMod as { JSDOM: new (html: string) => { window: JsdomWindow } }\n const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>')\n const w = dom.window\n\n return {\n createElement: (tag) => w.document.createElement(tag),\n createElementNS: (ns, tag) => w.document.createElementNS(ns, tag),\n createTextNode: (text) => w.document.createTextNode(text),\n createComment: (text) => w.document.createComment(text),\n createDocumentFragment: () => w.document.createDocumentFragment(),\n createRange: () => w.document.createRange(),\n Element: w.Element,\n Node: w.Node,\n Text: w.Text,\n Comment: w.Comment,\n DocumentFragment: w.DocumentFragment,\n HTMLElement: w.HTMLElement,\n HTMLTemplateElement: w.HTMLTemplateElement,\n ShadowRoot: w.ShadowRoot,\n MouseEvent: w.MouseEvent,\n parseHtmlFragment: (html) => {\n const template = w.document.createElement('template') as HTMLTemplateElement\n template.innerHTML = html\n return template.content\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@llui/dom/ssr/legacy` — back-compat shim for `initSsrDom()`.
|
|
3
|
+
*
|
|
4
|
+
* Imports jsdom and mutates globalThis with its window classes, the way
|
|
5
|
+
* the 0.0.23-and-earlier API worked. Kept for one release cycle so
|
|
6
|
+
* existing call sites don't break on upgrade.
|
|
7
|
+
*
|
|
8
|
+
* Anyone importing from here pays the jsdom bundle cost. Migrate to
|
|
9
|
+
* `jsdomEnv()` from `@llui/dom/ssr/jsdom` (or `linkedomEnv()` from
|
|
10
|
+
* `@llui/dom/ssr/linkedom`) and pass the result to `renderToString` /
|
|
11
|
+
* `renderNodes` — it's the only path that stays concurrent-safe, works
|
|
12
|
+
* on strict-isolate runtimes (Cloudflare Workers), and keeps jsdom out
|
|
13
|
+
* of bundles that don't want it.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Use `jsdomEnv()` from `@llui/dom/ssr/jsdom` (or
|
|
17
|
+
* `linkedomEnv()` from `@llui/dom/ssr/linkedom`) and pass the result
|
|
18
|
+
* to `renderToString` / `renderNodes` explicitly. Global mutation
|
|
19
|
+
* forbids concurrent SSR with different DOM implementations and
|
|
20
|
+
* doesn't work on strict isolate runtimes like Cloudflare Workers.
|
|
21
|
+
*
|
|
22
|
+
* This shim will be removed in a future breaking release.
|
|
23
|
+
*/
|
|
24
|
+
export declare function initSsrDom(): Promise<void>;
|
|
25
|
+
//# sourceMappingURL=legacy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacy.d.ts","sourceRoot":"","sources":["../../src/ssr/legacy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;;;;;;GAQG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAqChD"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@llui/dom/ssr/legacy` — back-compat shim for `initSsrDom()`.
|
|
3
|
+
*
|
|
4
|
+
* Imports jsdom and mutates globalThis with its window classes, the way
|
|
5
|
+
* the 0.0.23-and-earlier API worked. Kept for one release cycle so
|
|
6
|
+
* existing call sites don't break on upgrade.
|
|
7
|
+
*
|
|
8
|
+
* Anyone importing from here pays the jsdom bundle cost. Migrate to
|
|
9
|
+
* `jsdomEnv()` from `@llui/dom/ssr/jsdom` (or `linkedomEnv()` from
|
|
10
|
+
* `@llui/dom/ssr/linkedom`) and pass the result to `renderToString` /
|
|
11
|
+
* `renderNodes` — it's the only path that stays concurrent-safe, works
|
|
12
|
+
* on strict-isolate runtimes (Cloudflare Workers), and keeps jsdom out
|
|
13
|
+
* of bundles that don't want it.
|
|
14
|
+
*/
|
|
15
|
+
let _initSsrDomWarned = false;
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated Use `jsdomEnv()` from `@llui/dom/ssr/jsdom` (or
|
|
18
|
+
* `linkedomEnv()` from `@llui/dom/ssr/linkedom`) and pass the result
|
|
19
|
+
* to `renderToString` / `renderNodes` explicitly. Global mutation
|
|
20
|
+
* forbids concurrent SSR with different DOM implementations and
|
|
21
|
+
* doesn't work on strict isolate runtimes like Cloudflare Workers.
|
|
22
|
+
*
|
|
23
|
+
* This shim will be removed in a future breaking release.
|
|
24
|
+
*/
|
|
25
|
+
export async function initSsrDom() {
|
|
26
|
+
if (typeof document !== 'undefined')
|
|
27
|
+
return;
|
|
28
|
+
if (!_initSsrDomWarned) {
|
|
29
|
+
_initSsrDomWarned = true;
|
|
30
|
+
console.warn('[LLui] initSsrDom() is deprecated and will be removed in a future release.\n' +
|
|
31
|
+
'Migrate to:\n' +
|
|
32
|
+
" import { jsdomEnv } from '@llui/dom/ssr/jsdom'\n" +
|
|
33
|
+
" import { renderToString } from '@llui/dom/ssr'\n" +
|
|
34
|
+
' const env = await jsdomEnv()\n' +
|
|
35
|
+
' const html = renderToString(MyApp, state, env)\n' +
|
|
36
|
+
'The global-mutation approach is incompatible with concurrent SSR\n' +
|
|
37
|
+
'and strict-isolate runtimes (Cloudflare Workers).');
|
|
38
|
+
}
|
|
39
|
+
// @ts-expect-error — jsdom is an optional peer dependency, not typed
|
|
40
|
+
const jsdomMod = await import('jsdom');
|
|
41
|
+
const jsdom = jsdomMod;
|
|
42
|
+
const dom = new jsdom.JSDOM('<!DOCTYPE html><html><body></body></html>');
|
|
43
|
+
const g = globalThis;
|
|
44
|
+
const win = dom.window;
|
|
45
|
+
for (const key of [
|
|
46
|
+
'document',
|
|
47
|
+
'HTMLElement',
|
|
48
|
+
'Element',
|
|
49
|
+
'Node',
|
|
50
|
+
'Text',
|
|
51
|
+
'Comment',
|
|
52
|
+
'MouseEvent',
|
|
53
|
+
'ShadowRoot',
|
|
54
|
+
'DocumentFragment',
|
|
55
|
+
'HTMLTemplateElement',
|
|
56
|
+
]) {
|
|
57
|
+
if (win[key] !== undefined)
|
|
58
|
+
g[key] = win[key];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=legacy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacy.js","sourceRoot":"","sources":["../../src/ssr/legacy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,IAAI,iBAAiB,GAAG,KAAK,CAAA;AAE7B;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAM;IAE3C,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACvB,iBAAiB,GAAG,IAAI,CAAA;QACxB,OAAO,CAAC,IAAI,CACV,8EAA8E;YAC5E,eAAe;YACf,oDAAoD;YACpD,oDAAoD;YACpD,kCAAkC;YAClC,oDAAoD;YACpD,oEAAoE;YACpE,mDAAmD,CACtD,CAAA;IACH,CAAC;IAED,qEAAqE;IACrE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG,QAAgF,CAAA;IAC9F,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAA;IACxE,MAAM,CAAC,GAAG,UAAqC,CAAA;IAC/C,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAA;IACtB,KAAK,MAAM,GAAG,IAAI;QAChB,UAAU;QACV,aAAa;QACb,SAAS;QACT,MAAM;QACN,MAAM;QACN,SAAS;QACT,YAAY;QACZ,YAAY;QACZ,kBAAkB;QAClB,qBAAqB;KACtB,EAAE,CAAC;QACF,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS;YAAE,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IAC/C,CAAC;AACH,CAAC","sourcesContent":["/**\n * `@llui/dom/ssr/legacy` — back-compat shim for `initSsrDom()`.\n *\n * Imports jsdom and mutates globalThis with its window classes, the way\n * the 0.0.23-and-earlier API worked. Kept for one release cycle so\n * existing call sites don't break on upgrade.\n *\n * Anyone importing from here pays the jsdom bundle cost. Migrate to\n * `jsdomEnv()` from `@llui/dom/ssr/jsdom` (or `linkedomEnv()` from\n * `@llui/dom/ssr/linkedom`) and pass the result to `renderToString` /\n * `renderNodes` — it's the only path that stays concurrent-safe, works\n * on strict-isolate runtimes (Cloudflare Workers), and keeps jsdom out\n * of bundles that don't want it.\n */\n\nlet _initSsrDomWarned = false\n\n/**\n * @deprecated Use `jsdomEnv()` from `@llui/dom/ssr/jsdom` (or\n * `linkedomEnv()` from `@llui/dom/ssr/linkedom`) and pass the result\n * to `renderToString` / `renderNodes` explicitly. Global mutation\n * forbids concurrent SSR with different DOM implementations and\n * doesn't work on strict isolate runtimes like Cloudflare Workers.\n *\n * This shim will be removed in a future breaking release.\n */\nexport async function initSsrDom(): Promise<void> {\n if (typeof document !== 'undefined') return\n\n if (!_initSsrDomWarned) {\n _initSsrDomWarned = true\n console.warn(\n '[LLui] initSsrDom() is deprecated and will be removed in a future release.\\n' +\n 'Migrate to:\\n' +\n \" import { jsdomEnv } from '@llui/dom/ssr/jsdom'\\n\" +\n \" import { renderToString } from '@llui/dom/ssr'\\n\" +\n ' const env = await jsdomEnv()\\n' +\n ' const html = renderToString(MyApp, state, env)\\n' +\n 'The global-mutation approach is incompatible with concurrent SSR\\n' +\n 'and strict-isolate runtimes (Cloudflare Workers).',\n )\n }\n\n // @ts-expect-error — jsdom is an optional peer dependency, not typed\n const jsdomMod = await import('jsdom')\n const jsdom = jsdomMod as { JSDOM: new (html: string) => { window: Record<string, unknown> } }\n const dom = new jsdom.JSDOM('<!DOCTYPE html><html><body></body></html>')\n const g = globalThis as Record<string, unknown>\n const win = dom.window\n for (const key of [\n 'document',\n 'HTMLElement',\n 'Element',\n 'Node',\n 'Text',\n 'Comment',\n 'MouseEvent',\n 'ShadowRoot',\n 'DocumentFragment',\n 'HTMLTemplateElement',\n ]) {\n if (win[key] !== undefined) g[key] = win[key]\n }\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@llui/dom/ssr/linkedom` — linkedom-backed `DomEnv` factory.
|
|
3
|
+
*
|
|
4
|
+
* Linkedom is a lightweight JSDOM alternative — smaller bundle, works
|
|
5
|
+
* on Cloudflare Workers and other strict-isolate runtimes where jsdom's
|
|
6
|
+
* transitive `whatwg-url` / `tr46` / `punycode` chain fails to resolve.
|
|
7
|
+
*/
|
|
8
|
+
import type { DomEnv } from '../dom-env.js';
|
|
9
|
+
/**
|
|
10
|
+
* Construct a `DomEnv` backed by a fresh linkedom instance. Each call
|
|
11
|
+
* returns a new env — no process-level state, safe under concurrency
|
|
12
|
+
* and compatible with Cloudflare Workers.
|
|
13
|
+
*
|
|
14
|
+
* Requires `linkedom` as an installed dependency.
|
|
15
|
+
*/
|
|
16
|
+
export declare function linkedomEnv(): Promise<DomEnv>;
|
|
17
|
+
//# sourceMappingURL=linkedom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkedom.d.ts","sourceRoot":"","sources":["../../src/ssr/linkedom.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAyB3C;;;;;;GAMG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CA8BnD"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct a `DomEnv` backed by a fresh linkedom instance. Each call
|
|
3
|
+
* returns a new env — no process-level state, safe under concurrency
|
|
4
|
+
* and compatible with Cloudflare Workers.
|
|
5
|
+
*
|
|
6
|
+
* Requires `linkedom` as an installed dependency.
|
|
7
|
+
*/
|
|
8
|
+
export async function linkedomEnv() {
|
|
9
|
+
// @ts-expect-error — linkedom is an optional peer dependency, not typed
|
|
10
|
+
const linkedomMod = await import('linkedom');
|
|
11
|
+
const { parseHTML } = linkedomMod;
|
|
12
|
+
const w = parseHTML('<!DOCTYPE html><html><body></body></html>');
|
|
13
|
+
return {
|
|
14
|
+
createElement: (tag) => w.document.createElement(tag),
|
|
15
|
+
createElementNS: (ns, tag) => w.document.createElementNS(ns, tag),
|
|
16
|
+
createTextNode: (text) => w.document.createTextNode(text),
|
|
17
|
+
createComment: (text) => w.document.createComment(text),
|
|
18
|
+
createDocumentFragment: () => w.document.createDocumentFragment(),
|
|
19
|
+
createRange: () => w.document.createRange(),
|
|
20
|
+
Element: w.Element,
|
|
21
|
+
Node: w.Node,
|
|
22
|
+
Text: w.Text,
|
|
23
|
+
Comment: w.Comment,
|
|
24
|
+
DocumentFragment: w.DocumentFragment,
|
|
25
|
+
HTMLElement: w.HTMLElement,
|
|
26
|
+
HTMLTemplateElement: w.HTMLTemplateElement,
|
|
27
|
+
ShadowRoot: w.ShadowRoot,
|
|
28
|
+
MouseEvent: w.MouseEvent,
|
|
29
|
+
parseHtmlFragment: (html) => {
|
|
30
|
+
const template = w.document.createElement('template');
|
|
31
|
+
template.innerHTML = html;
|
|
32
|
+
return template.content;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=linkedom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkedom.js","sourceRoot":"","sources":["../../src/ssr/linkedom.ts"],"names":[],"mappings":"AAgCA;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,wEAAwE;IACxE,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAA;IAC5C,MAAM,EAAE,SAAS,EAAE,GAAG,WAErB,CAAA;IACD,MAAM,CAAC,GAAG,SAAS,CAAC,2CAA2C,CAAC,CAAA;IAEhE,OAAO;QACL,aAAa,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC;QACrD,eAAe,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,EAAE,GAAG,CAAC;QACjE,cAAc,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC;QACzD,aAAa,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC;QACvD,sBAAsB,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,sBAAsB,EAAE;QACjE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE;QAC3C,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,gBAAgB,EAAE,CAAC,CAAC,gBAAgB;QACpC,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,mBAAmB,EAAE,CAAC,CAAC,mBAAmB;QAC1C,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,iBAAiB,EAAE,CAAC,IAAI,EAAE,EAAE;YAC1B,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAwB,CAAA;YAC5E,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAA;YACzB,OAAO,QAAQ,CAAC,OAAO,CAAA;QACzB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * `@llui/dom/ssr/linkedom` — linkedom-backed `DomEnv` factory.\n *\n * Linkedom is a lightweight JSDOM alternative — smaller bundle, works\n * on Cloudflare Workers and other strict-isolate runtimes where jsdom's\n * transitive `whatwg-url` / `tr46` / `punycode` chain fails to resolve.\n */\nimport type { DomEnv } from '../dom-env.js'\n\n// Linkedom exposes a `parseHTML` function that returns a window-like\n// object with DOM constructors and a `document`. The exact shape is\n// narrower than JSDOM's but matches the DomEnv surface we need.\ninterface LinkedomWindow {\n document: {\n createElement: (tag: string) => Element\n createElementNS: (ns: string, tag: string) => Element\n createTextNode: (text: string) => Text\n createComment: (text: string) => Comment\n createDocumentFragment: () => DocumentFragment\n createRange: () => Range\n }\n Element: typeof Element\n Node: typeof Node\n Text: typeof Text\n Comment: typeof Comment\n DocumentFragment: typeof DocumentFragment\n HTMLElement: typeof HTMLElement\n HTMLTemplateElement: typeof HTMLTemplateElement\n ShadowRoot: typeof ShadowRoot\n MouseEvent: typeof MouseEvent\n}\n\n/**\n * Construct a `DomEnv` backed by a fresh linkedom instance. Each call\n * returns a new env — no process-level state, safe under concurrency\n * and compatible with Cloudflare Workers.\n *\n * Requires `linkedom` as an installed dependency.\n */\nexport async function linkedomEnv(): Promise<DomEnv> {\n // @ts-expect-error — linkedom is an optional peer dependency, not typed\n const linkedomMod = await import('linkedom')\n const { parseHTML } = linkedomMod as {\n parseHTML: (html: string) => LinkedomWindow\n }\n const w = parseHTML('<!DOCTYPE html><html><body></body></html>')\n\n return {\n createElement: (tag) => w.document.createElement(tag),\n createElementNS: (ns, tag) => w.document.createElementNS(ns, tag),\n createTextNode: (text) => w.document.createTextNode(text),\n createComment: (text) => w.document.createComment(text),\n createDocumentFragment: () => w.document.createDocumentFragment(),\n createRange: () => w.document.createRange(),\n Element: w.Element,\n Node: w.Node,\n Text: w.Text,\n Comment: w.Comment,\n DocumentFragment: w.DocumentFragment,\n HTMLElement: w.HTMLElement,\n HTMLTemplateElement: w.HTMLTemplateElement,\n ShadowRoot: w.ShadowRoot,\n MouseEvent: w.MouseEvent,\n parseHtmlFragment: (html) => {\n const template = w.document.createElement('template') as HTMLTemplateElement\n template.innerHTML = html\n return template.content\n },\n }\n}\n"]}
|