@kudzujs/core 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/framework/README.md +1 -1
- package/framework/build.mjs +164 -24
- package/framework/core.d.ts +3 -0
- package/framework/core.mjs +42 -10
- package/framework/list-runtime.js +59 -11
- package/framework/native-runtime.js +31 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -150,16 +150,25 @@ Map local array state directly to one keyed JSX element per item:
|
|
|
150
150
|
|
|
151
151
|
```tsx
|
|
152
152
|
const [items, setItems] = useState([
|
|
153
|
-
{ id: 1, name: "Oak" },
|
|
154
|
-
{ id: 2, name: "Pine" }
|
|
153
|
+
{ id: 1, name: "Oak", done: false },
|
|
154
|
+
{ id: 2, name: "Pine", done: true }
|
|
155
155
|
])
|
|
156
156
|
|
|
157
|
-
<ul>{items.map(item =>
|
|
157
|
+
<ul>{items.map(item =>
|
|
158
|
+
<li
|
|
159
|
+
key={item.id}
|
|
160
|
+
className={item.done ? "done" : "active"}
|
|
161
|
+
aria-label={`${item.name} item`}
|
|
162
|
+
>
|
|
163
|
+
{item.name.toUpperCase()}
|
|
164
|
+
<button onClick={() => setItems(items.filter(entry => entry.id !== item.id))}>Remove</button>
|
|
165
|
+
</li>
|
|
166
|
+
)}</ul>
|
|
158
167
|
```
|
|
159
168
|
|
|
160
|
-
Kudzu emits initial items as static HTML, then adds, removes, updates, and moves keyed elements directly. Existing keys move without remounting, preserving uncontrolled descendant state.
|
|
169
|
+
Kudzu emits initial items as static HTML, then adds, removes, updates, and moves keyed elements directly. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Item-local handlers use delegated events and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
|
|
161
170
|
|
|
162
|
-
The
|
|
171
|
+
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a direct local-state `.map`, one identifier callback parameter, one intrinsic JSX root, and `key={item.<field>}`. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, locals, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Nested conditions or lists, item spreads, component tags, fragments, and reactive `style`, `ref`, or `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
163
172
|
|
|
164
173
|
## Normal JavaScript
|
|
165
174
|
|
|
@@ -181,6 +190,8 @@ async function load() {
|
|
|
181
190
|
|
|
182
191
|
Primitive values, arrays, plain objects, and destructured props can be captured by client handlers. Functions, symbols, bigints, cycles, class instances, and imported helper functions are not yet supported as captures.
|
|
183
192
|
|
|
193
|
+
Native handlers are delegated after normal event bubbling and run from the target toward matching Kudzu ancestors in deterministic order. Delegated handlers cannot call or reference `preventDefault`, `stopPropagation`, or `stopImmediatePropagation`; the compiler rejects those methods because external ESM cannot apply them with correct synchronous DOM semantics.
|
|
194
|
+
|
|
184
195
|
## Rendering
|
|
185
196
|
|
|
186
197
|
```text
|
package/framework/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
- `runtime.js`: command-only runtime for direct state-to-text patches.
|
|
7
7
|
- `shared-runtime.js`: command runtime with capability commit and DOM lifecycle hooks, emitted only when needed.
|
|
8
8
|
- `binding-runtime.js`: optional generic attributes, form properties, and conditional range patches.
|
|
9
|
-
- `list-runtime.js`: optional keyed list validation,
|
|
9
|
+
- `list-runtime.js`: optional keyed list validation, external item-expression evaluation, dynamic item-handler scopes, moves, and cleanup.
|
|
10
10
|
- `serialization.js`: capture deserialization shared by binding and native handlers.
|
|
11
11
|
- `native-runtime.js`: optional runtime for normal synchronous and asynchronous ESM handlers.
|
|
12
12
|
- `*.d.ts`: public TypeScript and JSX declarations.
|
package/framework/build.mjs
CHANGED
|
@@ -150,6 +150,7 @@ async function compile(file) {
|
|
|
150
150
|
const source = await readFile(file, "utf8")
|
|
151
151
|
const nativeHandlers = []
|
|
152
152
|
const reactiveBindings = []
|
|
153
|
+
const listExpressions = []
|
|
153
154
|
const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
|
|
154
155
|
const result = ts.transpileModule(source, {
|
|
155
156
|
fileName: file,
|
|
@@ -159,7 +160,7 @@ async function compile(file) {
|
|
|
159
160
|
jsx: ts.JsxEmit.ReactJSX,
|
|
160
161
|
jsxImportSource: "@kudzujs/core"
|
|
161
162
|
},
|
|
162
|
-
transformers: { before: [createKudzuTransformer(nativeHandlers, reactiveBindings, `/assets/${handlerPath}`)] },
|
|
163
|
+
transformers: { before: [createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, `/assets/${handlerPath}`)] },
|
|
163
164
|
reportDiagnostics: true
|
|
164
165
|
})
|
|
165
166
|
|
|
@@ -172,10 +173,11 @@ async function compile(file) {
|
|
|
172
173
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
173
174
|
await writeFile(output, result.outputText)
|
|
174
175
|
|
|
175
|
-
if (!nativeHandlers.length && !reactiveBindings.length) return undefined
|
|
176
|
+
if (!nativeHandlers.length && !reactiveBindings.length && !listExpressions.length) return undefined
|
|
176
177
|
const moduleSource = [
|
|
177
178
|
...nativeHandlers.map(handler => printNativeHandler(handler)),
|
|
178
|
-
...reactiveBindings.map(entry => printReactiveBinding(entry))
|
|
179
|
+
...reactiveBindings.map(entry => printReactiveBinding(entry)),
|
|
180
|
+
...listExpressions.map(entry => printListExpression(entry))
|
|
179
181
|
].join("\n")
|
|
180
182
|
const moduleResult = ts.transpileModule(moduleSource, {
|
|
181
183
|
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
|
|
@@ -186,12 +188,13 @@ async function compile(file) {
|
|
|
186
188
|
return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0 }
|
|
187
189
|
}
|
|
188
190
|
|
|
189
|
-
function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
|
|
191
|
+
function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, handlerUrl) {
|
|
190
192
|
return context => sourceFile => {
|
|
191
193
|
const factory = context.factory
|
|
192
194
|
const settersByFunction = new Map()
|
|
193
195
|
const functions = new Map()
|
|
194
|
-
const
|
|
196
|
+
const listValues = new WeakMap()
|
|
197
|
+
const listEventItems = new WeakMap()
|
|
195
198
|
let usesBehavior = false
|
|
196
199
|
let usesBinding = false
|
|
197
200
|
let usesConditional = false
|
|
@@ -237,13 +240,20 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
|
|
|
237
240
|
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
|
|
238
241
|
}
|
|
239
242
|
|
|
243
|
+
if (ts.isJsxExpression(node) && node.expression && listValues.has(node.expression)) {
|
|
244
|
+
return factory.updateJsxExpression(node, compileListValue(node.expression, listValues.get(node.expression), factory, listExpressions, handlerUrl))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && listValues.has(node.initializer.expression)) {
|
|
248
|
+
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compileListValue(node.initializer.expression, listValues.get(node.initializer.expression), factory, listExpressions, handlerUrl)))
|
|
249
|
+
}
|
|
250
|
+
|
|
240
251
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
241
252
|
const listParts = keyedListParts(node.expression, settersForNode(node, settersByFunction))
|
|
242
253
|
if (listParts) {
|
|
243
254
|
if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
|
|
244
|
-
validateKeyedList(listParts, sourceFile,
|
|
255
|
+
validateKeyedList(listParts, sourceFile, settersForNode(node, settersByFunction), listValues, listEventItems)
|
|
245
256
|
usesBehavior = true
|
|
246
|
-
usesBinding = true
|
|
247
257
|
usesList = true
|
|
248
258
|
return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, [
|
|
249
259
|
listParts.state,
|
|
@@ -269,7 +279,6 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
|
|
|
269
279
|
|
|
270
280
|
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !/^on/i.test(node.name.getText()) && !["style", "key", "ref", "dangerouslysetinnerhtml"].includes(node.name.getText().toLowerCase())) {
|
|
271
281
|
const expression = node.initializer.expression
|
|
272
|
-
if (listFieldExpressions.has(expression)) return node
|
|
273
282
|
const setters = settersForNode(node, settersByFunction)
|
|
274
283
|
const usedStates = referencedStateNames(expression, setters)
|
|
275
284
|
const captures = captureNames(expression, expression, setters)
|
|
@@ -283,7 +292,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
|
|
|
283
292
|
|
|
284
293
|
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.getText())) {
|
|
285
294
|
const setters = settersForNode(node, settersByFunction)
|
|
286
|
-
const event = compileEvent(node.initializer.expression, setters, functions, factory, nativeHandlers, handlerUrl)
|
|
295
|
+
const event = compileEvent(node.initializer.expression, setters, functions, factory, nativeHandlers, handlerUrl, listEventItems.get(node))
|
|
287
296
|
if (event) {
|
|
288
297
|
usesBehavior = true
|
|
289
298
|
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, event))
|
|
@@ -302,7 +311,12 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
|
|
|
302
311
|
if (nativeHandlers.length) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
|
|
303
312
|
if (usesBinding) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
|
|
304
313
|
if (usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("conditional"), factory.createIdentifier("__kConditional")))
|
|
305
|
-
if (usesList)
|
|
314
|
+
if (usesList) {
|
|
315
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("list"), factory.createIdentifier("__kList")))
|
|
316
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listExpression"), factory.createIdentifier("__kListExpression")))
|
|
317
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listField"), factory.createIdentifier("__kListField")))
|
|
318
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
|
|
319
|
+
}
|
|
306
320
|
if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
|
|
307
321
|
const behaviorImport = factory.createImportDeclaration(
|
|
308
322
|
undefined,
|
|
@@ -331,7 +345,7 @@ function keyedListParts(expression, setters) {
|
|
|
331
345
|
return { state, callback, root, item: callback.parameters[0].name.text, keyField: field }
|
|
332
346
|
}
|
|
333
347
|
|
|
334
|
-
function validateKeyedList(parts, sourceFile,
|
|
348
|
+
function validateKeyedList(parts, sourceFile, setters, listValues, listEventItems) {
|
|
335
349
|
const fail = (node, message) => {
|
|
336
350
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
337
351
|
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
|
|
@@ -341,27 +355,112 @@ function validateKeyedList(parts, sourceFile, listFieldExpressions) {
|
|
|
341
355
|
if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
|
|
342
356
|
}
|
|
343
357
|
const visit = node => {
|
|
358
|
+
if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
|
|
344
359
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
|
|
345
|
-
if (node !== parts.root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map") fail(node, "Nested keyed lists are not supported")
|
|
360
|
+
if (node !== parts.root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, "Nested keyed lists are not supported")
|
|
346
361
|
if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, parts.item)) fail(node, "Keyed list item spreads are not supported")
|
|
347
|
-
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.getText()))
|
|
362
|
+
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.getText())) {
|
|
363
|
+
listEventItems.set(node, parts.item)
|
|
364
|
+
return
|
|
365
|
+
}
|
|
348
366
|
if (ts.isJsxExpression(node) && node.expression) {
|
|
349
367
|
const expression = unwrapExpression(node.expression)
|
|
368
|
+
if (conditionalParts(expression) && containsJsx(expression)) fail(node, "Nested reactive conditions are not supported in keyed lists")
|
|
350
369
|
const field = directProperty(expression, parts.item)
|
|
351
|
-
const isRootKey =
|
|
370
|
+
const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.getText() === "key"
|
|
371
|
+
if (field && ["__proto__", "constructor", "prototype"].includes(field)) fail(node, `Keyed list item property "${field}" is not supported`)
|
|
352
372
|
if (field && ts.isJsxAttribute(node.parent) && ["style", "ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
|
|
353
|
-
if (isRootKey
|
|
354
|
-
|
|
373
|
+
if (isRootKey) return
|
|
374
|
+
if (field) {
|
|
375
|
+
listValues.set(node.expression, { field })
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
if (referencesIdentifier(expression, parts.item)) {
|
|
379
|
+
validateListExpression(expression, parts.item, node, fail)
|
|
380
|
+
if (ts.isJsxAttribute(node.parent) && ["style", "ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
|
|
381
|
+
listValues.set(node.expression, { item: parts.item })
|
|
355
382
|
return
|
|
356
383
|
}
|
|
357
|
-
if (conditionalParts(expression)) fail(node, "Nested reactive conditions are not supported in keyed lists")
|
|
358
|
-
if (referencesIdentifier(expression, parts.item)) fail(node, `Keyed list item expressions must be direct ${parts.item}.<field> reads`)
|
|
359
384
|
}
|
|
360
385
|
ts.forEachChild(node, visit)
|
|
361
386
|
}
|
|
362
387
|
visit(parts.root)
|
|
363
388
|
}
|
|
364
389
|
|
|
390
|
+
const pureListMethods = new Set(["at", "charAt", "charCodeAt", "concat", "endsWith", "includes", "indexOf", "join", "lastIndexOf", "padEnd", "padStart", "repeat", "replace", "replaceAll", "slice", "startsWith", "substring", "toLowerCase", "toUpperCase", "trim", "trimEnd", "trimStart"])
|
|
391
|
+
const mutatingListMethods = new Set(["copyWithin", "fill", "pop", "push", "reverse", "shift", "sort", "splice", "unshift"])
|
|
392
|
+
const pureMathMethods = new Set(["abs", "ceil", "floor", "max", "min", "pow", "round", "sign", "sqrt", "trunc"])
|
|
393
|
+
const pureListGlobals = new Set(["Boolean", "Infinity", "Math", "NaN", "Number", "String", "undefined"])
|
|
394
|
+
const assignmentOperators = new Set([
|
|
395
|
+
ts.SyntaxKind.EqualsToken, ts.SyntaxKind.PlusEqualsToken, ts.SyntaxKind.MinusEqualsToken,
|
|
396
|
+
ts.SyntaxKind.AsteriskEqualsToken, ts.SyntaxKind.AsteriskAsteriskEqualsToken, ts.SyntaxKind.SlashEqualsToken,
|
|
397
|
+
ts.SyntaxKind.PercentEqualsToken, ts.SyntaxKind.LessThanLessThanEqualsToken, ts.SyntaxKind.GreaterThanGreaterThanEqualsToken,
|
|
398
|
+
ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, ts.SyntaxKind.AmpersandEqualsToken, ts.SyntaxKind.BarEqualsToken,
|
|
399
|
+
ts.SyntaxKind.CaretEqualsToken, ts.SyntaxKind.BarBarEqualsToken, ts.SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
400
|
+
ts.SyntaxKind.QuestionQuestionEqualsToken
|
|
401
|
+
])
|
|
402
|
+
|
|
403
|
+
function validateListExpression(expression, item, source, fail) {
|
|
404
|
+
const visit = node => {
|
|
405
|
+
if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
|
|
406
|
+
const key = node.argumentExpression
|
|
407
|
+
if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(source, "Derived keyed list item computed properties require a direct string or numeric literal key")
|
|
408
|
+
if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(source, `Derived keyed list item property "${key.text}" is not supported`)
|
|
409
|
+
}
|
|
410
|
+
if (ts.isPropertyAccessExpression(node) && ["__proto__", "constructor", "prototype"].includes(node.name.text) || ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression) && ["__proto__", "constructor", "prototype"].includes(node.argumentExpression.text)) {
|
|
411
|
+
fail(source, "Derived keyed list item expressions cannot read __proto__, prototype, or constructor")
|
|
412
|
+
}
|
|
413
|
+
if (ts.isBinaryExpression(node) && assignmentOperators.has(node.operatorToken.kind) || ts.isPostfixUnaryExpression(node) || ts.isPrefixUnaryExpression(node) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) {
|
|
414
|
+
fail(source, "Derived keyed list item expressions must be pure; assignments and updates are not supported")
|
|
415
|
+
}
|
|
416
|
+
if (ts.isDeleteExpression(node) || ts.isAwaitExpression(node) || ts.isNewExpression(node) || ts.isYieldExpression(node)) {
|
|
417
|
+
fail(source, "Derived keyed list item expressions must be synchronous and side-effect free; delete, await, yield, and new are not supported")
|
|
418
|
+
}
|
|
419
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isTaggedTemplateExpression(node)) {
|
|
420
|
+
fail(source, "Derived keyed list item expressions cannot create or invoke arbitrary functions")
|
|
421
|
+
}
|
|
422
|
+
if (ts.isCallExpression(node)) {
|
|
423
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
424
|
+
const method = node.expression.name.text
|
|
425
|
+
if (mutatingListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call mutating method "${method}"`)
|
|
426
|
+
const receiver = node.expression.expression
|
|
427
|
+
const mathCall = ts.isIdentifier(receiver) && receiver.text === "Math" && pureMathMethods.has(method)
|
|
428
|
+
if (!mathCall && !pureListMethods.has(method)) fail(source, `Derived keyed list item expressions cannot call arbitrary method "${method}"`)
|
|
429
|
+
} else if (!ts.isIdentifier(node.expression) || !["Boolean", "Number", "String"].includes(node.expression.text)) {
|
|
430
|
+
fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && node.text !== item && !pureListGlobals.has(node.text)) {
|
|
434
|
+
fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
|
|
435
|
+
}
|
|
436
|
+
ts.forEachChild(node, visit)
|
|
437
|
+
}
|
|
438
|
+
visit(expression)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function containsJsx(root) {
|
|
442
|
+
let found = false
|
|
443
|
+
const visit = node => {
|
|
444
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) found = true
|
|
445
|
+
if (!found) ts.forEachChild(node, visit)
|
|
446
|
+
}
|
|
447
|
+
visit(root)
|
|
448
|
+
return found
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function compileListExpression(read, expression, item, factory, listExpressions, handlerUrl) {
|
|
452
|
+
const exportName = `listExpression${listExpressions.length}`
|
|
453
|
+
listExpressions.push({ exportName, expression, item })
|
|
454
|
+
return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)])
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function compileListValue(expression, entry, factory, listExpressions, handlerUrl) {
|
|
458
|
+
const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), expression)
|
|
459
|
+
return entry.field
|
|
460
|
+
? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
|
|
461
|
+
: compileListExpression(read, expression, entry.item, factory, listExpressions, handlerUrl)
|
|
462
|
+
}
|
|
463
|
+
|
|
365
464
|
function directProperty(expression, objectName) {
|
|
366
465
|
const value = unwrapExpression(expression)
|
|
367
466
|
if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
|
|
@@ -454,10 +553,11 @@ function factoryNull() {
|
|
|
454
553
|
return ts.factory.createNull()
|
|
455
554
|
}
|
|
456
555
|
|
|
457
|
-
function compileEvent(expression, setters, functions, factory, nativeHandlers, handlerUrl) {
|
|
556
|
+
function compileEvent(expression, setters, functions, factory, nativeHandlers, handlerUrl, listItem) {
|
|
458
557
|
if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
|
|
459
558
|
if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
|
|
460
559
|
|
|
560
|
+
rejectNativeEventControls(expression)
|
|
461
561
|
const optimized = compileOptimizedEvent(expression, setters, factory)
|
|
462
562
|
if (optimized) return optimized
|
|
463
563
|
|
|
@@ -469,18 +569,45 @@ function compileEvent(expression, setters, functions, factory, nativeHandlers, h
|
|
|
469
569
|
factory.createStringLiteral(name),
|
|
470
570
|
factory.createIdentifier(name)
|
|
471
571
|
]))
|
|
472
|
-
const scope = [...captures].map(name => factory.createArrayLiteralExpression([
|
|
473
|
-
factory.createStringLiteral(name),
|
|
474
|
-
factory.createIdentifier(name)
|
|
475
|
-
]))
|
|
476
572
|
return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
|
|
477
573
|
factory.createStringLiteral(handlerUrl),
|
|
478
574
|
factory.createStringLiteral(exportName),
|
|
479
575
|
factory.createArrayLiteralExpression(states),
|
|
480
|
-
factory.createArrayLiteralExpression(
|
|
576
|
+
factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
|
|
577
|
+
factory.createStringLiteral(name),
|
|
578
|
+
name === listItem ? factory.createCallExpression(factory.createIdentifier("__kListItem"), undefined, []) : factory.createIdentifier(name)
|
|
579
|
+
])))
|
|
481
580
|
])
|
|
482
581
|
}
|
|
483
582
|
|
|
583
|
+
function rejectNativeEventControls(expression) {
|
|
584
|
+
const controls = new Set(["preventDefault", "stopPropagation", "stopImmediatePropagation"])
|
|
585
|
+
const found = new Set()
|
|
586
|
+
const eventAliases = new Set()
|
|
587
|
+
const parameter = expression.parameters[0]?.name
|
|
588
|
+
if (parameter && ts.isIdentifier(parameter)) eventAliases.add(parameter.text)
|
|
589
|
+
const visit = node => {
|
|
590
|
+
if (ts.isIdentifier(node) && controls.has(node.text)) found.add(node.text)
|
|
591
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isIdentifier(unwrapEventAlias(node.initializer)) && eventAliases.has(unwrapEventAlias(node.initializer).text)) {
|
|
592
|
+
eventAliases.add(node.name.text)
|
|
593
|
+
}
|
|
594
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(node.left) && ts.isIdentifier(unwrapEventAlias(node.right)) && eventAliases.has(unwrapEventAlias(node.right).text)) eventAliases.add(node.left.text)
|
|
595
|
+
if (ts.isElementAccessExpression(node) && ts.isIdentifier(unwrapEventAlias(node.expression)) && eventAliases.has(unwrapEventAlias(node.expression).text)) {
|
|
596
|
+
if (ts.isStringLiteral(node.argumentExpression) && controls.has(node.argumentExpression.text)) found.add(node.argumentExpression.text)
|
|
597
|
+
else if (!ts.isStringLiteral(node.argumentExpression)) for (const control of controls) found.add(control)
|
|
598
|
+
}
|
|
599
|
+
ts.forEachChild(node, visit)
|
|
600
|
+
}
|
|
601
|
+
for (const parameter of expression.parameters) visit(parameter)
|
|
602
|
+
visit(expression.body)
|
|
603
|
+
if (found.size) throw new Error(`Delegated native handlers do not support event control methods: ${[...found].sort().join(", ")}`)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function unwrapEventAlias(node) {
|
|
607
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node)) return unwrapEventAlias(node.expression)
|
|
608
|
+
return node
|
|
609
|
+
}
|
|
610
|
+
|
|
484
611
|
function nativeStateNames(expression, setters) {
|
|
485
612
|
return referencedStateNames(expression.body, setters, expression)
|
|
486
613
|
}
|
|
@@ -681,6 +808,19 @@ function printReactiveBinding({ exportName, expression, captures, states }) {
|
|
|
681
808
|
}
|
|
682
809
|
}
|
|
683
810
|
|
|
811
|
+
function printListExpression({ exportName, expression, item }) {
|
|
812
|
+
const declaration = ts.factory.createFunctionDeclaration(
|
|
813
|
+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
|
|
814
|
+
undefined,
|
|
815
|
+
exportName,
|
|
816
|
+
undefined,
|
|
817
|
+
[ts.factory.createParameterDeclaration(undefined, undefined, item)],
|
|
818
|
+
undefined,
|
|
819
|
+
ts.factory.createBlock([ts.factory.createReturnStatement(expression)], true)
|
|
820
|
+
)
|
|
821
|
+
return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
|
|
822
|
+
}
|
|
823
|
+
|
|
684
824
|
function scopeRead(factory, name) {
|
|
685
825
|
return factory.createCallExpression(
|
|
686
826
|
factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"),
|
package/framework/core.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export function binding(value: unknown, module: string, handler: string, states:
|
|
|
8
8
|
export function bindingValue(value: unknown): unknown
|
|
9
9
|
export function conditional(kind: "and" | "ternary", value: unknown, truthy: () => unknown, falsy: () => unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
|
|
10
10
|
export function list(items: unknown, keyField: string, render: (item: unknown) => unknown): unknown
|
|
11
|
+
export function listField(read: () => unknown, field: string): unknown
|
|
12
|
+
export function listExpression(read: () => unknown, module: string, handler: string): unknown
|
|
13
|
+
export function listItem(): unknown
|
|
11
14
|
|
|
12
15
|
export function renderPage(
|
|
13
16
|
component: (props: Record<string, never>) => unknown | Promise<unknown>,
|
package/framework/core.mjs
CHANGED
|
@@ -5,6 +5,8 @@ const bindingMarker = Symbol("kudzu.binding")
|
|
|
5
5
|
const conditionalMarker = Symbol("kudzu.conditional")
|
|
6
6
|
const listMarker = Symbol("kudzu.list")
|
|
7
7
|
const listFieldMarker = Symbol("kudzu.listField")
|
|
8
|
+
const listExpressionMarker = Symbol("kudzu.listExpression")
|
|
9
|
+
const listItemMarker = Symbol("kudzu.listItem")
|
|
8
10
|
const noSelectValue = Symbol("kudzu.no-select-value")
|
|
9
11
|
|
|
10
12
|
let renderContext
|
|
@@ -79,13 +81,27 @@ export function list(items, keyField, render) {
|
|
|
79
81
|
return { [listMarker]: true, items, keyField, render }
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
export function listField(read, field) {
|
|
85
|
+
return { [listFieldMarker]: true, field, value: renderContext?.listTemplate ? undefined : read() }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function listExpression(read, module, handler) {
|
|
89
|
+
const value = renderContext?.listTemplate ? undefined : read()
|
|
90
|
+
if (value && typeof value.then === "function") throw new Error("Derived keyed list item expressions must return synchronous values")
|
|
91
|
+
return { [listExpressionMarker]: true, module, handler, value }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function listItem() {
|
|
95
|
+
return { [listItemMarker]: true }
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
function validListKey(key) {
|
|
83
99
|
return typeof key === "string" || typeof key === "number" && Number.isFinite(key)
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
function assertListItem(item) {
|
|
87
103
|
const prototype = item && typeof item === "object" ? Object.getPrototypeOf(item) : undefined
|
|
88
|
-
if (!item || Array.isArray(item) || prototype !== Object.prototype
|
|
104
|
+
if (!item || Array.isArray(item) || prototype !== Object.prototype) throw new Error("Keyed list items must be ordinary plain objects")
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
function assertListValue(value, seen) {
|
|
@@ -93,7 +109,7 @@ function assertListValue(value, seen) {
|
|
|
93
109
|
if (!value || typeof value !== "object") throw new Error(`Keyed list items must contain only JSON-safe values`)
|
|
94
110
|
if (seen.has(value)) throw new Error("Keyed list items must not contain cycles")
|
|
95
111
|
const prototype = Object.getPrototypeOf(value)
|
|
96
|
-
if (!Array.isArray(value) && prototype !== Object.prototype
|
|
112
|
+
if (!Array.isArray(value) && prototype !== Object.prototype) throw new Error("Keyed list items must contain only arrays and ordinary plain objects")
|
|
97
113
|
if (Object.getOwnPropertySymbols(value).length) throw new Error("Keyed list items must not contain symbols")
|
|
98
114
|
seen.add(value)
|
|
99
115
|
const descriptors = Object.getOwnPropertyDescriptors(value)
|
|
@@ -139,6 +155,7 @@ function bindingDescriptor(value) {
|
|
|
139
155
|
}
|
|
140
156
|
|
|
141
157
|
function serializeCapture(name, value, seen) {
|
|
158
|
+
if (value?.[listItemMarker]) return { type: "list-item" }
|
|
142
159
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value
|
|
143
160
|
if (typeof value === "number") {
|
|
144
161
|
return Number.isFinite(value) && !Object.is(value, -0) ? value : { type: "number", value: String(value) }
|
|
@@ -170,7 +187,7 @@ function serializeCapture(name, value, seen) {
|
|
|
170
187
|
}
|
|
171
188
|
|
|
172
189
|
export async function renderPage(component, metadata = {}) {
|
|
173
|
-
renderContext = { nextState: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false }
|
|
190
|
+
renderContext = { nextState: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false }
|
|
174
191
|
|
|
175
192
|
try {
|
|
176
193
|
const body = await renderNode({ type: component, props: {} })
|
|
@@ -290,6 +307,10 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
290
307
|
if (node?.[listFieldMarker]) {
|
|
291
308
|
return `<template data-k-list-text="${escapeAttribute(node.field)}"></template>${escapeHtml(node.value ?? "")}<template data-k-list-text-end></template>`
|
|
292
309
|
}
|
|
310
|
+
if (node?.[listExpressionMarker]) {
|
|
311
|
+
const descriptor = { module: node.module, handler: node.handler }
|
|
312
|
+
return `<template data-k-list-expression='${escapeJsonAttribute(descriptor)}'></template>${escapeHtml(node.value ?? "")}<template data-k-list-expression-end></template>`
|
|
313
|
+
}
|
|
293
314
|
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
294
315
|
throw new Error(`Cannot render ${String(node)}`)
|
|
295
316
|
}
|
|
@@ -308,6 +329,8 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
308
329
|
let attributes = ""
|
|
309
330
|
const attributeBindings = []
|
|
310
331
|
const listAttributes = []
|
|
332
|
+
const listExpressionAttributes = []
|
|
333
|
+
const listEvents = []
|
|
311
334
|
|
|
312
335
|
if (renderContext.listRoot) {
|
|
313
336
|
const root = renderContext.listRoot
|
|
@@ -327,15 +350,17 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
327
350
|
}
|
|
328
351
|
|
|
329
352
|
if (/^on[A-Z]/.test(rawName)) {
|
|
330
|
-
|
|
353
|
+
const event = rawName.slice(2).toLowerCase()
|
|
331
354
|
if (value?.[behaviorMarker]) {
|
|
332
355
|
const commands = JSON.stringify(value.commands)
|
|
333
356
|
attributes += ` data-k-on-${event}='${escapeJsonAttribute(value.commands)}'`
|
|
334
357
|
renderContext.events.push({ event, commands: value.commands })
|
|
335
358
|
} else if (value?.[nativeBehaviorMarker]) {
|
|
336
|
-
const
|
|
359
|
+
const template = { module: value.module, handler: value.handler, states: value.states, scope: value.scope }
|
|
360
|
+
const native = template
|
|
337
361
|
attributes += ` data-k-native-${event}='${escapeJsonAttribute(native)}'`
|
|
338
362
|
renderContext.events.push({ event, native })
|
|
363
|
+
if (renderContext.listDepth && Object.values(template.scope).some(entry => entry?.type === "list-item")) listEvents.push([event, template])
|
|
339
364
|
renderContext.hasNativeBehaviors = true
|
|
340
365
|
} else {
|
|
341
366
|
throw new Error(`${rawName} must reference a compilable event handler`)
|
|
@@ -351,6 +376,11 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
351
376
|
listAttributes.push([name, value.field])
|
|
352
377
|
continue
|
|
353
378
|
}
|
|
379
|
+
if (value?.[listExpressionMarker]) {
|
|
380
|
+
attributes += renderAttribute(name, value.value)
|
|
381
|
+
listExpressionAttributes.push([name, value.module, value.handler])
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
354
384
|
if (value?.[signalMarker] || value?.[bindingMarker]) {
|
|
355
385
|
const initialValue = value[signalMarker] ? value.value : value.value
|
|
356
386
|
const reactive = value[signalMarker] || Object.keys(value.states).length > 0 || Object.keys(value.scopeStates).length > 0 || Object.keys(value.scopeBindings).length > 0
|
|
@@ -382,6 +412,8 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
382
412
|
|
|
383
413
|
if (attributeBindings.length) attributes += ` data-k-bind-attrs='${escapeJsonAttribute(attributeBindings)}'`
|
|
384
414
|
if (listAttributes.length) attributes += ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
|
|
415
|
+
if (listExpressionAttributes.length) attributes += ` data-k-list-expression-attrs='${escapeJsonAttribute(listExpressionAttributes)}'`
|
|
416
|
+
if (listEvents.length) attributes += ` data-k-list-events='${escapeJsonAttribute(listEvents)}'`
|
|
385
417
|
|
|
386
418
|
if (tag === "option" && selectValue !== noSelectValue && String(optionValue(props)) === (selectValue == null ? "" : String(selectValue))) attributes += " selected"
|
|
387
419
|
|
|
@@ -394,17 +426,16 @@ async function renderList(node, namespace, selectValue) {
|
|
|
394
426
|
if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
|
|
395
427
|
const id = `l${renderContext.nextList++}`
|
|
396
428
|
const descriptor = { id, state: node.items.id, key: node.keyField }
|
|
397
|
-
const itemProxy = value => new Proxy({}, {
|
|
398
|
-
get: (_, field) => ({ [listFieldMarker]: true, field: String(field), value: value?.[field] })
|
|
399
|
-
})
|
|
400
429
|
renderContext.listDepth++
|
|
401
430
|
try {
|
|
431
|
+
renderContext.listTemplate = true
|
|
402
432
|
renderContext.listRoot = { id, template: true }
|
|
403
|
-
const template = await renderNode(node.render(
|
|
433
|
+
const template = await renderNode(node.render({}), namespace, selectValue)
|
|
404
434
|
let current = ""
|
|
435
|
+
renderContext.listTemplate = false
|
|
405
436
|
for (const item of node.items.value) {
|
|
406
437
|
renderContext.listRoot = { id, key: item[node.keyField], template: false }
|
|
407
|
-
current += await renderNode(node.render(
|
|
438
|
+
current += await renderNode(node.render(item), namespace, selectValue)
|
|
408
439
|
}
|
|
409
440
|
renderContext.lists.push(descriptor)
|
|
410
441
|
renderContext.hasBehaviors = true
|
|
@@ -412,6 +443,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
412
443
|
return `<template data-k-list='${escapeJsonAttribute(descriptor)}'>${template}</template>${current}<template data-k-list-end="${id}"></template>`
|
|
413
444
|
} finally {
|
|
414
445
|
renderContext.listRoot = undefined
|
|
446
|
+
renderContext.listTemplate = false
|
|
415
447
|
renderContext.listDepth--
|
|
416
448
|
}
|
|
417
449
|
}
|
|
@@ -3,6 +3,8 @@ import { browserState, mountDom, registerCommitter, registerMountHook, registerU
|
|
|
3
3
|
const listTargets = new Map()
|
|
4
4
|
const listRegistrations = new WeakMap()
|
|
5
5
|
const mountedLists = new WeakSet()
|
|
6
|
+
const imports = new Map()
|
|
7
|
+
const revisions = new WeakMap()
|
|
6
8
|
|
|
7
9
|
function commitLists(id) {
|
|
8
10
|
const lists = listTargets.get(id)
|
|
@@ -97,20 +99,62 @@ function updateList(list) {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
function fillListItem(root, item) {
|
|
102
|
+
const revision = (revisions.get(root) ?? 0) + 1
|
|
103
|
+
revisions.set(root, revision)
|
|
100
104
|
for (const marker of matching(root, "template[data-k-list-text]")) {
|
|
101
|
-
|
|
102
|
-
let end = marker.nextSibling
|
|
103
|
-
while (end && !(end.nodeType === Node.ELEMENT_NODE && end.matches("template[data-k-list-text-end]"))) end = end.nextSibling
|
|
104
|
-
if (!end) throw new Error("Keyed list text marker has no end")
|
|
105
|
-
const range = marker.ownerDocument.createRange()
|
|
106
|
-
range.setStartAfter(marker)
|
|
107
|
-
range.setEndBefore(end)
|
|
108
|
-
range.deleteContents()
|
|
109
|
-
end.before(marker.ownerDocument.createTextNode(value == null ? "" : String(value)))
|
|
105
|
+
patchListText(marker, "template[data-k-list-text-end]", item?.[marker.dataset.kListText])
|
|
110
106
|
}
|
|
111
107
|
for (const node of matching(root, "[data-k-list-attrs]")) {
|
|
112
108
|
for (const [target, field] of JSON.parse(node.dataset.kListAttrs)) patchBinding(node, target, item?.[field])
|
|
113
109
|
}
|
|
110
|
+
for (const node of matching(root, "[data-k-list-events]")) {
|
|
111
|
+
for (const [event, native] of JSON.parse(node.dataset.kListEvents)) {
|
|
112
|
+
native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value]))
|
|
113
|
+
node.dataset[`kNative${capitalize(event)}`] = JSON.stringify(native)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const marker of matching(root, "template[data-k-list-expression]")) {
|
|
117
|
+
evaluate(JSON.parse(marker.dataset.kListExpression), item).then(value => {
|
|
118
|
+
if (revisions.get(root) === revision && root.isConnected) patchListText(marker, "template[data-k-list-expression-end]", value)
|
|
119
|
+
}).catch(error => console.error(error))
|
|
120
|
+
}
|
|
121
|
+
for (const node of matching(root, "[data-k-list-expression-attrs]")) {
|
|
122
|
+
for (const [target, module, handler] of JSON.parse(node.dataset.kListExpressionAttrs)) {
|
|
123
|
+
evaluate({ module, handler }, item).then(value => {
|
|
124
|
+
if (revisions.get(root) === revision && root.isConnected) patchBinding(node, target, value)
|
|
125
|
+
}).catch(error => console.error(error))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function patchListText(marker, endSelector, value) {
|
|
131
|
+
let end = marker.nextSibling
|
|
132
|
+
while (end && !(end.nodeType === Node.ELEMENT_NODE && end.matches(endSelector))) end = end.nextSibling
|
|
133
|
+
if (!end) throw new Error("Keyed list text marker has no end")
|
|
134
|
+
const range = marker.ownerDocument.createRange()
|
|
135
|
+
range.setStartAfter(marker)
|
|
136
|
+
range.setEndBefore(end)
|
|
137
|
+
range.deleteContents()
|
|
138
|
+
end.before(marker.ownerDocument.createTextNode(value == null ? "" : String(value)))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function evaluate(descriptor, item) {
|
|
142
|
+
let module = imports.get(descriptor.module)
|
|
143
|
+
if (!module) {
|
|
144
|
+
module = import(descriptor.module)
|
|
145
|
+
imports.set(descriptor.module, module)
|
|
146
|
+
}
|
|
147
|
+
return module.then(exports => {
|
|
148
|
+
const value = exports[descriptor.handler](item)
|
|
149
|
+
if (value && typeof value.then === "function") throw new Error("Derived keyed list item expressions must return synchronous values")
|
|
150
|
+
return value
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function serializeItem(value) {
|
|
155
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") return value
|
|
156
|
+
if (Array.isArray(value)) return { type: "array", value: value.map(serializeItem) }
|
|
157
|
+
return { type: "object", nullPrototype: false, value: Object.entries(value).map(([key, entry]) => [key, serializeItem(entry)]) }
|
|
114
158
|
}
|
|
115
159
|
|
|
116
160
|
function patchBinding(node, target, value) {
|
|
@@ -142,7 +186,7 @@ function validListKey(key) {
|
|
|
142
186
|
|
|
143
187
|
function assertListItem(item) {
|
|
144
188
|
const prototype = item && typeof item === "object" ? Object.getPrototypeOf(item) : undefined
|
|
145
|
-
if (!item || Array.isArray(item) || prototype !== Object.prototype
|
|
189
|
+
if (!item || Array.isArray(item) || prototype !== Object.prototype) throw new Error("Keyed list items must be ordinary plain objects")
|
|
146
190
|
}
|
|
147
191
|
|
|
148
192
|
function assertListValue(value, seen) {
|
|
@@ -150,7 +194,7 @@ function assertListValue(value, seen) {
|
|
|
150
194
|
if (!value || typeof value !== "object") throw new Error("Keyed list items must contain only JSON-safe values")
|
|
151
195
|
if (seen.has(value)) throw new Error("Keyed list items must not contain cycles")
|
|
152
196
|
const prototype = Object.getPrototypeOf(value)
|
|
153
|
-
if (!Array.isArray(value) && prototype !== Object.prototype
|
|
197
|
+
if (!Array.isArray(value) && prototype !== Object.prototype) throw new Error("Keyed list items must contain only arrays and ordinary plain objects")
|
|
154
198
|
if (Object.getOwnPropertySymbols(value).length) throw new Error("Keyed list items must not contain symbols")
|
|
155
199
|
seen.add(value)
|
|
156
200
|
const descriptors = Object.getOwnPropertyDescriptors(value)
|
|
@@ -183,3 +227,7 @@ function matching(root, selector) {
|
|
|
183
227
|
function isStringBooleanAttribute(name) {
|
|
184
228
|
return name.startsWith("aria-") || name.startsWith("data-")
|
|
185
229
|
}
|
|
230
|
+
|
|
231
|
+
function capitalize(value) {
|
|
232
|
+
return value[0].toUpperCase() + value.slice(1)
|
|
233
|
+
}
|
|
@@ -39,19 +39,38 @@ if (typeof document !== "undefined") {
|
|
|
39
39
|
const eventNames = ["click", "input", "change", "submit", "keydown", "keyup"]
|
|
40
40
|
for (const eventName of eventNames) {
|
|
41
41
|
document.addEventListener(eventName, event => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let modulePromise = modules.get(native.module)
|
|
47
|
-
if (!modulePromise) {
|
|
48
|
-
modulePromise = import(native.module)
|
|
49
|
-
modules.set(native.module, modulePromise)
|
|
42
|
+
try {
|
|
43
|
+
dispatchNative(event, snapshotNativeTargets(event, eventName), modules).catch(error => console.error(error))
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(error)
|
|
50
46
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function snapshotNativeTargets(event, eventName) {
|
|
52
|
+
const selector = `[data-k-native-${eventName}]`
|
|
53
|
+
const targets = []
|
|
54
|
+
for (let target = event.target.closest(selector); target; target = target.parentElement?.closest(selector)) {
|
|
55
|
+
targets.push({ target, native: JSON.parse(target.dataset[`kNative${capitalize(eventName)}`]) })
|
|
56
|
+
}
|
|
57
|
+
return targets
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function dispatchNative(event, targets, modules) {
|
|
61
|
+
for (const { target, native } of targets) {
|
|
62
|
+
let modulePromise = modules.get(native.module)
|
|
63
|
+
if (!modulePromise) {
|
|
64
|
+
modulePromise = import(native.module)
|
|
65
|
+
modules.set(native.module, modulePromise)
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const module = await modulePromise
|
|
69
|
+
const result = module[native.handler](createNativeContext(browserState, native.states, commitDom, native.scope), delegatedEvent(event, target))
|
|
70
|
+
if (result && typeof result.then === "function") result.catch(error => console.error(error))
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(error)
|
|
73
|
+
}
|
|
55
74
|
}
|
|
56
75
|
}
|
|
57
76
|
|