kerfjs 0.16.0 → 1.0.2
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/CHANGELOG.md +42 -0
- package/README.md +29 -7
- package/ai/cursorrules +15 -1
- package/ai/manifest.json +5 -5
- package/ai/skill.md +16 -1
- package/dist/array-signal.js +1 -1
- package/dist/{chunk-N4KF3GD2.js → chunk-4E26PO2C.js} +7 -4
- package/dist/chunk-4E26PO2C.js.map +1 -0
- package/dist/{chunk-4TJEO4AO.js → chunk-7SKIIA5P.js} +3 -3
- package/dist/{chunk-4TJEO4AO.js.map → chunk-7SKIIA5P.js.map} +1 -1
- package/dist/chunk-QNYOMGI4.js +543 -0
- package/dist/chunk-QNYOMGI4.js.map +1 -0
- package/dist/index.d.ts +22 -22
- package/dist/index.js +125 -37
- package/dist/index.js.map +1 -1
- package/dist/jsx-runtime.d.ts +89 -4
- package/dist/jsx-runtime.js +2 -1
- package/dist/testing.js +2 -2
- package/llms.txt +1 -1
- package/package.json +3 -2
- package/dist/chunk-4VT4YZOO.js +0 -297
- package/dist/chunk-4VT4YZOO.js.map +0 -1
- package/dist/chunk-N4KF3GD2.js.map +0 -1
package/dist/jsx-runtime.d.ts
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
+
import { Signal, ReadonlySignal } from '@preact/signals-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fine-grained signal bindings (KF-294 spike).
|
|
5
|
+
*
|
|
6
|
+
* When a `Signal` is interpolated straight into a JSX attribute
|
|
7
|
+
* (`class={sig}`) or a text child (`{sig}`) INSIDE a `mount()` render, the
|
|
8
|
+
* JSX runtime stops stringifying it. Instead it emits a marker into the HTML
|
|
9
|
+
* string and records a binding here; after the string is parsed to DOM, a
|
|
10
|
+
* wiring pass attaches one `effect` per binding that writes straight to the
|
|
11
|
+
* live node. A later change to that signal then updates the node WITHOUT
|
|
12
|
+
* re-running the render function or walking the list reconciler.
|
|
13
|
+
*
|
|
14
|
+
* This reuses the "marker in string, wire up after parse" mechanism the
|
|
15
|
+
* keyed-list reconciler already uses for `<!--kf-list:{id}-->` markers.
|
|
16
|
+
*
|
|
17
|
+
* TWO SCOPES of binding, with disjoint marker namespaces so their wiring
|
|
18
|
+
* passes never collide:
|
|
19
|
+
*
|
|
20
|
+
* - GLOBAL holes — signals in the static surrounds (outside any `each()`
|
|
21
|
+
* row). Markers: `data-kfb` attribute / `<!--kfb:{id}-->` comment. Ids come
|
|
22
|
+
* from the mount render context's counter; wired by `wireBindings()` over
|
|
23
|
+
* the whole mount root; disposed/re-wired by `mount()` each render.
|
|
24
|
+
*
|
|
25
|
+
* - ROW holes — signals inside an `each()` row. Markers: `data-kfbrow`
|
|
26
|
+
* attribute / `<!--kfbr:{id}-->` comment. Ids are row-LOCAL (reset per row)
|
|
27
|
+
* so they stay stable and collision-free as rows are inserted/removed/moved.
|
|
28
|
+
* Captured per row by `captureRowBindings()`, carried on the list segment
|
|
29
|
+
* item, and wired/disposed by the list reconciler at each row node's
|
|
30
|
+
* create/remove — so a binding's lifetime tracks its row node's lifetime,
|
|
31
|
+
* and row reorders (which reuse the same node) are free.
|
|
32
|
+
*
|
|
33
|
+
* Outside a `mount()` render (SSR / `SafeHtml.toString()`) neither scope is
|
|
34
|
+
* active: the runtime snapshots `signal.value` and emits no markers, so server
|
|
35
|
+
* output is correct and legacy `.toString()` callers are unaffected.
|
|
36
|
+
*
|
|
37
|
+
* Module-level mutable state note: `context` / `rowSink` here are a third
|
|
38
|
+
* sanctioned module-level mutable location (alongside `store.ts:REGISTRY` and
|
|
39
|
+
* `each.ts:context`). They hold the current render's binding sinks and are set
|
|
40
|
+
* / cleared by `mount()` and `each()` around the render calls.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
interface AttrBinding {
|
|
44
|
+
kind: 'attr';
|
|
45
|
+
id: string;
|
|
46
|
+
attr: string;
|
|
47
|
+
signal: Signal<unknown>;
|
|
48
|
+
}
|
|
49
|
+
interface TextBinding {
|
|
50
|
+
kind: 'text';
|
|
51
|
+
id: string;
|
|
52
|
+
signal: Signal<unknown>;
|
|
53
|
+
}
|
|
54
|
+
type Binding = AttrBinding | TextBinding;
|
|
55
|
+
|
|
1
56
|
/**
|
|
2
57
|
* JSX intrinsic-element types — kerf's per-tag attribute contracts.
|
|
3
58
|
*
|
|
@@ -32,10 +87,15 @@
|
|
|
32
87
|
* nothing. Use `delegate()` / `delegateCapture()` instead.
|
|
33
88
|
*/
|
|
34
89
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Every kerf attribute value resolves to one of these. A `ReadonlySignal`
|
|
92
|
+
* (covariant — accepts both `signal()` and `computed()` of any T) is a
|
|
93
|
+
* KF-294 fine-grained attribute binding: handed a signal, the runtime updates
|
|
94
|
+
* that attribute directly on change instead of re-running the render.
|
|
95
|
+
*/
|
|
96
|
+
type AttrValue = string | number | boolean | null | undefined | SafeHtml | ReadonlySignal<unknown>;
|
|
37
97
|
/** A typed-narrowing helper: `AttrLike<'a'|'b'>` accepts the literals plus the runtime fall-throughs. */
|
|
38
|
-
type AttrLike<T = string> = T | SafeHtml | null | undefined
|
|
98
|
+
type AttrLike<T = string> = T | SafeHtml | null | undefined | ReadonlySignal<unknown>;
|
|
39
99
|
/**
|
|
40
100
|
* `data-*` and `aria-*` index signatures. Applied via `KerfBaseAttrs` so
|
|
41
101
|
* every typed element accepts them without per-element enumeration.
|
|
@@ -702,6 +762,7 @@ interface KerfBuiltinIntrinsicElements {
|
|
|
702
762
|
* over rows we know are unchanged. The segment shape lets mount()
|
|
703
763
|
* skip both for the list parts.
|
|
704
764
|
*/
|
|
765
|
+
|
|
705
766
|
type Segment = StaticSegment | ListSegment | MixedSegment;
|
|
706
767
|
interface StaticSegment {
|
|
707
768
|
kind: 'static';
|
|
@@ -714,6 +775,13 @@ interface ListItem {
|
|
|
714
775
|
* existing live node; replaced ref → build a fresh node.
|
|
715
776
|
*/
|
|
716
777
|
ref: object;
|
|
778
|
+
/**
|
|
779
|
+
* KF-294: the row's fine-grained binding specs (signals in row attrs/text).
|
|
780
|
+
* Undefined for granular-path rows (which snapshot in this spike). The
|
|
781
|
+
* snapshot reconciler wires these to the fresh row node and disposes them
|
|
782
|
+
* when the row is removed.
|
|
783
|
+
*/
|
|
784
|
+
bindings?: Binding[];
|
|
717
785
|
/**
|
|
718
786
|
* Optional cache-invalidation key that captures external state affecting
|
|
719
787
|
* this row's render (e.g. selection class). Different cacheKey on the
|
|
@@ -750,11 +818,13 @@ type ArrayPatchInternal = {
|
|
|
750
818
|
index: number;
|
|
751
819
|
item: object;
|
|
752
820
|
html: string;
|
|
821
|
+
bindings?: Binding[];
|
|
753
822
|
} | {
|
|
754
823
|
type: 'insert';
|
|
755
824
|
index: number;
|
|
756
825
|
item: object;
|
|
757
826
|
html: string;
|
|
827
|
+
bindings?: Binding[];
|
|
758
828
|
} | {
|
|
759
829
|
type: 'remove';
|
|
760
830
|
index: number;
|
|
@@ -808,6 +878,21 @@ declare class SafeHtml {
|
|
|
808
878
|
* Type guard for `SafeHtml`. Prefer this over `instanceof SafeHtml` — it works
|
|
809
879
|
* across module copies (e.g. when the consumer's bundler loads kerf's barrel
|
|
810
880
|
* and JSX-runtime entries as independent modules).
|
|
881
|
+
*
|
|
882
|
+
* Security note (KF-321): this is a duck-check on the global `Symbol.for` brand,
|
|
883
|
+
* so same-realm code *can* forge a "trusted" value — `{ [SAFE_HTML_BRAND]: true,
|
|
884
|
+
* __html: '<img onerror=…>' }` passes and bypasses escaping + the URL screen.
|
|
885
|
+
* This is intentional and not a vulnerability: minting the brand requires a
|
|
886
|
+
* Symbol key, which no data channel (JSON.parse, form/query/localStorage,
|
|
887
|
+
* structuredClone, JSON-based prototype-pollution) can produce — those all yield
|
|
888
|
+
* string keys. The only way to forge it is to run JS that writes the symbol, and
|
|
889
|
+
* such code can equally `import { raw }`. Forgery therefore grants no capability
|
|
890
|
+
* an attacker with code execution lacks — the same posture as React's
|
|
891
|
+
* `$$typeof: Symbol.for('react.element')`. The global `Symbol.for` (vs a
|
|
892
|
+
* module-private symbol) is a deliberate cross-bundle-recognition tradeoff (see
|
|
893
|
+
* the `SAFE_HTML_BRAND` note above); a private symbol would additionally block
|
|
894
|
+
* same-realm forgery, but only closes a non-threat at the cost of that
|
|
895
|
+
* recognition, so it's kept global by design.
|
|
811
896
|
*/
|
|
812
897
|
declare function isSafeHtml(value: unknown): value is SafeHtml;
|
|
813
898
|
/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */
|
|
@@ -830,7 +915,7 @@ declare function listSafeHtml(id: string, items: ListSegment['items']): SafeHtml
|
|
|
830
915
|
* row rendering.
|
|
831
916
|
*/
|
|
832
917
|
declare function granularListSafeHtml(id: string, items: ListSegment['items'], patches: NonNullable<ListSegment['patches']>): SafeHtml;
|
|
833
|
-
type Child = SafeHtml | string | number | boolean | null | undefined
|
|
918
|
+
type Child = SafeHtml | string | number | boolean | null | undefined | ReadonlySignal<unknown>;
|
|
834
919
|
type Children = Child | Children[];
|
|
835
920
|
interface Props {
|
|
836
921
|
children?: Children;
|
package/dist/jsx-runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { Fragment, SafeHtml, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw } from './chunk-
|
|
1
|
+
export { Fragment, SafeHtml, granularListSafeHtml, isSafeHtml, jsx, jsx as jsxDEV, jsx as jsxs, listSafeHtml, raw } from './chunk-QNYOMGI4.js';
|
|
2
|
+
import './chunk-4E26PO2C.js';
|
|
2
3
|
//# sourceMappingURL=jsx-runtime.js.map
|
|
3
4
|
//# sourceMappingURL=jsx-runtime.js.map
|
package/dist/testing.js
CHANGED
package/llms.txt
CHANGED
|
@@ -37,4 +37,4 @@ kerf renders JSX to a structured `SafeHtml` (string for static content; tagged "
|
|
|
37
37
|
|
|
38
38
|
## Examples
|
|
39
39
|
|
|
40
|
-
- [reactivity-demo](https://github.com/brianwestphal/kerf/blob/main/examples/reactivity-demo):
|
|
40
|
+
- [reactivity-demo](https://github.com/brianwestphal/kerf/blob/main/examples/reactivity-demo): nine-section live demo exercising every primitive — counter, multi-consumer store, focus survival, keyed list, morph-skip, JSX-rendered SVG, capture-phase delegation, granular arraySignal, fine-grained signal bindings. Runs live at <https://brianwestphal.github.io/kerf/demo/>.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kerfjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Tiny reactive UI framework — fine-grained signals + DOM morphing + JSX. Apply the smallest possible cut to update your DOM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"test:dist:scaffold-typing": "npm run build && tsc -p tests/dist/scaffold-typing/tsconfig.json",
|
|
78
78
|
"test:browser": "npm run build && playwright test",
|
|
79
79
|
"bench:micro": "vitest bench --run --config vitest.config.bench.ts",
|
|
80
|
+
"bench:serve": "bash bench/site.sh",
|
|
80
81
|
"lint": "eslint src tests",
|
|
81
82
|
"typecheck": "tsc --noEmit",
|
|
82
83
|
"check": "npm run lint && npm run typecheck && node scripts/check-doc-test-inventory.mjs && node scripts/check-doc-api-coverage.mjs && node scripts/check-feature-coverage.mjs && node scripts/check-ai-bundle.mjs && npm test && npm run build && vitest run --config vitest.config.dist.ts && vitest run --config vitest.config.dist-full.ts && tsc -p tests/dist/jsx-typing/tsconfig.json && tsc -p site/src/examples/complete/tsconfig.json && tsc -p tests/dist/scaffold-typing/tsconfig.json && node scripts/check-docs-examples.mjs",
|
|
@@ -109,7 +110,7 @@
|
|
|
109
110
|
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
|
110
111
|
"@typescript-eslint/parser": "^8.18.0",
|
|
111
112
|
"@vitest/coverage-v8": "^3.0.0",
|
|
112
|
-
"domotion-svg": "^0.
|
|
113
|
+
"domotion-svg": "^0.21.1",
|
|
113
114
|
"eslint": "^9.16.0",
|
|
114
115
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
115
116
|
"gitgist": "^1.1.0",
|
package/dist/chunk-4VT4YZOO.js
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
// src/segment.ts
|
|
2
|
-
function flatten(segment, withMarkers) {
|
|
3
|
-
if (segment.kind === "static") return segment.html;
|
|
4
|
-
if (segment.kind === "list") {
|
|
5
|
-
const items = segment.items.map((i) => i.html).join("");
|
|
6
|
-
return withMarkers ? `<!--kf-list:${segment.id}-->${items}` : items;
|
|
7
|
-
}
|
|
8
|
-
return segment.parts.map((p) => flatten(p, withMarkers)).join("");
|
|
9
|
-
}
|
|
10
|
-
function flattenWithoutListItems(segment) {
|
|
11
|
-
if (segment.kind === "static") return segment.html;
|
|
12
|
-
if (segment.kind === "list") return `<!--kf-list:${segment.id}-->`;
|
|
13
|
-
return segment.parts.map(flattenWithoutListItems).join("");
|
|
14
|
-
}
|
|
15
|
-
function collectLists(segment, out = /* @__PURE__ */ new Map()) {
|
|
16
|
-
if (segment.kind === "list") out.set(segment.id, segment);
|
|
17
|
-
else if (segment.kind === "mixed") {
|
|
18
|
-
for (const part of segment.parts) collectLists(part, out);
|
|
19
|
-
}
|
|
20
|
-
return out;
|
|
21
|
-
}
|
|
22
|
-
function mergeChildSegments(parts) {
|
|
23
|
-
if (parts.length === 0) return { kind: "static", html: "" };
|
|
24
|
-
if (parts.every((p) => p.kind === "static")) {
|
|
25
|
-
return {
|
|
26
|
-
kind: "static",
|
|
27
|
-
html: parts.map((p) => p.html).join("")
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
const merged = [];
|
|
31
|
-
let coalesced = "";
|
|
32
|
-
for (const p of parts) {
|
|
33
|
-
if (p.kind === "static") {
|
|
34
|
-
coalesced += p.html;
|
|
35
|
-
} else {
|
|
36
|
-
if (coalesced !== "") {
|
|
37
|
-
merged.push({ kind: "static", html: coalesced });
|
|
38
|
-
coalesced = "";
|
|
39
|
-
}
|
|
40
|
-
merged.push(p);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (coalesced !== "") merged.push({ kind: "static", html: coalesced });
|
|
44
|
-
return { kind: "mixed", parts: merged };
|
|
45
|
-
}
|
|
46
|
-
function wrapWithTags(child, openTag, closeTag) {
|
|
47
|
-
if (child.kind === "static") {
|
|
48
|
-
return { kind: "static", html: openTag + child.html + closeTag };
|
|
49
|
-
}
|
|
50
|
-
if (child.kind === "mixed") {
|
|
51
|
-
return {
|
|
52
|
-
kind: "mixed",
|
|
53
|
-
parts: [
|
|
54
|
-
{ kind: "static", html: openTag },
|
|
55
|
-
...child.parts,
|
|
56
|
-
{ kind: "static", html: closeTag }
|
|
57
|
-
]
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
kind: "mixed",
|
|
62
|
-
parts: [
|
|
63
|
-
{ kind: "static", html: openTag },
|
|
64
|
-
child,
|
|
65
|
-
{ kind: "static", html: closeTag }
|
|
66
|
-
]
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// src/utils/escapeHtml.ts
|
|
71
|
-
function escapeHtml(str) {
|
|
72
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
73
|
-
}
|
|
74
|
-
function escapeAttr(str) {
|
|
75
|
-
return str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// src/utils/jsx-attr-aliases.ts
|
|
79
|
-
var ATTR_ALIASES = {
|
|
80
|
-
// HTML attributes
|
|
81
|
-
className: "class",
|
|
82
|
-
htmlFor: "for",
|
|
83
|
-
httpEquiv: "http-equiv",
|
|
84
|
-
acceptCharset: "accept-charset",
|
|
85
|
-
accessKey: "accesskey",
|
|
86
|
-
autoCapitalize: "autocapitalize",
|
|
87
|
-
autoComplete: "autocomplete",
|
|
88
|
-
autoFocus: "autofocus",
|
|
89
|
-
autoPlay: "autoplay",
|
|
90
|
-
colSpan: "colspan",
|
|
91
|
-
contentEditable: "contenteditable",
|
|
92
|
-
crossOrigin: "crossorigin",
|
|
93
|
-
dateTime: "datetime",
|
|
94
|
-
defaultChecked: "checked",
|
|
95
|
-
defaultValue: "value",
|
|
96
|
-
encType: "enctype",
|
|
97
|
-
formAction: "formaction",
|
|
98
|
-
formEncType: "formenctype",
|
|
99
|
-
formMethod: "formmethod",
|
|
100
|
-
formNoValidate: "formnovalidate",
|
|
101
|
-
formTarget: "formtarget",
|
|
102
|
-
hrefLang: "hreflang",
|
|
103
|
-
inputMode: "inputmode",
|
|
104
|
-
maxLength: "maxlength",
|
|
105
|
-
minLength: "minlength",
|
|
106
|
-
noModule: "nomodule",
|
|
107
|
-
noValidate: "novalidate",
|
|
108
|
-
readOnly: "readonly",
|
|
109
|
-
referrerPolicy: "referrerpolicy",
|
|
110
|
-
rowSpan: "rowspan",
|
|
111
|
-
spellCheck: "spellcheck",
|
|
112
|
-
srcDoc: "srcdoc",
|
|
113
|
-
srcLang: "srclang",
|
|
114
|
-
srcSet: "srcset",
|
|
115
|
-
tabIndex: "tabindex",
|
|
116
|
-
useMap: "usemap",
|
|
117
|
-
// SVG presentation attributes (camelCase → kebab-case)
|
|
118
|
-
strokeWidth: "stroke-width",
|
|
119
|
-
strokeLinecap: "stroke-linecap",
|
|
120
|
-
strokeLinejoin: "stroke-linejoin",
|
|
121
|
-
strokeDasharray: "stroke-dasharray",
|
|
122
|
-
strokeDashoffset: "stroke-dashoffset",
|
|
123
|
-
strokeMiterlimit: "stroke-miterlimit",
|
|
124
|
-
strokeOpacity: "stroke-opacity",
|
|
125
|
-
fillOpacity: "fill-opacity",
|
|
126
|
-
fillRule: "fill-rule",
|
|
127
|
-
clipPath: "clip-path",
|
|
128
|
-
clipRule: "clip-rule",
|
|
129
|
-
colorInterpolation: "color-interpolation",
|
|
130
|
-
colorInterpolationFilters: "color-interpolation-filters",
|
|
131
|
-
floodColor: "flood-color",
|
|
132
|
-
floodOpacity: "flood-opacity",
|
|
133
|
-
lightingColor: "lighting-color",
|
|
134
|
-
stopColor: "stop-color",
|
|
135
|
-
stopOpacity: "stop-opacity",
|
|
136
|
-
shapeRendering: "shape-rendering",
|
|
137
|
-
imageRendering: "image-rendering",
|
|
138
|
-
textRendering: "text-rendering",
|
|
139
|
-
pointerEvents: "pointer-events",
|
|
140
|
-
vectorEffect: "vector-effect",
|
|
141
|
-
paintOrder: "paint-order",
|
|
142
|
-
// SVG text/font attributes
|
|
143
|
-
fontFamily: "font-family",
|
|
144
|
-
fontSize: "font-size",
|
|
145
|
-
fontStyle: "font-style",
|
|
146
|
-
fontVariant: "font-variant",
|
|
147
|
-
fontWeight: "font-weight",
|
|
148
|
-
fontStretch: "font-stretch",
|
|
149
|
-
textAnchor: "text-anchor",
|
|
150
|
-
textDecoration: "text-decoration",
|
|
151
|
-
dominantBaseline: "dominant-baseline",
|
|
152
|
-
alignmentBaseline: "alignment-baseline",
|
|
153
|
-
baselineShift: "baseline-shift",
|
|
154
|
-
letterSpacing: "letter-spacing",
|
|
155
|
-
wordSpacing: "word-spacing",
|
|
156
|
-
writingMode: "writing-mode",
|
|
157
|
-
// SVG marker attributes
|
|
158
|
-
markerStart: "marker-start",
|
|
159
|
-
markerMid: "marker-mid",
|
|
160
|
-
markerEnd: "marker-end",
|
|
161
|
-
// SVG xlink (legacy but still used)
|
|
162
|
-
xlinkHref: "xlink:href",
|
|
163
|
-
xlinkShow: "xlink:show",
|
|
164
|
-
xlinkActuate: "xlink:actuate",
|
|
165
|
-
xlinkType: "xlink:type",
|
|
166
|
-
xlinkRole: "xlink:role",
|
|
167
|
-
xlinkTitle: "xlink:title",
|
|
168
|
-
xlinkArcrole: "xlink:arcrole",
|
|
169
|
-
xmlBase: "xml:base",
|
|
170
|
-
xmlLang: "xml:lang",
|
|
171
|
-
xmlSpace: "xml:space",
|
|
172
|
-
xmlnsXlink: "xmlns:xlink"
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// src/jsx-runtime.ts
|
|
176
|
-
var SAFE_HTML_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.SafeHtml");
|
|
177
|
-
var SafeHtml = class {
|
|
178
|
-
__html;
|
|
179
|
-
__segment;
|
|
180
|
-
// Branded so `isSafeHtml()` recognizes instances from any copy of this module.
|
|
181
|
-
[SAFE_HTML_BRAND] = true;
|
|
182
|
-
constructor(input) {
|
|
183
|
-
if (typeof input === "string") {
|
|
184
|
-
this.__segment = { kind: "static", html: input };
|
|
185
|
-
this.__html = input;
|
|
186
|
-
} else {
|
|
187
|
-
this.__segment = input;
|
|
188
|
-
this.__html = flatten(input, false);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
toString() {
|
|
192
|
-
return this.__html;
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
function isSafeHtml(value) {
|
|
196
|
-
return typeof value === "object" && value !== null && value[SAFE_HTML_BRAND] === true;
|
|
197
|
-
}
|
|
198
|
-
function raw(html) {
|
|
199
|
-
return new SafeHtml(html);
|
|
200
|
-
}
|
|
201
|
-
function listSafeHtml(id, items) {
|
|
202
|
-
return new SafeHtml({ kind: "list", id, items });
|
|
203
|
-
}
|
|
204
|
-
function granularListSafeHtml(id, items, patches) {
|
|
205
|
-
return new SafeHtml({ kind: "list", id, items, patches });
|
|
206
|
-
}
|
|
207
|
-
var VOID_TAGS = /* @__PURE__ */ new Set([
|
|
208
|
-
"area",
|
|
209
|
-
"base",
|
|
210
|
-
"br",
|
|
211
|
-
"col",
|
|
212
|
-
"embed",
|
|
213
|
-
"hr",
|
|
214
|
-
"img",
|
|
215
|
-
"input",
|
|
216
|
-
"link",
|
|
217
|
-
"meta",
|
|
218
|
-
"source",
|
|
219
|
-
"track",
|
|
220
|
-
"wbr"
|
|
221
|
-
]);
|
|
222
|
-
function toSegment(child) {
|
|
223
|
-
if (child == null || typeof child === "boolean") return { kind: "static", html: "" };
|
|
224
|
-
if (isSafeHtml(child)) {
|
|
225
|
-
return child.__segment ?? { kind: "static", html: child.__html };
|
|
226
|
-
}
|
|
227
|
-
if (typeof child === "string") return { kind: "static", html: escapeHtml(child) };
|
|
228
|
-
if (typeof child === "number") return { kind: "static", html: String(child) };
|
|
229
|
-
if (Array.isArray(child)) return mergeChildSegments(child.map(toSegment));
|
|
230
|
-
const maybeNode = child;
|
|
231
|
-
if (typeof maybeNode === "object" && maybeNode !== null && ("nodeType" in maybeNode || "outerHTML" in maybeNode)) {
|
|
232
|
-
throw new Error(
|
|
233
|
-
"JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). Build the tree in one JSX expression and use querySelector after toElement() to get element refs."
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
throw new Error(
|
|
237
|
-
`JSX: unsupported child of type ${describeValue(child)}. Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), passing a function (call it first), or passing a Promise (await it before render).`
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
function describeValue(v) {
|
|
241
|
-
if (Array.isArray(v)) return "array";
|
|
242
|
-
if (typeof v === "object" && v !== null) {
|
|
243
|
-
const ctor = v.constructor?.name;
|
|
244
|
-
return ctor && ctor !== "Object" ? `object (${ctor})` : "object";
|
|
245
|
-
}
|
|
246
|
-
return typeof v;
|
|
247
|
-
}
|
|
248
|
-
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction", "action"]);
|
|
249
|
-
var DANGEROUS_URL_RE = /^\s*(?:(?:java|vb)script:|data:text\/html[;,])/i;
|
|
250
|
-
function renderAttr(key, value) {
|
|
251
|
-
const name = ATTR_ALIASES[key] ?? key;
|
|
252
|
-
if (value == null || value === false) return "";
|
|
253
|
-
if (value === true) return ` ${name}`;
|
|
254
|
-
let strValue;
|
|
255
|
-
if (isSafeHtml(value)) {
|
|
256
|
-
strValue = value.__html;
|
|
257
|
-
} else if (typeof value === "number") {
|
|
258
|
-
strValue = String(value);
|
|
259
|
-
} else if (typeof value === "string") {
|
|
260
|
-
if (URL_ATTRS.has(name) && DANGEROUS_URL_RE.test(value)) {
|
|
261
|
-
console.warn(
|
|
262
|
-
`JSX: dropped dangerous URL value for ${name}=${JSON.stringify(value.slice(0, 80))}. kerf blocks javascript:, vbscript:, and data:text/html URLs in href/src/formaction/action/xlink:href by default. Wrap in raw() if this is intentional (e.g. bookmarklets), or sanitize upstream.`
|
|
263
|
-
);
|
|
264
|
-
return "";
|
|
265
|
-
}
|
|
266
|
-
strValue = escapeAttr(value);
|
|
267
|
-
} else if (typeof value === "function" && /^on[A-Z]/.test(key)) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
`JSX: inline event handlers like ${key}={fn} are not supported by kerf's JSX \u2192 HTML-string runtime. Use event delegation from the mount root instead:
|
|
270
|
-
|
|
271
|
-
delegate(rootEl, 'click', '[data-action="..."]', (evt, target) => { ... });
|
|
272
|
-
<button data-action="...">click</button>
|
|
273
|
-
|
|
274
|
-
See docs/5-event-delegation.md for the tier-1/tier-2/tier-3 model.`
|
|
275
|
-
);
|
|
276
|
-
} else {
|
|
277
|
-
throw new Error(
|
|
278
|
-
`JSX: unsupported value for attribute "${key}" \u2014 got ${describeValue(value)}. Attribute values must be string, number, boolean, null, undefined, or SafeHtml. Did you mean to read .value off a Signal, or stringify the object first?`
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
return ` ${name}="${strValue}"`;
|
|
282
|
-
}
|
|
283
|
-
function jsx(tag, props) {
|
|
284
|
-
if (typeof tag === "function") return tag(props);
|
|
285
|
-
const { children, ...attrs } = props;
|
|
286
|
-
const attrStr = Object.entries(attrs).map(([k, v]) => renderAttr(k, v)).join("");
|
|
287
|
-
if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);
|
|
288
|
-
const childSegment = children != null ? toSegment(children) : { kind: "static", html: "" };
|
|
289
|
-
return new SafeHtml(wrapWithTags(childSegment, `<${tag}${attrStr}>`, `</${tag}>`));
|
|
290
|
-
}
|
|
291
|
-
function Fragment({ children }) {
|
|
292
|
-
return new SafeHtml(children != null ? toSegment(children) : { kind: "static", html: "" });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export { Fragment, SafeHtml, collectLists, flatten, flattenWithoutListItems, granularListSafeHtml, isSafeHtml, jsx, listSafeHtml, raw };
|
|
296
|
-
//# sourceMappingURL=chunk-4VT4YZOO.js.map
|
|
297
|
-
//# sourceMappingURL=chunk-4VT4YZOO.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/segment.ts","../src/utils/escapeHtml.ts","../src/utils/jsx-attr-aliases.ts","../src/jsx-runtime.ts"],"names":[],"mappings":"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","file":"chunk-4VT4YZOO.js","sourcesContent":["/**\n * `Segment` — kerf's structured render output.\n *\n * The JSX runtime emits a `SafeHtml` wrapping a `Segment`. Most renders\n * produce a single static segment (just an HTML string), which behaves\n * exactly like a string for backward compatibility. When the tree\n * contains a list (`each()`) or a parent whose children include a list,\n * the runtime emits a structured segment that `mount()` can dispatch\n * on — running its native keyed reconciler for the list parts and\n * leaving the static surrounds to the general-purpose diff.\n *\n * Why have a structured form at all: the perf bottleneck for huge\n * keyed lists isn't the per-row JSX work (which `each()` already\n * memoizes). It's that flattening every render's whole tree to one\n * big HTML string forces a full `innerHTML` parse and a tree walk\n * over rows we know are unchanged. The segment shape lets mount()\n * skip both for the list parts.\n */\n\nexport type Segment = StaticSegment | ListSegment | MixedSegment;\n\nexport interface StaticSegment {\n kind: 'static';\n html: string;\n}\n\nexport interface ListItem {\n /**\n * The row's object identity. Used by the reconciler to match new items\n * against live DOM nodes across renders. Unchanged ref → reuse the\n * existing live node; replaced ref → build a fresh node.\n */\n ref: object;\n /**\n * Optional cache-invalidation key that captures external state affecting\n * this row's render (e.g. selection class). Different cacheKey on the\n * same `ref` triggers a cache miss for that row. `undefined` when the\n * user didn't pass a `key` callback to `each()`.\n */\n cacheKey: unknown;\n html: string;\n}\n\nexport interface ListSegment {\n kind: 'list';\n id: string;\n items: ListItem[];\n /**\n * Optional granular patches (KF-92). When present, the list reconciler\n * applies these directly to the existing binding instead of doing a\n * full classify+reconcile pass. Emitted by `each()` when bound to an\n * `arraySignal`. Mutually exclusive with the `items` snapshot in the\n * sense that the snapshot is treated as informational/fall-back when\n * patches are present.\n */\n patches?: ArrayPatchInternal[];\n}\n\n/**\n * Internal patch shape used inside list segments. Mirrors `ArrayPatch<T>`\n * from `array-signal.ts` but typed against `object` so the segment layer\n * doesn't need to be generic. `update` / `insert` patches carry the row's\n * pre-rendered HTML — `each()` renders them at JSX-evaluation time inside a\n * try/catch so a throwing render falls back to the snapshot path (KF-99)\n * instead of leaving the signal and DOM divergent.\n */\nexport type ArrayPatchInternal =\n | { type: 'update'; index: number; item: object; html: string }\n | { type: 'insert'; index: number; item: object; html: string }\n | { type: 'remove'; index: number }\n | { type: 'move'; from: number; to: number }\n | { type: 'replace'; items: readonly object[] };\n\nexport interface MixedSegment {\n kind: 'mixed';\n parts: Segment[];\n}\n\n/**\n * Flatten a segment to a complete HTML string. Used for first render\n * (bulk innerHTML), for SSR-style consumption via `toString()`, and\n * for diagnostics.\n *\n * If `withMarkers` is set, list segments are wrapped in\n * `<!--kf-list:{id}-->` comments so the post-parse walk can find each\n * list's live parent. Plain (non-marker) flatten is what JSX consumers\n * see when they call `.toString()` on the SafeHtml.\n */\nexport function flatten(segment: Segment, withMarkers: boolean): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list') {\n const items = segment.items.map((i) => i.html).join('');\n return withMarkers ? `<!--kf-list:${segment.id}-->${items}` : items;\n }\n return segment.parts.map((p) => flatten(p, withMarkers)).join('');\n}\n\n/**\n * Variant of `flatten` for the static-only diff path on subsequent\n * renders. Lists are reduced to a single marker comment with no items\n * inside — the actual list children stay in the live DOM and are\n * reconciled separately. Keeping list items out of this string is\n * what makes the morph cheap on huge lists where most rows are\n * unchanged.\n */\nexport function flattenWithoutListItems(segment: Segment): string {\n if (segment.kind === 'static') return segment.html;\n if (segment.kind === 'list') return `<!--kf-list:${segment.id}-->`;\n return segment.parts.map(flattenWithoutListItems).join('');\n}\n\n/** Collect every `ListSegment` in the tree, keyed by its id. */\nexport function collectLists(\n segment: Segment,\n out: Map<string, ListSegment> = new Map(),\n): Map<string, ListSegment> {\n if (segment.kind === 'list') out.set(segment.id, segment);\n else if (segment.kind === 'mixed') {\n for (const part of segment.parts) collectLists(part, out);\n }\n return out;\n}\n\n/**\n * Combine a list of child segments into the smallest equivalent\n * representation: collapses adjacent statics into one static, returns\n * a single static if everything is static, otherwise a mixed segment\n * with statics coalesced.\n */\nexport function mergeChildSegments(parts: Segment[]): Segment {\n if (parts.length === 0) return { kind: 'static', html: '' };\n if (parts.every((p) => p.kind === 'static')) {\n return {\n kind: 'static',\n html: parts.map((p) => (p as StaticSegment).html).join(''),\n };\n }\n const merged: Segment[] = [];\n let coalesced = '';\n for (const p of parts) {\n if (p.kind === 'static') {\n coalesced += p.html;\n } else {\n if (coalesced !== '') {\n merged.push({ kind: 'static', html: coalesced });\n coalesced = '';\n }\n merged.push(p);\n }\n }\n if (coalesced !== '') merged.push({ kind: 'static', html: coalesced });\n return { kind: 'mixed', parts: merged };\n}\n\n/**\n * Wrap a child segment with surrounding open/close tags from the\n * parent JSX element. Used by the JSX runtime when constructing\n * `_jsx(tag, ...)` output.\n */\nexport function wrapWithTags(child: Segment, openTag: string, closeTag: string): Segment {\n if (child.kind === 'static') {\n return { kind: 'static', html: openTag + child.html + closeTag };\n }\n if (child.kind === 'mixed') {\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n ...child.parts,\n { kind: 'static', html: closeTag },\n ],\n };\n }\n return {\n kind: 'mixed',\n parts: [\n { kind: 'static', html: openTag },\n child,\n { kind: 'static', html: closeTag },\n ],\n };\n}\n","/**\n * HTML / attribute escaping for the JSX runtime. Identical to the helpers\n * used in any reasonable HTML emitter — included here so kerf has no extra\n * runtime dependencies beyond `@preact/signals-core`.\n */\n\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nexport function escapeAttr(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n}\n","/**\n * JSX → HTML / SVG attribute name aliases.\n *\n * The JSX runtime translates camelCase attributes (React convention) to\n * the kebab-case / colon-form names the browser actually wants. Anything\n * not in this map is passed through verbatim — `data-*`, `aria-*`, and\n * any custom attribute work without ceremony.\n *\n * Lives in its own module so `src/jsx-runtime.ts` can stay under the\n * 200-LOC project guideline; the bulk of `jsx-runtime.ts` was this table.\n */\n\nexport const ATTR_ALIASES: Record<string, string> = {\n // HTML attributes\n className: 'class',\n htmlFor: 'for',\n httpEquiv: 'http-equiv',\n acceptCharset: 'accept-charset',\n accessKey: 'accesskey',\n autoCapitalize: 'autocapitalize',\n autoComplete: 'autocomplete',\n autoFocus: 'autofocus',\n autoPlay: 'autoplay',\n colSpan: 'colspan',\n contentEditable: 'contenteditable',\n crossOrigin: 'crossorigin',\n dateTime: 'datetime',\n defaultChecked: 'checked',\n defaultValue: 'value',\n encType: 'enctype',\n formAction: 'formaction',\n formEncType: 'formenctype',\n formMethod: 'formmethod',\n formNoValidate: 'formnovalidate',\n formTarget: 'formtarget',\n hrefLang: 'hreflang',\n inputMode: 'inputmode',\n maxLength: 'maxlength',\n minLength: 'minlength',\n noModule: 'nomodule',\n noValidate: 'novalidate',\n readOnly: 'readonly',\n referrerPolicy: 'referrerpolicy',\n rowSpan: 'rowspan',\n spellCheck: 'spellcheck',\n srcDoc: 'srcdoc',\n srcLang: 'srclang',\n srcSet: 'srcset',\n tabIndex: 'tabindex',\n useMap: 'usemap',\n\n // SVG presentation attributes (camelCase → kebab-case)\n strokeWidth: 'stroke-width',\n strokeLinecap: 'stroke-linecap',\n strokeLinejoin: 'stroke-linejoin',\n strokeDasharray: 'stroke-dasharray',\n strokeDashoffset: 'stroke-dashoffset',\n strokeMiterlimit: 'stroke-miterlimit',\n strokeOpacity: 'stroke-opacity',\n fillOpacity: 'fill-opacity',\n fillRule: 'fill-rule',\n clipPath: 'clip-path',\n clipRule: 'clip-rule',\n colorInterpolation: 'color-interpolation',\n colorInterpolationFilters: 'color-interpolation-filters',\n floodColor: 'flood-color',\n floodOpacity: 'flood-opacity',\n lightingColor: 'lighting-color',\n stopColor: 'stop-color',\n stopOpacity: 'stop-opacity',\n shapeRendering: 'shape-rendering',\n imageRendering: 'image-rendering',\n textRendering: 'text-rendering',\n pointerEvents: 'pointer-events',\n vectorEffect: 'vector-effect',\n paintOrder: 'paint-order',\n\n // SVG text/font attributes\n fontFamily: 'font-family',\n fontSize: 'font-size',\n fontStyle: 'font-style',\n fontVariant: 'font-variant',\n fontWeight: 'font-weight',\n fontStretch: 'font-stretch',\n textAnchor: 'text-anchor',\n textDecoration: 'text-decoration',\n dominantBaseline: 'dominant-baseline',\n alignmentBaseline: 'alignment-baseline',\n baselineShift: 'baseline-shift',\n letterSpacing: 'letter-spacing',\n wordSpacing: 'word-spacing',\n writingMode: 'writing-mode',\n\n // SVG marker attributes\n markerStart: 'marker-start',\n markerMid: 'marker-mid',\n markerEnd: 'marker-end',\n\n // SVG xlink (legacy but still used)\n xlinkHref: 'xlink:href',\n xlinkShow: 'xlink:show',\n xlinkActuate: 'xlink:actuate',\n xlinkType: 'xlink:type',\n xlinkRole: 'xlink:role',\n xlinkTitle: 'xlink:title',\n xlinkArcrole: 'xlink:arcrole',\n xmlBase: 'xml:base',\n xmlLang: 'xml:lang',\n xmlSpace: 'xml:space',\n xmlnsXlink: 'xmlns:xlink',\n};\n","/**\n * kerf JSX runtime.\n *\n * JSX renders to `SafeHtml`, which wraps both:\n * - `__html`: the flattened HTML string (what `toString()` returns; what\n * legacy/SSR consumers care about)\n * - `__segment`: a structured representation that distinguishes \"static\n * html\", \"keyed list\", and \"mixed\" content.\n *\n * Most renders are pure-static and the segment is just `{kind:'static',html}`.\n * When the tree contains a list (via `each()`) or a parent whose children\n * include a non-static segment, the runtime threads that structure up so\n * `mount()` can dispatch on it — running its native keyed reconciler for\n * the list parts and leaving the static surrounds to the general-purpose\n * diff.\n *\n * Configure in your `tsconfig.json`:\n *\n * \"jsx\": \"react-jsx\",\n * \"jsxImportSource\": \"kerfjs\"\n *\n * Then write JSX as you normally would — kerf provides the `jsx` /\n * `jsxs` / `jsxDEV` / `Fragment` exports the JSX transform looks for.\n */\n\nimport type { KerfBuiltinIntrinsicElements } from './jsx-types.js';\nimport {\n flatten,\n type ListSegment,\n mergeChildSegments,\n type Segment,\n wrapWithTags,\n} from './segment.js';\nimport { escapeAttr, escapeHtml } from './utils/escapeHtml.js';\nimport { ATTR_ALIASES } from './utils/jsx-attr-aliases.js';\n\n// Cross-realm/cross-bundle brand. Using `Symbol.for` (the global registry)\n// means two `SafeHtml` classes from different module copies still recognize\n// each other. Same approach React uses for `$$typeof: Symbol.for('react.element')`.\n// Without this, `instanceof SafeHtml` fails when the consumer's bundler ends\n// up loading two copies of kerf (separate barrel + jsx-runtime entries,\n// monorepo dedup misses, ESM/CJS interop, etc.).\nconst SAFE_HTML_BRAND = Symbol.for('kerfjs.SafeHtml');\n\nexport class SafeHtml {\n readonly __html: string;\n readonly __segment: Segment;\n // Branded so `isSafeHtml()` recognizes instances from any copy of this module.\n readonly [SAFE_HTML_BRAND] = true as const;\n constructor(input: string | Segment) {\n if (typeof input === 'string') {\n this.__segment = { kind: 'static', html: input };\n this.__html = input;\n } else {\n this.__segment = input;\n this.__html = flatten(input, false);\n }\n }\n toString(): string {\n return this.__html;\n }\n}\n\n/**\n * Type guard for `SafeHtml`. Prefer this over `instanceof SafeHtml` — it works\n * across module copies (e.g. when the consumer's bundler loads kerf's barrel\n * and JSX-runtime entries as independent modules).\n */\nexport function isSafeHtml(value: unknown): value is SafeHtml {\n return typeof value === 'object'\n && value !== null\n && (value as Record<symbol, unknown>)[SAFE_HTML_BRAND] === true;\n}\n\n/** Inject a pre-escaped HTML string. Use sparingly — caller is responsible for escaping. */\nexport function raw(html: string): SafeHtml {\n return new SafeHtml(html);\n}\n\n/**\n * Internal: build a `SafeHtml` representing a list segment. Used by\n * `each()` so the JSX runtime is the sole owner of `SafeHtml` construction.\n */\nexport function listSafeHtml(id: string, items: ListSegment['items']): SafeHtml {\n return new SafeHtml({ kind: 'list', id, items });\n}\n\n/**\n * Internal: build a `SafeHtml` representing a granular list segment with\n * patches (KF-92). The reconciler applies the patches to the existing\n * binding directly, skipping the per-item iteration that the snapshot\n * `listSafeHtml` requires. `items` is included for fall-through paths\n * (toString during SSR, fall-back when the binding doesn't exist yet).\n *\n * Patch HTML is rendered upstream (in `each()`) inside a try/catch — see\n * KF-99 — so by the time we get here every `update` / `insert` patch\n * already carries a `html` string, and the reconciler does no further\n * row rendering.\n */\nexport function granularListSafeHtml(\n id: string,\n items: ListSegment['items'],\n patches: NonNullable<ListSegment['patches']>,\n): SafeHtml {\n return new SafeHtml({ kind: 'list', id, items, patches });\n}\n\ntype Child = SafeHtml | string | number | boolean | null | undefined;\ntype Children = Child | Children[];\n\ninterface Props {\n children?: Children;\n [key: string]: unknown;\n}\n\nconst VOID_TAGS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'source', 'track', 'wbr',\n]);\n\n/**\n * Convert a single JSX child into a Segment. Handles SafeHtml passthrough,\n * primitive coercion + escaping, arrays (recursive), and the nullish/false\n * skip cases.\n */\nfunction toSegment(child: Children): Segment {\n if (child == null || typeof child === 'boolean') return { kind: 'static', html: '' };\n if (isSafeHtml(child)) {\n // Cross-bundle SafeHtml shims (KF-14 case) may have only `__html`.\n return child.__segment ?? { kind: 'static', html: child.__html };\n }\n if (typeof child === 'string') return { kind: 'static', html: escapeHtml(child) };\n if (typeof child === 'number') return { kind: 'static', html: String(child) };\n if (Array.isArray(child)) return mergeChildSegments(child.map(toSegment));\n // Catch the common mistake of passing a DOM element (e.g. the result of\n // toElement(...)) as a JSX child. The runtime renders to HTML strings, so\n // DOM nodes can't be composed — they'd silently serialize to \"\" and their\n // event listeners would be lost. Throw loudly so this can't sneak in.\n const maybeNode = child as unknown;\n if (typeof maybeNode === 'object' && maybeNode !== null\n && ('nodeType' in maybeNode || 'outerHTML' in maybeNode)) {\n throw new Error(\n 'JSX: DOM elements cannot be passed as children (the JSX runtime renders to HTML strings). '\n + 'Build the tree in one JSX expression and use querySelector after toElement() to get element refs.',\n );\n }\n throw new Error(\n `JSX: unsupported child of type ${describeValue(child)}. `\n + 'Children must be SafeHtml, string, number, boolean, null, undefined, or an array of those. '\n + 'Common mistakes: passing a Signal/Store object directly (use signal.value or store.state.value), '\n + 'passing a function (call it first), or passing a Promise (await it before render).',\n );\n}\n\nfunction describeValue(v: unknown): string {\n if (Array.isArray(v)) return 'array';\n if (typeof v === 'object' && v !== null) {\n const ctor = (v as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== 'Object' ? `object (${ctor})` : 'object';\n }\n return typeof v;\n}\n\n// URL-bearing HTML/SVG attributes. Plain-string values written here are\n// screened against `DANGEROUS_URL_RE` so a stored-XSS payload like\n// `<a href={userInput}>` with `userInput === 'javascript:alert(1)'` produces\n// a dropped attribute (and a console.warn) rather than a clickable script\n// vector. `SafeHtml` values (i.e. `raw(...)`) bypass the screen — that's the\n// documented opt-out for legitimate cases (bookmarklet builders, sanitized\n// inputs from a separate trust layer).\nconst URL_ATTRS = new Set(['href', 'src', 'xlink:href', 'formaction', 'action']);\nconst DANGEROUS_URL_RE = /^\\s*(?:(?:java|vb)script:|data:text\\/html[;,])/i;\n\nfunction renderAttr(key: string, value: unknown): string {\n const name = ATTR_ALIASES[key] ?? key;\n if (value == null || value === false) return '';\n if (value === true) return ` ${name}`;\n let strValue: string;\n if (isSafeHtml(value)) {\n strValue = value.__html;\n } else if (typeof value === 'number') {\n strValue = String(value);\n } else if (typeof value === 'string') {\n if (URL_ATTRS.has(name) && DANGEROUS_URL_RE.test(value)) {\n console.warn(\n `JSX: dropped dangerous URL value for ${name}=${JSON.stringify(value.slice(0, 80))}. `\n + 'kerf blocks javascript:, vbscript:, and data:text/html URLs in href/src/formaction/action/xlink:href by default. '\n + 'Wrap in raw() if this is intentional (e.g. bookmarklets), or sanitize upstream.',\n );\n return '';\n }\n strValue = escapeAttr(value);\n } else if (typeof value === 'function' && /^on[A-Z]/.test(key)) {\n throw new Error(\n `JSX: inline event handlers like ${key}={fn} are not supported by kerf's JSX → HTML-string runtime. `\n + 'Use event delegation from the mount root instead:\\n\\n'\n + ' delegate(rootEl, \\'click\\', \\'[data-action=\"...\"]\\', (evt, target) => { ... });\\n'\n + ' <button data-action=\"...\">click</button>\\n\\n'\n + 'See docs/5-event-delegation.md for the tier-1/tier-2/tier-3 model.',\n );\n } else {\n throw new Error(\n `JSX: unsupported value for attribute \"${key}\" — got ${describeValue(value)}. `\n + 'Attribute values must be string, number, boolean, null, undefined, or SafeHtml. '\n + 'Did you mean to read .value off a Signal, or stringify the object first?',\n );\n }\n return ` ${name}=\"${strValue}\"`;\n}\n\nexport function jsx(tag: string | ((props: Props) => SafeHtml), props: Props): SafeHtml {\n if (typeof tag === 'function') return tag(props);\n\n const { children, ...attrs } = props;\n const attrStr = Object.entries(attrs)\n .map(([k, v]) => renderAttr(k, v))\n .join('');\n\n if (VOID_TAGS.has(tag)) return new SafeHtml(`<${tag}${attrStr}>`);\n\n const childSegment: Segment = children != null\n ? toSegment(children)\n : { kind: 'static', html: '' };\n return new SafeHtml(wrapWithTags(childSegment, `<${tag}${attrStr}>`, `</${tag}>`));\n}\n\nexport { jsx as jsxs };\n// vitest's dev-mode JSX transform emits `jsxDEV(tag, props, ...)`; the\n// alias lets tests import this module without the production build pipeline\n// caring.\nexport { jsx as jsxDEV };\n\nexport function Fragment({ children }: { children?: Children }): SafeHtml {\n return new SafeHtml(children != null ? toSegment(children) : { kind: 'static', html: '' });\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace JSX {\n export type Element = SafeHtml;\n export interface ElementChildrenAttribute {\n children: unknown;\n }\n // Per-tag attribute contracts live in `./jsx-types.ts` as\n // `KerfBuiltinIntrinsicElements`. Re-exposed as an **interface** (not a\n // type alias) so consumers can declaration-merge custom-element tags\n // (KF-100):\n //\n // declare module 'kerfjs/jsx-runtime' {\n // namespace JSX {\n // interface IntrinsicElements {\n // 'my-element': KerfCustomElement & { foo?: string };\n // }\n // }\n // }\n //\n // KF-123: the imported interface is named `KerfBuiltinIntrinsicElements`\n // upstream so tsup/tsc cannot strip an import alias and end up emitting\n // `interface IntrinsicElements extends IntrinsicElements {}` in the .d.ts\n // — that shadowed form self-resolves to empty and breaks every `<tag>` in\n // consumer .tsx with TS2339. Verified against `dist/jsx-runtime.d.ts` by\n // `tests/dist/jsx-typing/` on every `npm run build`.\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n export interface IntrinsicElements extends KerfBuiltinIntrinsicElements {}\n}\n\n/**\n * Public re-exports of the JSX type primitives so consumers can compose\n * attribute interfaces for custom elements without reaching into\n * `kerfjs/jsx-types` (which is intentionally not in `package.json#exports`).\n */\nexport type {\n AttrLike,\n AttrValue,\n DataAriaAttrs,\n KerfBaseAttrs,\n KerfCustomElement,\n} from './jsx-types.js';\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/dev-delegate-warn.ts","../src/dev-signal.ts","../src/reactive.ts"],"names":["coreSignal","coreEffect"],"mappings":";;;;;;AAyBA,IAAI,KAAA,GAAQ,CAAA;AACZ,IAAI,MAAA,GAAS,KAAA;AAEb,SAAS,SAAA,GAAqB;AAC5B,EAAA,MAAM,OAAQ,UAAA,CAA0E,OAAA;AACxF,EAAA,IAAI,IAAA,EAAM,GAAA,EAAK,QAAA,KAAa,YAAA,EAAc,OAAO,KAAA;AACjD,EAAA,OAAO,IAAA,EAAM,KAAK,gCAAA,KAAqC,GAAA;AACzD;AAGO,SAAS,WAAA,GAAoB;AAClC,EAAA,KAAA,EAAA;AACF;AAGO,SAAS,UAAA,GAAmB;AACjC,EAAA,KAAA,EAAA;AACF;AAGO,SAAS,gCAAA,GAA4C;AAC1D,EAAA,OAAO,SAAA,EAAU;AACnB;AAQO,SAAS,mBAAmB,EAAA,EAA0C;AAC3E,EAAA,IAAI,CAAC,WAAU,EAAG;AAClB,EAAA,IAAI,UAAU,CAAA,EAAG;AACjB,EAAA,IAAI,MAAA,EAAQ;AACZ,EAAA,MAAA,GAAS,IAAA;AACT,EAAA,OAAA,CAAQ,IAAA;AAAA,IACN,SAAS,EAAE,CAAA,gjBAAA;AAAA,GAOb;AACF;AC3CA,IAAM,eAAA,GACF,gUAAA;AAMG,IAAM,SAAA,GAAN,cAA2B,MAAA,CAAU;AAAA,EAClC,eAAA,GAAkB,KAAA;AAAA,EAClB,QAAA,GAAW,KAAA;AAAA,EACX,aAAA,GAAgB,KAAA;AAAA,EAExB,YAAY,OAAA,EAAa;AACvB,IAAA,KAAA,CAAM,OAAA,EAAc;AAAA,MAClB,OAAA,GAAyB;AACvB,QAAC,KAAiD,eAAA,GAAkB,IAAA;AAAA,MACtE;AAAA,KACD,CAAA;AACD,IAAA,IAAA,CAAK,aAAA,GAAgB,IAAA;AAAA,EACvB;AAAA,EAEA,IAAa,KAAA,GAAW;AAAE,IAAA,OAAO,KAAA,CAAM,KAAA;AAAA,EAAO;AAAA,EAC9C,IAAa,MAAM,CAAA,EAAM;AACvB,IAAA,KAAA,CAAM,KAAA,GAAQ,CAAA;AACd,IAAA,IAAI,KAAK,aAAA,IAAiB,CAAC,KAAK,eAAA,IAAmB,CAAC,KAAK,QAAA,EAAU;AACjE,MAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,MAAA,OAAA,CAAQ,KAAK,eAAe,CAAA;AAAA,IAC9B;AAAA,EACF;AACF,CAAA;AAEO,SAAS,yBAAA,GAAqC;AACnD,EAAA,MAAM,OAAQ,UAAA,CAA0E,OAAA;AACxF,EAAA,IAAI,IAAA,EAAM,GAAA,EAAK,QAAA,KAAa,YAAA,EAAc,OAAO,KAAA;AACjD,EAAA,OAAO,IAAA,EAAM,KAAK,+BAAA,KAAoC,GAAA;AACxD;AC9BO,SAAS,OAAU,KAAA,EAAsB;AAC9C,EAAA,IAAI,yBAAA,EAA0B,EAAG,OAAO,IAAI,UAAa,KAAU,CAAA;AACnE,EAAA,OAAOA,SAAW,KAAU,CAAA;AAC9B;AAEO,SAAS,OAAO,EAAA,EAA2C;AAChE,EAAA,IAAI,CAAC,gCAAA,EAAiC,EAAG,OAAOC,SAAW,EAAE,CAAA;AAC7D,EAAA,OAAOA,SAAW,MAAM;AACtB,IAAA,WAAA,EAAY;AACZ,IAAA,IAAI;AACF,MAAA,OAAO,EAAA,EAAG;AAAA,IACZ,CAAA,SAAE;AACA,MAAA,UAAA,EAAW;AAAA,IACb;AAAA,EACF,CAAC,CAAA;AACH","file":"chunk-N4KF3GD2.js","sourcesContent":["/**\n * Dev-mode warning for `delegate()` / `delegateCapture()` calls that run\n * inside an `effect()` body (KERF_DEV_WARN_DELEGATE_IN_EFFECT=1).\n *\n * Why the pattern matters: every effect re-run executes its body fresh, which\n * means a `delegate()` call inside the body installs a NEW root listener on\n * each re-run. The effect's disposer cleans up the reactive subscription but\n * not the side-effects the body produced — so previous listeners stay\n * attached, the per-listener closure pins `rootEl` / `handler` / everything\n * the handler closes over, and listener count grows linearly with signal\n * churn. Structurally identical to the addEventListener-inside-mount foot-gun\n * (Hard Rule 4) but doesn't *look* like it.\n *\n * Static analysis can't reliably detect \"inside an effect\" without flow\n * information (effect() is just a function call), so the canonical defense\n * is this runtime opt-in warning. When enabled, `reactive.ts`'s `effect()`\n * wrapper increments a module-level counter before invoking the user body\n * and decrements after; `delegate.ts` checks the counter and fires the\n * warning once total.\n *\n * Production behavior is unchanged for zero runtime cost — the env-var check\n * short-circuits before any state is touched, and the wrapper in\n * `reactive.ts` only wraps when the gate is on.\n */\n\nlet depth = 0;\nlet warned = false;\n\nfunction isOptedIn(): boolean {\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n if (proc?.env?.NODE_ENV === 'production') return false;\n return proc?.env?.KERF_DEV_WARN_DELEGATE_IN_EFFECT === '1';\n}\n\n/** Called by the `effect()` wrapper in `reactive.ts` before running the user body. */\nexport function enterEffect(): void {\n depth++;\n}\n\n/** Called by the `effect()` wrapper in `reactive.ts` after the user body returns or throws. */\nexport function exitEffect(): void {\n depth--;\n}\n\n/** Public re-export of the env-var check so `reactive.ts` can decide whether to wrap. */\nexport function isDevWarnDelegateInEffectEnabled(): boolean {\n return isOptedIn();\n}\n\n/**\n * Called at the top of `delegate()` and `delegateCapture()`. If the call is\n * happening inside an `effect()` body (depth > 0) AND the env var is on, fire\n * a one-shot warning. The `fn` argument is the name of the caller for the\n * message (\"delegate\" vs \"delegateCapture\").\n */\nexport function warnIfInsideEffect(fn: 'delegate' | 'delegateCapture'): void {\n if (!isOptedIn()) return;\n if (depth === 0) return;\n if (warned) return;\n warned = true;\n console.warn(\n `kerf: ${fn}() was called inside an effect() body. `\n + 'Every effect re-run installs a fresh root listener; the effect disposer cleans up the '\n + 'reactive subscription but not the listeners, so listener count grows linearly with signal '\n + 'churn and each listener pins its handler closure. Register the delegate once at module '\n + 'or setup scope and gate behavior on the signal *inside the handler* where the read is free. '\n + 'See docs/5-event-delegation.md §5.3 \"When capturing the disposer still isn\\'t enough\". '\n + 'Set KERF_DEV_WARN_DELEGATE_IN_EFFECT=0 (or unset it) to silence this warning.',\n );\n}\n\n/** Test helper — resets the one-shot dedup flag and depth counter for unit tests. */\nexport function _resetWarnedForTests(): void {\n warned = false;\n depth = 0;\n}\n","/**\n * Dev-mode signal subclass with subscriber tracking (KF-176). When the\n * dev-warn opt-in is enabled, `signal()` returns a `DevSignal` that emits a\n * one-shot `console.warn` the first time `.value` is written to an instance\n * that has never had a subscriber attached. This surfaces the canonical\n * Rule 7 violation (read `.value` outside a render fn / effect — the read\n * doesn't subscribe, so subsequent writes silently fail to re-render) at\n * the moment the user makes the wrong write, instead of leaving them to\n * notice that their UI never updates.\n *\n * The gate is `process.env.NODE_ENV !== 'production'` AND\n * `KERF_DEV_WARN_UNTRACKED_SIGNALS === '1'`. Off by default because the\n * heuristic produces false positives for purely imperative signals (used as\n * mutable cells with no UI consumer); opt-in is the right shape until a\n * sharper heuristic is found. Production behavior is unchanged for zero\n * runtime cost.\n *\n * The subclass uses signals-core's `SignalOptions.watched` callback to set a\n * per-instance `__hasSubscriber` flag — fired by signals-core when the first\n * subscriber attaches. We never clear the flag on `unwatched`, so a signal\n * that *was* subscribed at some point won't warn even if its subscribers\n * later detach.\n */\n\nimport { Signal } from '@preact/signals-core';\n\nconst WARNING_MESSAGE\n = 'kerf: signal was written but has no subscribers. '\n + 'Did you read `.value` outside of a render fn / effect()? '\n + 'Hoisted reads do not subscribe, so subsequent writes will not re-render. '\n + 'Move the read inside mount()\\'s render fn or effect() callback. '\n + 'Set KERF_DEV_WARN_UNTRACKED_SIGNALS=0 (or unset it) to silence this warning.';\n\nexport class DevSignal<T> extends Signal<T> {\n private __hasSubscriber = false;\n private __warned = false;\n private __constructed = false;\n\n constructor(initial?: T) {\n super(initial as T, {\n watched(this: Signal<T>) {\n (this as unknown as { __hasSubscriber: boolean }).__hasSubscriber = true;\n },\n });\n this.__constructed = true;\n }\n\n override get value(): T { return super.value; }\n override set value(v: T) {\n super.value = v;\n if (this.__constructed && !this.__hasSubscriber && !this.__warned) {\n this.__warned = true;\n console.warn(WARNING_MESSAGE);\n }\n }\n}\n\nexport function isDevWarnUntrackedEnabled(): boolean {\n const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;\n if (proc?.env?.NODE_ENV === 'production') return false;\n return proc?.env?.KERF_DEV_WARN_UNTRACKED_SIGNALS === '1';\n}\n","/**\n * Re-exports of `@preact/signals-core`. Lets the rest of the codebase depend\n * on `'./reactive.js'` without naming the underlying lib, so swapping it out\n * later (or fronting it with a hand-rolled implementation) is a one-file\n * change.\n *\n * Two dev-gated wrappers sit in front of the bare re-exports:\n *\n * - `signal()` returns a `DevSignal` when `KERF_DEV_WARN_UNTRACKED_SIGNALS=1`\n * (KF-176) — warns on writes to signals with no subscribers.\n *\n * - `effect()` wraps the user body in `enterEffect()` / `exitEffect()` calls\n * when `KERF_DEV_WARN_DELEGATE_IN_EFFECT=1` so `delegate()` can detect when\n * it's running inside an effect body and fire the appropriate warning.\n *\n * Both gates short-circuit on `NODE_ENV === 'production'` — production\n * always sees the bare `@preact/signals-core` exports with zero overhead.\n */\n\nimport { effect as coreEffect,type Signal,signal as coreSignal } from '@preact/signals-core';\n\nimport { enterEffect, exitEffect, isDevWarnDelegateInEffectEnabled } from './dev-delegate-warn.js';\nimport { DevSignal, isDevWarnUntrackedEnabled } from './dev-signal.js';\n\nexport {\n batch,\n computed,\n type ReadonlySignal,\n type Signal,\n} from '@preact/signals-core';\n\nexport function signal<T>(value?: T): Signal<T> {\n if (isDevWarnUntrackedEnabled()) return new DevSignal<T>(value as T) as Signal<T>;\n return coreSignal(value as T);\n}\n\nexport function effect(fn: () => void | (() => void)): () => void {\n if (!isDevWarnDelegateInEffectEnabled()) return coreEffect(fn);\n return coreEffect(() => {\n enterEffect();\n try {\n return fn();\n } finally {\n exitEffect();\n }\n });\n}\n"]}
|