@kudzujs/core 0.8.14 → 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.
Files changed (32) hide show
  1. package/MIGRATION_ROADMAP.md +247 -0
  2. package/PERFORMANCE.md +212 -0
  3. package/README.md +34 -6
  4. package/RELEASES.md +64 -0
  5. package/docs/next-architecture/README.md +42 -0
  6. package/docs/next-architecture/compiler-current-architecture.md +71 -0
  7. package/docs/next-architecture/goal-a-compiler-foundation.md +152 -0
  8. package/docs/next-architecture/goal-b-optimization-benchmarks.md +59 -0
  9. package/docs/next-architecture/goal-c-state-resource-research.md +50 -0
  10. package/docs/next-architecture/goal-d-routing-compatibility-decisions.md +51 -0
  11. package/docs/next-architecture/performance-gates.md +50 -0
  12. package/docs/next-architecture/versioning.md +42 -0
  13. package/framework/README.md +23 -2
  14. package/framework/build.mjs +285 -3569
  15. package/framework/compiler/animation-frame-pass.mjs +103 -0
  16. package/framework/compiler/ast-helpers.mjs +181 -0
  17. package/framework/compiler/browser-signal-passes.mjs +182 -0
  18. package/framework/compiler/collection-analysis.mjs +187 -0
  19. package/framework/compiler/custom-hook-timer-pass.mjs +126 -0
  20. package/framework/compiler/descriptor-session.mjs +222 -0
  21. package/framework/compiler/effect-codegen.mjs +884 -0
  22. package/framework/compiler/event-command-pass.mjs +35 -0
  23. package/framework/compiler/handler-codegen.mjs +296 -0
  24. package/framework/compiler/normalization-pipeline.mjs +9 -0
  25. package/framework/compiler/react-migration-pass.mjs +339 -0
  26. package/framework/compiler/render-control-pass.mjs +96 -0
  27. package/framework/compiler/route-capability-planner.mjs +118 -0
  28. package/framework/compiler/router-pass.mjs +245 -0
  29. package/framework/compiler/worker-compiler.mjs +163 -0
  30. package/framework/compiler/zustand-pass.mjs +95 -0
  31. package/framework/dev-server.mjs +244 -0
  32. package/package.json +4 -1
@@ -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,126 @@
1
+ import { createHash } from "node:crypto"
2
+ import ts from "typescript"
3
+ import { effectReturns, nearestFunction, referencesIdentifier, sourceNodeError, unwrapExpression } from "./ast-helpers.mjs"
4
+
5
+ const customHookTimerStatePrefix = "__kTimerState_"
6
+ const customHookTimerSetterPrefix = "__kSetTimerState_"
7
+
8
+ export function normalizeCustomHookTimerRefs(sourceFile, factory, context) {
9
+ const timerCall = (node, name) => ts.isCallExpression(node) && (
10
+ ts.isIdentifier(node.expression) && node.expression.text === name ||
11
+ ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "window" && node.expression.name.text === name
12
+ )
13
+ const currentAccess = (node, name) => ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name && node.name.text === "current"
14
+ const clearStatement = (node, name) => {
15
+ if (!ts.isIfStatement(node) || node.elseStatement || !currentAccess(unwrapExpression(node.expression), name)) return undefined
16
+ const statement = ts.isBlock(node.thenStatement) && node.thenStatement.statements.length === 1 ? node.thenStatement.statements[0] : node.thenStatement
17
+ if (!ts.isExpressionStatement(statement) || !timerCall(statement.expression, "clearTimeout") || statement.expression.arguments.length !== 1 || !currentAccess(unwrapExpression(statement.expression.arguments[0]), name)) return undefined
18
+ return { condition: unwrapExpression(node.expression), argument: unwrapExpression(statement.expression.arguments[0]) }
19
+ }
20
+ const analyze = (hook, hookName) => {
21
+ if (!/^use[A-Z]/.test(hookName) || hook.parameters.length || !hook.body || !ts.isBlock(hook.body)) return undefined
22
+ const returnedStatement = hook.body.statements.at(-1)
23
+ const returned = returnedStatement && ts.isReturnStatement(returnedStatement) && returnedStatement.expression ? unwrapExpression(returnedStatement.expression) : undefined
24
+ if (!returned || !ts.isObjectLiteralExpression(returned)) return undefined
25
+ const returnedNames = new Set(returned.properties.filter(ts.isShorthandPropertyAssignment).map(property => property.name.text))
26
+ const callbacks = new Map()
27
+ const refs = []
28
+ for (const statement of hook.body.statements) {
29
+ if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) continue
30
+ for (const declaration of statement.declarationList.declarations) {
31
+ if (ts.isIdentifier(declaration.name) && declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) callbacks.set(declaration.name.text, declaration.initializer)
32
+ if (ts.isIdentifier(declaration.name) && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef" && declaration.initializer.arguments.length === 1 && declaration.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword) refs.push({ declaration, name: declaration.name.text })
33
+ }
34
+ }
35
+ const candidates = []
36
+ for (const ref of refs) {
37
+ const assignments = []
38
+ const accesses = []
39
+ const clearStatements = []
40
+ const collect = node => {
41
+ if (currentAccess(node, ref.name)) accesses.push(node)
42
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && currentAccess(unwrapExpression(node.left), ref.name)) assignments.push(node)
43
+ const clear = clearStatement(node, ref.name)
44
+ if (clear) clearStatements.push({ node, ...clear })
45
+ ts.forEachChild(node, collect)
46
+ }
47
+ collect(hook.body)
48
+ if (assignments.some(assignment => timerCall(unwrapExpression(assignment.right), "setTimeout"))) candidates.push({ ...ref, assignments, accesses, clearStatements })
49
+ }
50
+ if (!candidates.length) return undefined
51
+ if (candidates.length !== 1) throw sourceNodeError(hook, sourceFile, "Relative custom hooks may own only one private timeout ref")
52
+ const timer = candidates[0]
53
+ if (timer.assignments.length !== 1) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout refs require one direct timer.current = setTimeout(...) assignment")
54
+ const assignment = timer.assignments[0]
55
+ const timeout = unwrapExpression(assignment.right)
56
+ const timeoutCallback = timeout.arguments[0]
57
+ const delay = timeout.arguments[1]
58
+ if (!timerCall(timeout, "setTimeout") || timeout.arguments.length !== 2 || !timeoutCallback || !(ts.isArrowFunction(timeoutCallback) || ts.isFunctionExpression(timeoutCallback)) || timeoutCallback.parameters.length || !delay || !ts.isNumericLiteral(unwrapExpression(delay))) throw sourceNodeError(assignment, sourceFile, "Private timeout refs require setTimeout() with one zero-argument callback and a numeric literal delay")
59
+ const callback = nearestFunction(assignment)
60
+ const callbackName = [...callbacks].find(([, value]) => value === callback)?.[0]
61
+ if (!callbackName || !returnedNames.has(callbackName) || !ts.isBlock(callback.body) || !ts.isExpressionStatement(assignment.parent) || assignment.parent.parent !== callback.body) throw sourceNodeError(assignment, sourceFile, "Private timeout refs must be assigned directly inside one returned custom-hook callback")
62
+ const callbackClear = timer.clearStatements.find(entry => nearestFunction(entry.node) === callback)
63
+ if (!callbackClear || callbackClear.node.parent !== callback.body || callback.body.statements.indexOf(callbackClear.node) >= callback.body.statements.indexOf(assignment.parent)) throw sourceNodeError(callback, sourceFile, "Private timeout callbacks must directly clear the previous timer before assigning its replacement")
64
+ const effectCalls = hook.body.statements.flatMap(statement => {
65
+ if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression) || !ts.isIdentifier(statement.expression.expression) || statement.expression.expression.text !== "useEffect") return []
66
+ return [statement.expression]
67
+ })
68
+ let cleanupClear
69
+ for (const effect of effectCalls) {
70
+ const [setup, dependencies] = effect.arguments
71
+ if (!(ts.isArrowFunction(setup) || ts.isFunctionExpression(setup)) || !ts.isArrayLiteralExpression(dependencies) || dependencies.elements.length) continue
72
+ const returns = effectReturns(setup)
73
+ if (returns.cleanups.length !== 1) continue
74
+ const cleanup = returns.cleanups[0]
75
+ const entry = timer.clearStatements.find(candidate => nearestFunction(candidate.node) === cleanup)
76
+ if (entry && ts.isBlock(cleanup.body) && cleanup.body.statements.length === 1 && cleanup.body.statements[0] === entry.node) cleanupClear = entry
77
+ }
78
+ if (!cleanupClear) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout refs require one empty-dependency effect that directly clears the timer on cleanup")
79
+ const accepted = new Set([assignment.left, callbackClear.condition, callbackClear.argument, cleanupClear.condition, cleanupClear.argument].map(unwrapExpression))
80
+ const unsupported = timer.accesses.find(access => !accepted.has(access))
81
+ if (unsupported) throw sourceNodeError(unsupported, sourceFile, "Private timeout refs may only be read by their direct replacement and cleanup guards")
82
+ const identity = createHash("sha256").update(`${sourceFile.fileName}:${hook.pos}:${timer.name}`).digest("hex").slice(0, 10)
83
+ const stateName = `${customHookTimerStatePrefix}${identity}`
84
+ const setterName = `${customHookTimerSetterPrefix}${identity}`
85
+ if (referencesIdentifier(hook.body, stateName) || referencesIdentifier(hook.body, setterName)) throw sourceNodeError(timer.declaration, sourceFile, "Private timeout ref conflicts with compiler-owned bindings")
86
+ return { assignment, declaration: timer.declaration, refName: timer.name, returned, stateName, setterName }
87
+ }
88
+ const timerStates = new Set()
89
+ const transform = (hook, hookName) => {
90
+ const timer = analyze(hook, hookName)
91
+ if (!timer) return undefined
92
+ timerStates.add(timer.stateName)
93
+ const timerVisitor = current => {
94
+ if (current === timer.declaration) {
95
+ const binding = factory.createArrayBindingPattern([
96
+ factory.createBindingElement(undefined, undefined, timer.stateName),
97
+ factory.createBindingElement(undefined, undefined, timer.setterName)
98
+ ])
99
+ const initializer = factory.updateCallExpression(current.initializer, factory.createIdentifier("useState"), current.initializer.typeArguments, current.initializer.arguments)
100
+ return factory.updateVariableDeclaration(current, binding, current.exclamationToken, undefined, initializer)
101
+ }
102
+ if (current === timer.assignment) return factory.createCallExpression(factory.createIdentifier(timer.setterName), undefined, [ts.visitNode(current.right, timerVisitor)])
103
+ if (currentAccess(current, timer.refName)) return factory.createIdentifier(timer.stateName)
104
+ if (current === timer.returned) return factory.updateObjectLiteralExpression(current, [
105
+ ...current.properties,
106
+ factory.createShorthandPropertyAssignment(timer.stateName),
107
+ factory.createShorthandPropertyAssignment(timer.setterName)
108
+ ])
109
+ return ts.visitEachChild(current, timerVisitor, context)
110
+ }
111
+ return ts.visitEachChild(hook, timerVisitor, context)
112
+ }
113
+ const visitor = node => {
114
+ if (ts.isFunctionDeclaration(node)) {
115
+ const hookName = node.name?.text ?? (node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) ? "useDefault" : "")
116
+ const transformed = transform(node, hookName)
117
+ if (transformed) return transformed
118
+ }
119
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
120
+ const transformed = transform(node.initializer, node.name.text)
121
+ if (transformed) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, transformed)
122
+ }
123
+ return ts.visitEachChild(node, visitor, context)
124
+ }
125
+ return { sourceFile: ts.visitNode(sourceFile, visitor), timerStates }
126
+ }
@@ -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
+ ])