@kudzujs/core 0.8.15 → 0.8.16

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.
@@ -0,0 +1,187 @@
1
+ import ts from "typescript"
2
+ import { nearestFunction, unwrapExpression } from "./ast-helpers.mjs"
3
+
4
+ export const pureCollectionMethods = new Set(["at", "charAt", "charCodeAt", "concat", "endsWith", "includes", "indexOf", "join", "lastIndexOf", "localeCompare", "padEnd", "padStart", "repeat", "replace", "replaceAll", "slice", "startsWith", "substring", "toLowerCase", "toUpperCase", "trim", "trimEnd", "trimStart"])
5
+ export const mutatingCollectionMethods = new Set(["copyWithin", "fill", "pop", "push", "reverse", "shift", "sort", "splice", "unshift"])
6
+ export const pureCollectionMathMethods = new Set(["abs", "ceil", "floor", "max", "min", "pow", "round", "sign", "sqrt", "trunc"])
7
+
8
+ export function isArrayFromCall(value) {
9
+ return ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression) && ts.isIdentifier(value.expression.expression) && value.expression.expression.text === "Array" && value.expression.name.text === "from"
10
+ }
11
+
12
+ export function collectionParameters(callback, label, fail) {
13
+ if (!ts.isArrowFunction(callback) || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || callback.parameters.length < 1 || callback.parameters.length > 2 || callback.parameters.some(parameter => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken)) fail(callback, `${label} callback must be a synchronous arrow function with (item) or (item, index) identifier parameters`)
14
+ return { item: callback.parameters[0].name.text, index: callback.parameters[1]?.name.text }
15
+ }
16
+
17
+ export function analyzeCollectionPipeline(expression, options) {
18
+ const {
19
+ setters = new Map(),
20
+ declarations,
21
+ fail,
22
+ aliases = new Set(),
23
+ importedCollections = new Set(),
24
+ stateNames = new Set(),
25
+ importedCollectionTransforms = new Map(),
26
+ calculatedCollection,
27
+ staticCollection
28
+ } = options
29
+ const nestedOptions = { setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, calculatedCollection, staticCollection }
30
+ const value = unwrapExpression(expression)
31
+ if (ts.isIdentifier(value)) {
32
+ if ([...setters.values()].includes(value.text)) {
33
+ const localStatic = staticCollection?.(value.text)
34
+ return { state: value, static: localStatic, localStatic, selector: [], selectorStates: new Set() }
35
+ }
36
+ if (importedCollections.has(value.text)) return { state: value, static: true, selector: [], selectorStates: new Set() }
37
+ const entries = declarations?.get(value.text)
38
+ if (!entries) return undefined
39
+ if (entries.length !== 1 || aliases.has(value.text) || entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value, `Rendered collection alias "${value.text}" must be one top-level immutable local`)
40
+ aliases.add(value.text)
41
+ const source = analyzeCollectionPipeline(entries[0].initializer, nestedOptions)
42
+ aliases.delete(value.text)
43
+ return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node], aliasUses: [...(source.aliasUses ?? []), value] }
44
+ }
45
+ if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) {
46
+ const calculation = calculatedCollection?.(value)
47
+ if (calculation) return { calculation, selector: [], selectorStates: new Set() }
48
+ return { state: undefined, ownerField: value.name.text, selector: [], parentItem: value.expression.text }
49
+ }
50
+ if (ts.isCallExpression(value) && ts.isIdentifier(value.expression) && importedCollectionTransforms.has(value.expression.text)) {
51
+ const transform = importedCollectionTransforms.get(value.expression.text)
52
+ const parameter = transform.parameters[0]
53
+ if (value.arguments.length !== 1 || transform.parameters.length !== 1 || transform.asteriskToken || transform.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !parameter || !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken) fail(value, `Imported collection transform "${value.expression.text}" must be synchronous with exactly one identifier parameter and one argument`)
54
+ const returned = ts.isBlock(transform.body)
55
+ ? transform.body.statements.length === 1 && ts.isReturnStatement(transform.body.statements[0]) ? transform.body.statements[0].expression : undefined
56
+ : transform.body
57
+ if (!returned) fail(value, `Imported collection transform "${value.expression.text}" must contain only one returned collection expression`)
58
+ const transformSource = analyzeCollectionPipeline(returned, { setters: new Map([[parameter.name.text, parameter.name.text]]), fail, stateNames: new Set([parameter.name.text]) })
59
+ if (!transformSource?.state || transformSource.state.text !== parameter.name.text || transformSource.selectorStates.size) fail(value, `Imported collection transform "${value.expression.text}" must return a supported pure pipeline rooted only in its parameter`)
60
+ const source = analyzeCollectionPipeline(value.arguments[0], nestedOptions)
61
+ if (!source) fail(value.arguments[0], `Imported collection transform "${value.expression.text}" requires a supported collection argument`)
62
+ return { ...source, selector: [...source.selector, ...transformSource.selector] }
63
+ }
64
+ if (ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression)) {
65
+ const method = value.expression.name.text
66
+ if (method === "filter") {
67
+ if (value.arguments.length !== 1) fail(value, "Rendered collection filter() requires one inline predicate")
68
+ const source = analyzeCollectionPipeline(value.expression.expression, nestedOptions)
69
+ if (!source) return undefined
70
+ const parameters = collectionParameters(value.arguments[0], "Rendered collection filter()", fail)
71
+ const selectorStates = new Set(source.selectorStates)
72
+ return { ...source, selector: [...source.selector, ["filter", collectionExpression(unwrapExpression(value.arguments[0].body), { parameters, fail, stateNames, selectorStates })]], selectorStates }
73
+ }
74
+ if (method === "flatMap") {
75
+ if (value.arguments.length !== 1) fail(value, "Rendered collection flatMap() requires one inline projector")
76
+ const source = analyzeCollectionPipeline(value.expression.expression, nestedOptions)
77
+ if (!source) return undefined
78
+ const parameters = collectionParameters(value.arguments[0], "Rendered collection flatMap()", fail)
79
+ const field = directProperty(value.arguments[0].body, parameters.item)
80
+ if (!field) fail(value.arguments[0].body, `Rendered collection flatMap() projector must be ${parameters.item}.<field>`)
81
+ if (["__proto__", "constructor", "prototype"].includes(field)) fail(value.arguments[0].body, `Rendered collection property "${field}" is not supported`)
82
+ return { ...source, selector: [...source.selector, ["flatMap", field]] }
83
+ }
84
+ if (method === "slice") {
85
+ if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered collection slice() requires a start and optional end")
86
+ const source = analyzeCollectionPipeline(value.expression.expression, nestedOptions)
87
+ if (!source) return undefined
88
+ const selectorStates = new Set(source.selectorStates)
89
+ const start = collectionExpression(value.arguments[0], { fail, stateNames, selectorStates })
90
+ const end = value.arguments[1] && collectionExpression(value.arguments[1], { fail, stateNames, selectorStates })
91
+ return { ...source, selector: [...source.selector, ["slice", start, end]], selectorStates }
92
+ }
93
+ if (method === "toSorted") {
94
+ if (value.arguments.length !== 1) fail(value, "Rendered collection toSorted() requires one inline comparator")
95
+ const source = analyzeCollectionPipeline(value.expression.expression, nestedOptions)
96
+ if (!source) return undefined
97
+ const comparator = value.arguments[0]
98
+ const parameters = collectionParameters(comparator, "Rendered collection toSorted()", fail)
99
+ if (comparator.parameters.length !== 2 || ts.isBlock(comparator.body)) fail(comparator, "Rendered collection toSorted() comparator must be a synchronous expression arrow with (left, right) identifier parameters")
100
+ const selectorStates = new Set(source.selectorStates)
101
+ const encoded = collectionExpression(comparator.body, { parameters, fail, stateNames, selectorStates })
102
+ return { ...source, selector: [...source.selector, ["sort", encoded]], selectorStates }
103
+ }
104
+ if (method === "sort") fail(value, "Rendered collections cannot use mutating sort(); use toSorted()")
105
+ }
106
+ if (isArrayFromCall(value)) {
107
+ if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered Array.from() requires an anchor and optional inline mapper")
108
+ const source = analyzeCollectionPipeline(value.arguments[0], nestedOptions)
109
+ if (!source) return undefined
110
+ let mapper
111
+ if (value.arguments[1]) {
112
+ const parameters = collectionParameters(value.arguments[1], "Rendered Array.from() mapper", fail)
113
+ const selectorStates = new Set(source.selectorStates)
114
+ mapper = collectionExpression(unwrapExpression(value.arguments[1].body), { parameters, fail, stateNames, selectorStates })
115
+ source.selectorStates = selectorStates
116
+ }
117
+ return { ...source, selector: [...source.selector, ["from", mapper]] }
118
+ }
119
+ }
120
+
121
+ export function collectionExpression(expression, { parameters = {}, fail, stateNames = new Set(), selectorStates = new Set() }) {
122
+ const encode = node => {
123
+ node = unwrapExpression(node)
124
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return ["value", ts.isNumericLiteral(node) ? Number(node.text) : node.text]
125
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return ["value", true]
126
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return ["value", false]
127
+ if (node.kind === ts.SyntaxKind.NullKeyword) return ["value", null]
128
+ if (ts.isIdentifier(node)) {
129
+ if (node.text === parameters.item) return ["item"]
130
+ if (node.text === parameters.index) return ["index"]
131
+ if (node.text === "undefined") return ["undefined"]
132
+ if (stateNames.has(node.text)) {
133
+ selectorStates.add(node.text)
134
+ return ["state", node.text]
135
+ }
136
+ fail(node, `Rendered collection expression identifier "${node.text}" is not allowed`)
137
+ }
138
+ if (ts.isPropertyAccessExpression(node)) {
139
+ if (["__proto__", "constructor", "prototype"].includes(node.name.text)) fail(node, `Rendered collection property "${node.name.text}" is not supported`)
140
+ return ["get", encode(node.expression), node.name.text, Boolean(node.questionDotToken)]
141
+ }
142
+ if (ts.isElementAccessExpression(node)) {
143
+ const key = node.argumentExpression
144
+ if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(node, "Rendered collection computed properties require a direct string or numeric literal key")
145
+ if (ts.isStringLiteral(key) && ["__proto__", "constructor", "prototype"].includes(key.text)) fail(node, `Rendered collection property "${key.text}" is not supported`)
146
+ return ["get", encode(node.expression), ts.isNumericLiteral(key) ? Number(key.text) : key.text, Boolean(node.questionDotToken)]
147
+ }
148
+ if (ts.isPrefixUnaryExpression(node)) {
149
+ const operator = node.operator === ts.SyntaxKind.ExclamationToken ? "!" : node.operator === ts.SyntaxKind.PlusToken ? "+" : node.operator === ts.SyntaxKind.MinusToken ? "-" : undefined
150
+ if (!operator) fail(node, "Rendered collection expression uses an unsupported unary operator")
151
+ return ["unary", operator, encode(node.operand)]
152
+ }
153
+ if (ts.isTypeOfExpression(node)) return ["unary", "typeof", encode(node.expression)]
154
+ if (ts.isBinaryExpression(node)) {
155
+ const operator = node.operatorToken.getText()
156
+ if (!new Set(["&&", "||", "??", "===", "!==", "==", "!=", "<", "<=", ">", ">=", "+", "-", "*", "/", "%"]).has(operator)) fail(node, `Rendered collection expression operator "${operator}" is not supported`)
157
+ return ["binary", operator, encode(node.left), encode(node.right)]
158
+ }
159
+ if (ts.isConditionalExpression(node)) return ["conditional", encode(node.condition), encode(node.whenTrue), encode(node.whenFalse)]
160
+ if (ts.isArrayLiteralExpression(node) && !node.elements.some(ts.isSpreadElement)) return ["array", ...node.elements.map(encode)]
161
+ if (ts.isObjectLiteralExpression(node)) return ["object", ...node.properties.map(property => {
162
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name) && !ts.isNumericLiteral(property.name)) fail(property, "Rendered collection mapper objects require direct properties")
163
+ return [property.name.text, encode(property.initializer)]
164
+ })]
165
+ if (ts.isTemplateExpression(node)) return ["template", [node.head.text, ...node.templateSpans.map(span => span.literal.text)], node.templateSpans.map(span => encode(span.expression))]
166
+ if (ts.isCallExpression(node)) {
167
+ if (ts.isIdentifier(node.expression) && ["Boolean", "Number", "String"].includes(node.expression.text)) return ["global", node.expression.text, ...node.arguments.map(encode)]
168
+ if (ts.isPropertyAccessExpression(node.expression)) {
169
+ const method = node.expression.name.text
170
+ if (ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "Math" && pureCollectionMathMethods.has(method)) return ["math", method, ...node.arguments.map(encode)]
171
+ if (pureCollectionMethods.has(method)) return ["call", encode(node.expression.expression), method, ...node.arguments.map(encode)]
172
+ if (mutatingCollectionMethods.has(method)) fail(node, `Rendered collection expressions cannot call mutating method "${method}"`)
173
+ }
174
+ fail(node, "Rendered collection expressions cannot call arbitrary functions")
175
+ }
176
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isAwaitExpression(node) || ts.isNewExpression(node) || ts.isYieldExpression(node) || ts.isDeleteExpression(node) || ts.isPostfixUnaryExpression(node)) fail(node, "Rendered collection expressions must be pure and synchronous")
177
+ fail(node, "Rendered collection expression is not supported")
178
+ }
179
+ return encode(expression)
180
+ }
181
+
182
+ function directProperty(expression, objectName) {
183
+ const value = unwrapExpression(expression)
184
+ if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
185
+ if (objectName !== undefined && value.expression.text !== objectName) return undefined
186
+ return value.name.text
187
+ }
@@ -0,0 +1,222 @@
1
+ import ts from "typescript"
2
+ import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
3
+
4
+ export function createSemanticArtifact() {
5
+ return { nativeHandlers: [], effectHandlers: [], reactiveBindings: [], listExpressions: [], clientImports: new Set() }
6
+ }
7
+
8
+ export function createDescriptorSession({ semantic, handlerUrl, factory, context, compileEventCommand, isPrimitiveLiteral, rejectWorkerConstructions }) {
9
+ const { nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
10
+
11
+ function compileListExpression(read, expression, item, index, states = new Set()) {
12
+ const exportName = `listExpression${listExpressions.length}`
13
+ listExpressions.push({ exportName, expression, item, index, states })
14
+ const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
15
+ if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
16
+ return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
17
+ }
18
+
19
+ function compileListConditional(entry) {
20
+ const exportName = `listExpression${listExpressions.length}`
21
+ listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index })
22
+ const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
23
+ const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
24
+ return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
25
+ factory.createStringLiteral(entry.kind), read, thunk(entry.truthy), thunk(entry.falsy), factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)
26
+ ])
27
+ }
28
+
29
+ function compileListValue(expression, entry) {
30
+ const rewrite = node => {
31
+ if (ts.isShorthandPropertyAssignment(node) && entry.states?.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
32
+ if (ts.isIdentifier(node) && entry.states?.has(node.text) && isReferenceIdentifier(node)) return factory.createPropertyAccessExpression(node, "value")
33
+ return ts.visitEachChild(node, rewrite, context)
34
+ }
35
+ const initial = entry.states?.size ? ts.visitNode(expression, rewrite) : expression
36
+ const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), initial)
37
+ return entry.field
38
+ ? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
39
+ : compileListExpression(read, expression, entry.item, entry.index, entry.states)
40
+ }
41
+
42
+ function compileReactiveBinding(expression, { setters, importBindings = new Map() }) {
43
+ const parts = conditionalParts(expression)
44
+ const state = parts && directStateIdentifier(parts.condition, setters)
45
+ if (state && isPrimitiveLiteral(parts.truthy) && isPrimitiveLiteral(parts.falsy)) {
46
+ return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
47
+ }
48
+ return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings))
49
+ }
50
+
51
+ function compileConditional(kind, expression, truthy, falsy, setters) {
52
+ const state = directStateIdentifier(expression, setters)
53
+ const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
54
+ if (state) return factory.createCallExpression(factory.createIdentifier("__kStateConditional"), undefined, [factory.createStringLiteral(kind), state, thunk(truthy), thunk(falsy)])
55
+ const [initial, ...descriptor] = compileReactiveExpression(expression, setters)
56
+ return factory.createCallExpression(factory.createIdentifier("__kConditional"), undefined, [factory.createStringLiteral(kind), initial, thunk(truthy), thunk(falsy), ...descriptor])
57
+ }
58
+
59
+ function compileReactiveExpression(expression, setters, importBindings = new Map()) {
60
+ const usedStates = referencedStateNames(expression, setters)
61
+ const importedNames = referencedImportedBindings(expression, importBindings)
62
+ const imports = [...importedNames].map(name => importBindings.get(name))
63
+ registerClientImports(imports)
64
+ const captures = new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
65
+ const exportName = `binding${reactiveBindings.length}`
66
+ reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports })
67
+ const states = [...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
68
+ const scope = [...captures].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
69
+ const stateNames = new Set(usedStates)
70
+ const rewriteInitial = node => {
71
+ if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
72
+ if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) return factory.createPropertyAccessExpression(node, "value")
73
+ if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createIdentifier("__kBindingValue"), undefined, [node.name]))
74
+ if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) return factory.createCallExpression(factory.createIdentifier("__kBindingValue"), undefined, [node])
75
+ return ts.visitEachChild(node, rewriteInitial, context)
76
+ }
77
+ return [
78
+ ts.visitNode(expression, rewriteInitial),
79
+ factory.createStringLiteral(handlerUrl),
80
+ factory.createStringLiteral(exportName),
81
+ factory.createArrayLiteralExpression(states),
82
+ factory.createArrayLiteralExpression(scope)
83
+ ]
84
+ }
85
+
86
+ function compileEvent(expression, { setters, reducers, functions, listItem, importBindings }) {
87
+ if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
88
+ if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
89
+ const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters)
90
+ if (optimized) return optimized
91
+ rejectWorkerConstructions(expression)
92
+ const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", listItem })
93
+ return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
94
+ factory.createStringLiteral(handlerUrl), factory.createStringLiteral(descriptor.exportName), descriptor.states, descriptor.scope
95
+ ])
96
+ }
97
+
98
+ function compileEffectCallback(expression, options) {
99
+ return compileNativeCallback(expression, { ...options, entries: effectHandlers, prefix: "effect" })
100
+ }
101
+
102
+ function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, listItem, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
103
+ const allCaptures = nativeCaptureNames(expression, setters)
104
+ const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
105
+ const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
106
+ imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
107
+ const captures = new Set([...allCaptures].filter(name => !importBindings.has(name) && !usedReducers.has(name)))
108
+ registerClientImports(imports)
109
+ const usedStates = referencedStateNames(expression.body, setters, expression)
110
+ for (const name of usedReducers) {
111
+ const reducer = reducers.get(name)
112
+ if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction)) usedStates.add(state)
113
+ }
114
+ const exportName = `${prefix}${entries.length}`
115
+ entries.push({ exportName, expression, captures, imports, liveStates, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested })
116
+ const value = name => deferValues
117
+ ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
118
+ : factory.createIdentifier(name)
119
+ return {
120
+ exportName,
121
+ states: factory.createArrayLiteralExpression([...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), value(name)]))),
122
+ scope: factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
123
+ factory.createStringLiteral(name),
124
+ name === (typeof listItem === "string" ? listItem : listItem?.item)
125
+ ? factory.createCallExpression(factory.createIdentifier("__kListItem"), undefined, [])
126
+ : name === listItem?.index ? factory.createCallExpression(factory.createIdentifier("__kListIndex"), undefined, []) : value(name)
127
+ ])))
128
+ }
129
+ }
130
+
131
+ function compileOptimizedEvent(expression, setters) {
132
+ const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
133
+ const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters, factory) : undefined)
134
+ if (!commands.length || commands.some(command => !command)) return undefined
135
+ return factory.createCallExpression(factory.createIdentifier("__kBehavior"), undefined, [factory.createArrayLiteralExpression(commands)])
136
+ }
137
+
138
+ function registerClientImports(imports) {
139
+ for (const entry of imports) if (!entry.package) clientImports.add(entry.target)
140
+ }
141
+
142
+ return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding }
143
+ }
144
+
145
+ function directStateIdentifier(expression, setters) {
146
+ const value = unwrapExpression(expression)
147
+ return ts.isIdentifier(value) && new Set(setters.values()).has(value.text) ? value : undefined
148
+ }
149
+
150
+ function conditionalParts(expression) {
151
+ const unwrap = node => ts.isParenthesizedExpression(node) ? unwrap(node.expression) : node
152
+ const value = unwrap(expression)
153
+ if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) return { condition: value.left, truthy: unwrap(value.right), falsy: ts.factory.createNull() }
154
+ if (ts.isConditionalExpression(value)) return { condition: value.condition, truthy: unwrap(value.whenTrue), falsy: unwrap(value.whenFalse) }
155
+ return undefined
156
+ }
157
+
158
+ export function referencedReducerDispatches(root, reducers, scopeRoot = root) {
159
+ const used = new Set()
160
+ const visit = node => {
161
+ if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(node.text)
162
+ ts.forEachChild(node, visit)
163
+ }
164
+ visit(root)
165
+ return used
166
+ }
167
+
168
+ export function referencedStateNames(root, setters, scopeRoot = root) {
169
+ const stateNames = new Set(setters.values())
170
+ const used = new Set()
171
+ const visit = node => {
172
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, scopeRoot)) used.add(setters.get(node.expression.text))
173
+ if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(setters.get(node.text))
174
+ if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, scopeRoot)) used.add(node.text)
175
+ ts.forEachChild(node, visit)
176
+ }
177
+ visit(root)
178
+ return used
179
+ }
180
+
181
+ export function nativeCaptureNames(expression, setters) {
182
+ return captureNames(expression, expression.body, setters)
183
+ }
184
+
185
+ function referencedImportedBindings(expression, imports) {
186
+ const names = new Set()
187
+ const visit = node => {
188
+ if (ts.isIdentifier(node) && imports.has(node.text) && isReferenceIdentifier(node)) names.add(node.text)
189
+ ts.forEachChild(node, visit)
190
+ }
191
+ visit(expression.body ?? expression)
192
+ return names
193
+ }
194
+
195
+ export function captureNames(declarationRoot, referenceRoot, setters) {
196
+ const local = new Set()
197
+ if (!isFunctionLike(declarationRoot)) {
198
+ const collectDeclarations = node => {
199
+ if (ts.isVariableDeclaration(node)) for (const name of bindingNames(node.name)) local.add(name)
200
+ if (ts.isParameter(node)) for (const name of bindingNames(node.name)) local.add(name)
201
+ if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) local.add(node.name.text)
202
+ ts.forEachChild(node, collectDeclarations)
203
+ }
204
+ collectDeclarations(declarationRoot)
205
+ }
206
+ const stateNames = new Set(setters.values())
207
+ const captures = new Set()
208
+ const visit = node => {
209
+ if (ts.isTypeNode(node)) return
210
+ if (ts.isIdentifier(node)) {
211
+ const declared = isFunctionLike(declarationRoot) ? isShadowedIdentifier(node, declarationRoot) : local.has(node.text)
212
+ if (isReferenceIdentifier(node) && !declared && !setters.has(node.text) && !stateNames.has(node.text) && !nativeGlobals.has(node.text)) captures.add(node.text)
213
+ }
214
+ ts.forEachChild(node, visit)
215
+ }
216
+ visit(referenceRoot)
217
+ return captures
218
+ }
219
+
220
+ const nativeGlobals = new Set([
221
+ "Array", "ArrayBuffer", "BigInt", "Blob", "Boolean", "Date", "Error", "Event", "FileReader", "FormData", "Infinity", "IntersectionObserver", "Intl", "JSON", "Map", "Math", "NaN", "Number", "Object", "Promise", "Proxy", "RangeError", "ReferenceError", "Reflect", "RegExp", "Set", "String", "Symbol", "TypeError", "URL", "URLSearchParams", "WeakMap", "WeakSet", "WebSocket", "Worker", "alert", "atob", "btoa", "cancelAnimationFrame", "clearInterval", "clearTimeout", "console", "crypto", "document", "fetch", "globalThis", "history", "isFinite", "isNaN", "localStorage", "location", "navigator", "parseFloat", "parseInt", "performance", "queueMicrotask", "requestAnimationFrame", "setInterval", "setTimeout", "structuredClone", "undefined", "window"
222
+ ])
@@ -0,0 +1,35 @@
1
+ import ts from "typescript"
2
+
3
+ export function createEventCommandCompiler({ isPrimitiveLiteral, synthesizeSerializableStateLiteral }) {
4
+ return function compileEventCommand(expression, setters, factory) {
5
+ if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && ts.isIdentifier(expression.expression.expression) && expression.expression.expression.text === "console" && expression.expression.name.text === "log" && expression.arguments.length === 2 && ts.isStringLiteral(expression.arguments[0]) && ts.isIdentifier(expression.arguments[1]) && [...setters.values()].includes(expression.arguments[1].text)) {
6
+ return command(factory, "log", expression.arguments[1], factory.createStringLiteral(expression.arguments[0].text))
7
+ }
8
+
9
+ if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.arguments.length !== 1) return undefined
10
+ const stateName = setters.get(expression.expression.text)
11
+ if (!stateName) return undefined
12
+
13
+ const state = factory.createIdentifier(stateName)
14
+ const value = expression.arguments[0]
15
+ if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === stateName && ts.isNumericLiteral(value.right)) {
16
+ if (value.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
17
+ return command(factory, "add", state, numericExpression(factory, Number(value.right.text), value.operatorToken.kind === ts.SyntaxKind.MinusToken))
18
+ }
19
+ if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text && ts.isNumericLiteral(value.body.right)) {
20
+ if (value.body.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.body.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
21
+ return command(factory, "add", state, numericExpression(factory, Number(value.body.right.text), value.body.operatorToken.kind === ts.SyntaxKind.MinusToken))
22
+ }
23
+ if (isPrimitiveLiteral(value)) return command(factory, "set", state, synthesizeSerializableStateLiteral(value, factory))
24
+ return undefined
25
+ }
26
+ }
27
+
28
+ function command(factory, operation, state, value) {
29
+ return factory.createArrayLiteralExpression([factory.createStringLiteral(operation), state, value])
30
+ }
31
+
32
+ function numericExpression(factory, value, negative) {
33
+ const literal = factory.createNumericLiteral(value)
34
+ return negative ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, literal) : literal
35
+ }
@@ -1,7 +1,8 @@
1
1
  import ts from "typescript"
2
2
  import { bindingNames, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, loopDeclaresName, nearestFunction, nearestFunctionLike, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
3
+ import { analyzeCollectionPipeline, isArrayFromCall } from "./collection-analysis.mjs"
3
4
 
4
- export function createReactMigrationPass({ cloneAst, isArrayFromCall, jsxTagName, renderedCollectionSource }) {
5
+ export function createReactMigrationPass({ cloneAst, jsxTagName }) {
5
6
  function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections = new Set()) {
6
7
  const supported = new Set(["createContext", "useContext", "useEffect", "useId", "useReducer", "useRef", "useState"])
7
8
  const erased = new Set(["forwardRef", "memo", "useCallback", "useMemo"])
@@ -291,7 +292,7 @@ export function createReactMigrationPass({ cloneAst, isArrayFromCall, jsxTagName
291
292
  function reactMemoCollection(expression, states, importedCollections, sourceFile) {
292
293
  const setters = new Map([...states].map(state => [state, state]))
293
294
  const fail = (node, message) => { throw sourceNodeError(node, sourceFile, message) }
294
- return renderedCollectionSource(expression, setters, undefined, fail, new Set(), importedCollections, states)
295
+ return analyzeCollectionPipeline(expression, { setters, fail, importedCollections, stateNames: states })
295
296
  }
296
297
 
297
298
  function reactMemoComponentExpression(identifier, sourceFile, factory, context) {
@@ -0,0 +1,118 @@
1
+ export function usesRouteDependencyRuntime({ plan, navigable, hasBindings, hasLists }) {
2
+ const hasDependencies = plan.effects.some(effect => effect.dependencies?.length)
3
+ return !navigable && hasDependencies && !plan.effects.some(effect => effect.owner) && !hasBindings && !hasLists && !plan.events.some(event => event.native)
4
+ }
5
+
6
+ export function planRouteCapabilities(plans, { routes = new Map(), navigationRouteCount = 0 } = {}) {
7
+ const commandEvents = new Set()
8
+ const nativeEvents = new Set()
9
+ const bindings = { count: 0, text: false, svgConditions: false }
10
+ const lists = {
11
+ count: 0,
12
+ styleCount: 0,
13
+ conditions: false,
14
+ svg: false,
15
+ deepConditions: false,
16
+ textRanges: false,
17
+ attributes: false,
18
+ events: false,
19
+ expressions: false,
20
+ expressionAttributes: false,
21
+ seeds: false,
22
+ effects: false,
23
+ rowHooks: false,
24
+ rowRefs: false,
25
+ complexRowState: false,
26
+ nested: false,
27
+ selectors: false,
28
+ calculated: false,
29
+ static: false,
30
+ indexes: false,
31
+ stableFastPaths: false,
32
+ generalRowHooks: false,
33
+ asyncParts: false,
34
+ mounts: false
35
+ }
36
+ const effects = { any: false, derivedDependencies: false, itemDependencies: false, captures: false, navigable: false, navigableOwners: false }
37
+
38
+ for (let index = 0; index < plans.length; index++) {
39
+ const plan = plans[index]
40
+ const route = routes.get(plan.route)
41
+ for (const event of plan.events) {
42
+ if (event.commands) commandEvents.add(event.event)
43
+ if (event.native) nativeEvents.add(event.event)
44
+ }
45
+ bindings.text ||= plan.bindings.some(binding => binding.target === "text")
46
+ bindings.svgConditions ||= plan.conditions.some(condition => condition.svg)
47
+ effects.any ||= plan.effects.length > 0
48
+ effects.derivedDependencies ||= plan.effects.some(effect => effect.dependencyExpressions?.length)
49
+ effects.itemDependencies ||= plan.effects.some(effect => effect.itemDependencies?.length)
50
+ effects.captures ||= plan.effects.some(effect => Object.keys(effect.scope).length)
51
+ effects.navigable ||= Boolean(route?.navigable && plan.effects.length)
52
+ effects.navigableOwners ||= Boolean(route?.navigable && plan.effects.some(effect => effect.owner))
53
+ for (const list of plan.lists) {
54
+ lists.conditions ||= Boolean(list.conditions)
55
+ lists.svg ||= Boolean(list.svg)
56
+ lists.deepConditions ||= Boolean(list.conditionHandlers)
57
+ lists.textRanges ||= Boolean(list.textRanges)
58
+ lists.attributes ||= Boolean(list.attributes)
59
+ lists.events ||= Boolean(list.events)
60
+ lists.expressions ||= Boolean(list.expressions)
61
+ lists.expressionAttributes ||= Boolean(list.expressionAttributes)
62
+ lists.seeds ||= Boolean(list.seed || list.valueSeed)
63
+ lists.effects ||= Boolean(list.effects)
64
+ lists.rowHooks ||= Boolean(list.rowStates?.length || list.rowRefs?.length)
65
+ lists.rowRefs ||= Boolean(list.rowRefs?.length)
66
+ lists.complexRowState ||= Boolean(list.rowStates?.some(state => state.initialValue !== null && typeof state.initialValue === "object"))
67
+ lists.nested ||= Boolean(list.ownerField)
68
+ lists.selectors ||= Boolean(list.selector)
69
+ lists.calculated ||= Boolean(list.source)
70
+ lists.static ||= Boolean(list.static)
71
+ lists.indexes ||= Boolean(list.indexed)
72
+ lists.stableFastPaths ||= !list.children && !list.ownerField && list.key !== null && !list.indexed && !list.reducer && !list.selector
73
+ lists.generalRowHooks ||= Boolean(list.ownerField && (list.rowStates?.length || list.rowRefs?.length))
74
+ lists.mounts ||= Boolean(list.mount)
75
+ }
76
+ }
77
+
78
+ const routeEntries = [...routes.values()]
79
+ const routeCounts = {
80
+ behaviors: routeEntries.filter(route => route.hasBehaviors).length,
81
+ regularBehaviors: routeEntries.filter(route => route.hasBehaviors && !route.usesDependencyRuntime).length,
82
+ regularStateSeeds: routeEntries.filter(route => route.hasStateSeed && !route.usesDependencyRuntime).length,
83
+ dependencyStateSeeds: routeEntries.filter(route => route.hasStateSeed && route.usesDependencyRuntime).length
84
+ }
85
+ bindings.count = routeEntries.filter(route => route.hasBindings).length
86
+ lists.count = routeEntries.filter(route => route.hasLists).length
87
+ lists.styleCount = routeEntries.filter(route => route.hasListStyles).length
88
+ lists.generalRowHooks ||= lists.rowRefs || lists.complexRowState
89
+ lists.asyncParts = lists.expressions || lists.expressionAttributes || lists.conditions
90
+ lists.mounts ||= lists.conditions || lists.nested
91
+
92
+ return {
93
+ routes: routeCounts,
94
+ events: { command: [...commandEvents].sort(), native: [...nativeEvents].sort(), hasNativeHandlers: nativeEvents.size > 0 },
95
+ bindings,
96
+ lists,
97
+ effects,
98
+ captures: { nestedState: hasNestedCaptureState(plans), setter: hasCaptureType(plans, "setter") },
99
+ runtime: {
100
+ shared: Boolean(bindings.count || lists.count || nativeEvents.size || navigationRouteCount),
101
+ dependency: routeEntries.some(route => route.usesDependencyRuntime)
102
+ }
103
+ }
104
+ }
105
+
106
+ function hasCaptureType(value, type) {
107
+ if (!value || typeof value !== "object") return false
108
+ if (value.type === type) return true
109
+ return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasCaptureType(entry, type))
110
+ }
111
+
112
+ function hasNestedCaptureState(value, insideCapture = false) {
113
+ if (!value || typeof value !== "object") return false
114
+ if (value.type === "state") return insideCapture
115
+ if (value.type === "array") return value.value.some(entry => hasNestedCaptureState(entry, true))
116
+ if (value.type === "object") return value.value.some(([, entry]) => hasNestedCaptureState(entry, true))
117
+ return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasNestedCaptureState(entry, false))
118
+ }