@kudzujs/core 0.8.15 → 0.8.17

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,20 @@
1
+ import ts from "typescript"
2
+
3
+ export function generateCommandBehavior(moduleIR, handler, factory = ts.factory) {
4
+ const commands = handler.commands.map(command => factory.createArrayLiteralExpression([
5
+ factory.createStringLiteral(command.operation),
6
+ factory.createIdentifier(moduleIR.signals[command.signal].debugName),
7
+ literal(factory, command.value, command.syntax)
8
+ ]))
9
+ return factory.createCallExpression(factory.createIdentifier("__kBehavior"), undefined, [factory.createArrayLiteralExpression(commands)])
10
+ }
11
+
12
+ function literal(factory, value, syntax) {
13
+ if (value === null) return factory.createNull()
14
+ if (typeof value === "string") return factory.createStringLiteral(value)
15
+ if (typeof value === "boolean") return value ? factory.createTrue() : factory.createFalse()
16
+ const number = factory.createNumericLiteral(Math.abs(value))
17
+ if (syntax === "positive") return factory.createPrefixUnaryExpression(ts.SyntaxKind.PlusToken, number)
18
+ if (syntax === "negative") return factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, number)
19
+ return value < 0 ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, number) : number
20
+ }
@@ -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,231 @@
1
+ import ts from "typescript"
2
+ import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
3
+ import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
4
+ import { createModuleIR, registerCommandHandler } from "./ir/module-ir.mjs"
5
+
6
+ export function createSemanticArtifact(file) {
7
+ return { moduleIR: createModuleIR(file), nativeHandlers: [], effectHandlers: [], reactiveBindings: [], listExpressions: [], clientImports: new Set() }
8
+ }
9
+
10
+ export function createDescriptorSession({ semantic, handlerUrl, factory, context, compileEventCommand, isPrimitiveLiteral, rejectWorkerConstructions, sourceName = source => source.fileName }) {
11
+ const { moduleIR, nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
12
+ const stateScopes = new WeakMap()
13
+ let nextStateScope = 0
14
+
15
+ function compileListExpression(read, expression, item, index, states = new Set()) {
16
+ const exportName = `listExpression${listExpressions.length}`
17
+ listExpressions.push({ exportName, expression, item, index, states })
18
+ const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
19
+ if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
20
+ return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
21
+ }
22
+
23
+ function compileListConditional(entry) {
24
+ const exportName = `listExpression${listExpressions.length}`
25
+ listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index })
26
+ const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
27
+ const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
28
+ return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
29
+ factory.createStringLiteral(entry.kind), read, thunk(entry.truthy), thunk(entry.falsy), factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)
30
+ ])
31
+ }
32
+
33
+ function compileListValue(expression, entry) {
34
+ const rewrite = node => {
35
+ if (ts.isShorthandPropertyAssignment(node) && entry.states?.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
36
+ if (ts.isIdentifier(node) && entry.states?.has(node.text) && isReferenceIdentifier(node)) return factory.createPropertyAccessExpression(node, "value")
37
+ return ts.visitEachChild(node, rewrite, context)
38
+ }
39
+ const initial = entry.states?.size ? ts.visitNode(expression, rewrite) : expression
40
+ const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), initial)
41
+ return entry.field
42
+ ? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
43
+ : compileListExpression(read, expression, entry.item, entry.index, entry.states)
44
+ }
45
+
46
+ function compileReactiveBinding(expression, { setters, importBindings = new Map() }) {
47
+ const parts = conditionalParts(expression)
48
+ const state = parts && directStateIdentifier(parts.condition, setters)
49
+ if (state && isPrimitiveLiteral(parts.truthy) && isPrimitiveLiteral(parts.falsy)) {
50
+ return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
51
+ }
52
+ return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings))
53
+ }
54
+
55
+ function compileConditional(kind, expression, truthy, falsy, setters) {
56
+ const state = directStateIdentifier(expression, setters)
57
+ const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
58
+ if (state) return factory.createCallExpression(factory.createIdentifier("__kStateConditional"), undefined, [factory.createStringLiteral(kind), state, thunk(truthy), thunk(falsy)])
59
+ const [initial, ...descriptor] = compileReactiveExpression(expression, setters)
60
+ return factory.createCallExpression(factory.createIdentifier("__kConditional"), undefined, [factory.createStringLiteral(kind), initial, thunk(truthy), thunk(falsy), ...descriptor])
61
+ }
62
+
63
+ function compileReactiveExpression(expression, setters, importBindings = new Map()) {
64
+ const usedStates = referencedStateNames(expression, setters)
65
+ const importedNames = referencedImportedBindings(expression, importBindings)
66
+ const imports = [...importedNames].map(name => importBindings.get(name))
67
+ registerClientImports(imports)
68
+ const captures = new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
69
+ const exportName = `binding${reactiveBindings.length}`
70
+ reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports })
71
+ const states = [...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
72
+ const scope = [...captures].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
73
+ const stateNames = new Set(usedStates)
74
+ const rewriteInitial = node => {
75
+ if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
76
+ if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) return factory.createPropertyAccessExpression(node, "value")
77
+ if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createIdentifier("__kBindingValue"), undefined, [node.name]))
78
+ if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) return factory.createCallExpression(factory.createIdentifier("__kBindingValue"), undefined, [node])
79
+ return ts.visitEachChild(node, rewriteInitial, context)
80
+ }
81
+ return [
82
+ ts.visitNode(expression, rewriteInitial),
83
+ factory.createStringLiteral(handlerUrl),
84
+ factory.createStringLiteral(exportName),
85
+ factory.createArrayLiteralExpression(states),
86
+ factory.createArrayLiteralExpression(scope)
87
+ ]
88
+ }
89
+
90
+ function compileEvent(expression, { setters, reducers, functions, listItem, importBindings }) {
91
+ if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
92
+ if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
93
+ const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters)
94
+ if (optimized) return optimized
95
+ rejectWorkerConstructions(expression)
96
+ const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", listItem })
97
+ return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
98
+ factory.createStringLiteral(handlerUrl), factory.createStringLiteral(descriptor.exportName), descriptor.states, descriptor.scope
99
+ ])
100
+ }
101
+
102
+ function compileEffectCallback(expression, options) {
103
+ return compileNativeCallback(expression, { ...options, entries: effectHandlers, prefix: "effect" })
104
+ }
105
+
106
+ function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, listItem, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
107
+ const allCaptures = nativeCaptureNames(expression, setters)
108
+ const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
109
+ const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
110
+ imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
111
+ const captures = new Set([...allCaptures].filter(name => !importBindings.has(name) && !usedReducers.has(name)))
112
+ registerClientImports(imports)
113
+ const usedStates = referencedStateNames(expression.body, setters, expression)
114
+ for (const name of usedReducers) {
115
+ const reducer = reducers.get(name)
116
+ if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction)) usedStates.add(state)
117
+ }
118
+ const exportName = `${prefix}${entries.length}`
119
+ 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 })
120
+ const value = name => deferValues
121
+ ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
122
+ : factory.createIdentifier(name)
123
+ return {
124
+ exportName,
125
+ states: factory.createArrayLiteralExpression([...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), value(name)]))),
126
+ scope: factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
127
+ factory.createStringLiteral(name),
128
+ name === (typeof listItem === "string" ? listItem : listItem?.item)
129
+ ? factory.createCallExpression(factory.createIdentifier("__kListItem"), undefined, [])
130
+ : name === listItem?.index ? factory.createCallExpression(factory.createIdentifier("__kListIndex"), undefined, []) : value(name)
131
+ ])))
132
+ }
133
+ }
134
+
135
+ function compileOptimizedEvent(expression, setters) {
136
+ const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
137
+ const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters) : undefined)
138
+ if (!commands.length || commands.some(command => !command)) return undefined
139
+ const original = ts.getOriginalNode(expression)
140
+ const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
141
+ if (!stateScopes.has(setters)) stateScopes.set(setters, `state:${nextStateScope++}`)
142
+ const scope = stateScopes.get(setters)
143
+ const handler = registerCommandHandler(moduleIR, commands, source, scope)
144
+ return generateCommandBehavior(moduleIR, handler, factory)
145
+ }
146
+
147
+ function registerClientImports(imports) {
148
+ for (const entry of imports) if (!entry.package) clientImports.add(entry.target)
149
+ }
150
+
151
+ return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding }
152
+ }
153
+
154
+ function directStateIdentifier(expression, setters) {
155
+ const value = unwrapExpression(expression)
156
+ return ts.isIdentifier(value) && new Set(setters.values()).has(value.text) ? value : undefined
157
+ }
158
+
159
+ function conditionalParts(expression) {
160
+ const unwrap = node => ts.isParenthesizedExpression(node) ? unwrap(node.expression) : node
161
+ const value = unwrap(expression)
162
+ if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) return { condition: value.left, truthy: unwrap(value.right), falsy: ts.factory.createNull() }
163
+ if (ts.isConditionalExpression(value)) return { condition: value.condition, truthy: unwrap(value.whenTrue), falsy: unwrap(value.whenFalse) }
164
+ return undefined
165
+ }
166
+
167
+ export function referencedReducerDispatches(root, reducers, scopeRoot = root) {
168
+ const used = new Set()
169
+ const visit = node => {
170
+ if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(node.text)
171
+ ts.forEachChild(node, visit)
172
+ }
173
+ visit(root)
174
+ return used
175
+ }
176
+
177
+ export function referencedStateNames(root, setters, scopeRoot = root) {
178
+ const stateNames = new Set(setters.values())
179
+ const used = new Set()
180
+ const visit = node => {
181
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, scopeRoot)) used.add(setters.get(node.expression.text))
182
+ if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(setters.get(node.text))
183
+ if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, scopeRoot)) used.add(node.text)
184
+ ts.forEachChild(node, visit)
185
+ }
186
+ visit(root)
187
+ return used
188
+ }
189
+
190
+ export function nativeCaptureNames(expression, setters) {
191
+ return captureNames(expression, expression.body, setters)
192
+ }
193
+
194
+ function referencedImportedBindings(expression, imports) {
195
+ const names = new Set()
196
+ const visit = node => {
197
+ if (ts.isIdentifier(node) && imports.has(node.text) && isReferenceIdentifier(node)) names.add(node.text)
198
+ ts.forEachChild(node, visit)
199
+ }
200
+ visit(expression.body ?? expression)
201
+ return names
202
+ }
203
+
204
+ export function captureNames(declarationRoot, referenceRoot, setters) {
205
+ const local = new Set()
206
+ if (!isFunctionLike(declarationRoot)) {
207
+ const collectDeclarations = node => {
208
+ if (ts.isVariableDeclaration(node)) for (const name of bindingNames(node.name)) local.add(name)
209
+ if (ts.isParameter(node)) for (const name of bindingNames(node.name)) local.add(name)
210
+ if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) local.add(node.name.text)
211
+ ts.forEachChild(node, collectDeclarations)
212
+ }
213
+ collectDeclarations(declarationRoot)
214
+ }
215
+ const stateNames = new Set(setters.values())
216
+ const captures = new Set()
217
+ const visit = node => {
218
+ if (ts.isTypeNode(node)) return
219
+ if (ts.isIdentifier(node)) {
220
+ const declared = isFunctionLike(declarationRoot) ? isShadowedIdentifier(node, declarationRoot) : local.has(node.text)
221
+ if (isReferenceIdentifier(node) && !declared && !setters.has(node.text) && !stateNames.has(node.text) && !nativeGlobals.has(node.text)) captures.add(node.text)
222
+ }
223
+ ts.forEachChild(node, visit)
224
+ }
225
+ visit(referenceRoot)
226
+ return captures
227
+ }
228
+
229
+ const nativeGlobals = new Set([
230
+ "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"
231
+ ])
@@ -0,0 +1,23 @@
1
+ export function createModuleIR(file) {
2
+ return { version: 1, file, signals: [], handlers: [] }
3
+ }
4
+
5
+ export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
6
+ const slots = new Map(moduleIR.signals.map(signal => [signal.key, signal.slot]))
7
+ for (const { state } of commands) {
8
+ const key = `${scope}:${state}`
9
+ if (slots.has(key)) continue
10
+ const slot = moduleIR.signals.length
11
+ slots.set(key, slot)
12
+ moduleIR.signals.push({ slot, key, debugName: state })
13
+ }
14
+ const signal = state => slots.get(`${scope}:${state}`) ?? slots.get(state)
15
+ const handler = {
16
+ slot: moduleIR.handlers.length,
17
+ kind: "commands",
18
+ commands: commands.map(({ operation, state, value, syntax }) => ({ operation, signal: signal(state), value, ...(syntax ? { syntax } : {}) })),
19
+ ...(source ? { source } : {})
20
+ }
21
+ moduleIR.handlers.push(handler)
22
+ return handler
23
+ }
@@ -0,0 +1,46 @@
1
+ import ts from "typescript"
2
+
3
+ export function createCommandSpecializer({ isPrimitiveLiteral }) {
4
+ return function specializeCommand(expression, setters) {
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 { operation: "log", state: expression.arguments[1].text, value: expression.arguments[0].text }
7
+ }
8
+
9
+ if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.arguments.length !== 1) return undefined
10
+ const state = setters.get(expression.expression.text)
11
+ if (!state) return undefined
12
+
13
+ const value = expression.arguments[0]
14
+ if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === state && ts.isNumericLiteral(value.right)) {
15
+ if (value.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
16
+ const operand = signedNumber(value.right, value.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
17
+ return operand ? { operation: "add", state, ...operand } : undefined
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
+ const operand = signedNumber(value.body.right, value.body.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
22
+ return operand ? { operation: "add", state, ...operand } : undefined
23
+ }
24
+ if (isPrimitiveLiteral(value)) {
25
+ const literal = primitiveValue(value)
26
+ return literal ? { operation: "set", state, ...literal } : undefined
27
+ }
28
+ return undefined
29
+ }
30
+ }
31
+
32
+ function primitiveValue(node) {
33
+ if (ts.isStringLiteral(node)) return { value: node.text }
34
+ if (ts.isNumericLiteral(node)) return signedNumber(node)
35
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return { value: true }
36
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return { value: false }
37
+ if (node.kind === ts.SyntaxKind.NullKeyword) return { value: null }
38
+ return signedNumber(node.operand, node.operator === ts.SyntaxKind.MinusToken ? "negative" : "positive")
39
+ }
40
+
41
+ function signedNumber(node, syntax) {
42
+ const number = Number(node.text)
43
+ if (!Number.isFinite(number)) return undefined
44
+ if (syntax === "negative") return number === 0 ? { value: 0, syntax } : { value: -number }
45
+ return syntax ? { value: number, syntax } : { value: number }
46
+ }
@@ -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) {