@kudzujs/core 0.3.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.
@@ -31,6 +31,7 @@ export async function build({ quiet = false } = {}) {
31
31
 
32
32
  let behaviorCount = 0
33
33
  let bindingCount = 0
34
+ let listCount = 0
34
35
  let stateSeedCount = 0
35
36
  const plans = []
36
37
  const hasStyles = await exists(join(sourceDirectory, "style.css"))
@@ -51,6 +52,7 @@ export async function build({ quiet = false } = {}) {
51
52
  plans.push({ route: `/${route}`, ...result.plan })
52
53
  if (result.hasBehaviors) behaviorCount++
53
54
  if (result.hasBindings) bindingCount++
55
+ if (result.hasLists) listCount++
54
56
  if (result.hasStateSeed) stateSeedCount++
55
57
  }
56
58
 
@@ -59,7 +61,7 @@ export async function build({ quiet = false } = {}) {
59
61
  const commandEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.commands).map(event => event.event)))].sort()
60
62
  const nativeEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.native).map(event => event.event)))].sort()
61
63
  if (behaviorCount) {
62
- const runtimeFile = bindingCount ? "./shared-runtime.js" : "./runtime.js"
64
+ const runtimeFile = bindingCount || listCount ? "./shared-runtime.js" : "./runtime.js"
63
65
  const runtime = specializeRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), commandEvents, stateSeedCount > 0)
64
66
  await writeFile(join(assetsDirectory, "kudzu.js"), runtime)
65
67
  }
@@ -71,6 +73,11 @@ export async function build({ quiet = false } = {}) {
71
73
  .replace('"./serialization.js"', '"./kudzu-serialization.js"')
72
74
  await writeFile(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime)
73
75
  }
76
+ if (listCount) {
77
+ const listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
78
+ .replace('"./shared-runtime.js"', '"./kudzu.js"')
79
+ await writeFile(join(assetsDirectory, "kudzu-list.js"), listRuntime)
80
+ }
74
81
  if (hasNativeHandlers) {
75
82
  const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
76
83
  .replace('"./runtime.js"', '"./kudzu.js"')
@@ -143,6 +150,7 @@ async function compile(file) {
143
150
  const source = await readFile(file, "utf8")
144
151
  const nativeHandlers = []
145
152
  const reactiveBindings = []
153
+ const listExpressions = []
146
154
  const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
147
155
  const result = ts.transpileModule(source, {
148
156
  fileName: file,
@@ -152,7 +160,7 @@ async function compile(file) {
152
160
  jsx: ts.JsxEmit.ReactJSX,
153
161
  jsxImportSource: "@kudzujs/core"
154
162
  },
155
- transformers: { before: [createKudzuTransformer(nativeHandlers, reactiveBindings, `/assets/${handlerPath}`)] },
163
+ transformers: { before: [createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, `/assets/${handlerPath}`)] },
156
164
  reportDiagnostics: true
157
165
  })
158
166
 
@@ -165,10 +173,11 @@ async function compile(file) {
165
173
  await mkdir(resolve(output, ".."), { recursive: true })
166
174
  await writeFile(output, result.outputText)
167
175
 
168
- if (!nativeHandlers.length && !reactiveBindings.length) return undefined
176
+ if (!nativeHandlers.length && !reactiveBindings.length && !listExpressions.length) return undefined
169
177
  const moduleSource = [
170
178
  ...nativeHandlers.map(handler => printNativeHandler(handler)),
171
- ...reactiveBindings.map(entry => printReactiveBinding(entry))
179
+ ...reactiveBindings.map(entry => printReactiveBinding(entry)),
180
+ ...listExpressions.map(entry => printListExpression(entry))
172
181
  ].join("\n")
173
182
  const moduleResult = ts.transpileModule(moduleSource, {
174
183
  compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
@@ -179,14 +188,17 @@ async function compile(file) {
179
188
  return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0 }
180
189
  }
181
190
 
182
- function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
191
+ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, handlerUrl) {
183
192
  return context => sourceFile => {
184
193
  const factory = context.factory
185
194
  const settersByFunction = new Map()
186
195
  const functions = new Map()
196
+ const listValues = new WeakMap()
197
+ const listEventItems = new WeakMap()
187
198
  let usesBehavior = false
188
199
  let usesBinding = false
189
200
  let usesConditional = false
201
+ let usesList = false
190
202
 
191
203
  const collect = node => {
192
204
  if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer)) {
@@ -228,7 +240,27 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
228
240
  return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
229
241
  }
230
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
+
231
251
  if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
252
+ const listParts = keyedListParts(node.expression, settersForNode(node, settersByFunction))
253
+ if (listParts) {
254
+ if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
255
+ validateKeyedList(listParts, sourceFile, settersForNode(node, settersByFunction), listValues, listEventItems)
256
+ usesBehavior = true
257
+ usesList = true
258
+ return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, [
259
+ listParts.state,
260
+ factory.createStringLiteral(listParts.keyField),
261
+ ts.visitNode(listParts.callback, visitor)
262
+ ]))
263
+ }
232
264
  const parts = conditionalParts(node.expression)
233
265
  if (parts) {
234
266
  const setters = settersForNode(node, settersByFunction)
@@ -245,7 +277,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
245
277
  }
246
278
  }
247
279
 
248
- if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && ["className", "disabled", "value"].includes(node.name.getText())) {
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())) {
249
281
  const expression = node.initializer.expression
250
282
  const setters = settersForNode(node, settersByFunction)
251
283
  const usedStates = referencedStateNames(expression, setters)
@@ -260,7 +292,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
260
292
 
261
293
  if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.getText())) {
262
294
  const setters = settersForNode(node, settersByFunction)
263
- 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))
264
296
  if (event) {
265
297
  usesBehavior = true
266
298
  return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, event))
@@ -279,6 +311,12 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
279
311
  if (nativeHandlers.length) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
280
312
  if (usesBinding) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
281
313
  if (usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("conditional"), factory.createIdentifier("__kConditional")))
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
+ }
282
320
  if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
283
321
  const behaviorImport = factory.createImportDeclaration(
284
322
  undefined,
@@ -289,6 +327,168 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, handlerUrl) {
289
327
  }
290
328
  }
291
329
 
330
+ function keyedListParts(expression, setters) {
331
+ const value = unwrapExpression(expression)
332
+ if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map" || !ts.isIdentifier(value.expression.expression)) return undefined
333
+ const state = value.expression.expression
334
+ if (![...setters.values()].includes(state.text)) return undefined
335
+ const callback = value.arguments[0]
336
+ if (!ts.isArrowFunction(callback) || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name)) {
337
+ throw new Error("Keyed list map callback must be an arrow function with one identifier parameter")
338
+ }
339
+ const root = unwrapExpression(callback.body)
340
+ if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) throw new Error("Keyed list map callback must return one JSX element")
341
+ const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
342
+ const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && attribute.name.getText() === "key")
343
+ const field = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression && directProperty(key.initializer.expression, callback.parameters[0].name.text)
344
+ if (!field) throw new Error(`Keyed list root must have key={${callback.parameters[0].name.text}.<field>}`)
345
+ return { state, callback, root, item: callback.parameters[0].name.text, keyField: field }
346
+ }
347
+
348
+ function validateKeyedList(parts, sourceFile, setters, listValues, listEventItems) {
349
+ const fail = (node, message) => {
350
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
351
+ throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
352
+ }
353
+ const validateElement = node => {
354
+ const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
355
+ if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
356
+ }
357
+ const visit = node => {
358
+ if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
359
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
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")
361
+ if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, parts.item)) fail(node, "Keyed list item spreads are not supported")
362
+ if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.getText())) {
363
+ listEventItems.set(node, parts.item)
364
+ return
365
+ }
366
+ if (ts.isJsxExpression(node) && node.expression) {
367
+ const expression = unwrapExpression(node.expression)
368
+ if (conditionalParts(expression) && containsJsx(expression)) fail(node, "Nested reactive conditions are not supported in keyed lists")
369
+ const field = directProperty(expression, parts.item)
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`)
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`)
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 })
382
+ return
383
+ }
384
+ }
385
+ ts.forEachChild(node, visit)
386
+ }
387
+ visit(parts.root)
388
+ }
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
+
464
+ function directProperty(expression, objectName) {
465
+ const value = unwrapExpression(expression)
466
+ if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
467
+ if (objectName !== undefined && value.expression.text !== objectName) return undefined
468
+ return value.name.text
469
+ }
470
+
471
+ function keyedListParentTag(node) {
472
+ for (let current = node.parent; current; current = current.parent) {
473
+ if (ts.isJsxElement(current)) return current.openingElement.tagName.getText().toLowerCase()
474
+ }
475
+ return undefined
476
+ }
477
+
478
+ function referencesIdentifier(root, name) {
479
+ let found = false
480
+ const visit = node => {
481
+ if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) found = true
482
+ if (!found) ts.forEachChild(node, visit)
483
+ }
484
+ visit(root)
485
+ return found
486
+ }
487
+
488
+ function unwrapExpression(node) {
489
+ return ts.isParenthesizedExpression(node) ? unwrapExpression(node.expression) : node
490
+ }
491
+
292
492
  function compileReactiveBinding(expression, setters, factory, context, reactiveBindings, handlerUrl) {
293
493
  return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, factory, context, reactiveBindings, handlerUrl))
294
494
  }
@@ -353,10 +553,11 @@ function factoryNull() {
353
553
  return ts.factory.createNull()
354
554
  }
355
555
 
356
- function compileEvent(expression, setters, functions, factory, nativeHandlers, handlerUrl) {
556
+ function compileEvent(expression, setters, functions, factory, nativeHandlers, handlerUrl, listItem) {
357
557
  if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
358
558
  if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
359
559
 
560
+ rejectNativeEventControls(expression)
360
561
  const optimized = compileOptimizedEvent(expression, setters, factory)
361
562
  if (optimized) return optimized
362
563
 
@@ -368,18 +569,45 @@ function compileEvent(expression, setters, functions, factory, nativeHandlers, h
368
569
  factory.createStringLiteral(name),
369
570
  factory.createIdentifier(name)
370
571
  ]))
371
- const scope = [...captures].map(name => factory.createArrayLiteralExpression([
372
- factory.createStringLiteral(name),
373
- factory.createIdentifier(name)
374
- ]))
375
572
  return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
376
573
  factory.createStringLiteral(handlerUrl),
377
574
  factory.createStringLiteral(exportName),
378
575
  factory.createArrayLiteralExpression(states),
379
- factory.createArrayLiteralExpression(scope)
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
+ ])))
380
580
  ])
381
581
  }
382
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
+
383
611
  function nativeStateNames(expression, setters) {
384
612
  return referencedStateNames(expression.body, setters, expression)
385
613
  }
@@ -580,6 +808,19 @@ function printReactiveBinding({ exportName, expression, captures, states }) {
580
808
  }
581
809
  }
582
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
+
583
824
  function scopeRead(factory, name) {
584
825
  return factory.createCallExpression(
585
826
  factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"),
@@ -7,6 +7,10 @@ export function nativeBehavior(module: string, handler: string, states: Array<[s
7
7
  export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
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
+ 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
10
14
 
11
15
  export function renderPage(
12
16
  component: (props: Record<string, never>) => unknown | Promise<unknown>,
@@ -32,6 +36,7 @@ export function renderPage(
32
36
  html: string
33
37
  hasBehaviors: boolean
34
38
  hasBindings: boolean
39
+ hasLists: boolean
35
40
  hasStateSeed: boolean
36
41
  plan: {
37
42
  states: Array<{ id: string; name: string; initialValue: unknown }>
@@ -41,7 +46,7 @@ export function renderPage(
41
46
  native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
42
47
  }>
43
48
  bindings: Array<{
44
- target: "class" | "disabled" | "value"
49
+ target: string
45
50
  state?: string
46
51
  module?: string
47
52
  handler?: string
@@ -51,5 +56,6 @@ export function renderPage(
51
56
  scopeBindings?: Record<string, unknown>
52
57
  }>
53
58
  conditions: Array<Record<string, unknown>>
59
+ lists: Array<Record<string, unknown>>
54
60
  }
55
61
  }>