@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.
- package/README.md +56 -12
- package/framework/README.md +4 -3
- package/framework/binding-runtime.js +41 -22
- package/framework/build.mjs +254 -13
- package/framework/core.d.ts +7 -1
- package/framework/core.mjs +182 -30
- package/framework/list-runtime.js +233 -0
- package/framework/native-runtime.js +31 -12
- package/framework/shared-runtime.js +19 -0
- package/package.json +1 -1
package/framework/build.mjs
CHANGED
|
@@ -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 && ["
|
|
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(
|
|
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"),
|
package/framework/core.d.ts
CHANGED
|
@@ -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:
|
|
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
|
}>
|