@kudzujs/core 0.8.19 → 0.8.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_ROADMAP.md +7 -0
- package/PERFORMANCE.md +66 -0
- package/README.md +1 -1
- package/RELEASES.md +67 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +6 -5
- package/docs/next-architecture/goal-a-compiler-foundation.md +5 -5
- package/docs/next-architecture/versioning.md +1 -1
- package/framework/README.md +4 -3
- package/framework/build.mjs +176 -160
- package/framework/compiler/descriptor-session.mjs +33 -16
- package/framework/compiler/effect-analysis.mjs +89 -0
- package/framework/compiler/ir/module-ir.mjs +13 -1
- package/framework/compiler/worker-compiler.mjs +12 -6
- package/framework/core.d.ts +67 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import ts from "typescript"
|
|
|
2
2
|
import { createComponentAnalysis } from "./analysis/component-analysis.mjs"
|
|
3
3
|
import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
|
|
4
4
|
import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
|
|
5
|
-
import { createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerModuleHandler } from "./ir/module-ir.mjs"
|
|
5
|
+
import { createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler } from "./ir/module-ir.mjs"
|
|
6
6
|
|
|
7
7
|
export function createSemanticArtifact(file) {
|
|
8
8
|
return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file) }
|
|
@@ -16,9 +16,9 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
16
16
|
const listExpressions = []
|
|
17
17
|
const clientModules = new Set()
|
|
18
18
|
|
|
19
|
-
function compileListExpression(read, expression, item, index, states = new Set()) {
|
|
19
|
+
function compileListExpression(read, expression, item, index, states = new Set(), keyedBlock) {
|
|
20
20
|
const exportName = `listExpression${listExpressions.length}`
|
|
21
|
-
listExpressions.push({ exportName, expression, item, index, states, role: "list-expression" })
|
|
21
|
+
listExpressions.push({ exportName, expression, item, index, states, role: "list-expression", keyedBlock })
|
|
22
22
|
const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
|
|
23
23
|
if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
24
24
|
return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
|
|
@@ -26,7 +26,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
26
26
|
|
|
27
27
|
function compileListConditional(entry) {
|
|
28
28
|
const exportName = `listExpression${listExpressions.length}`
|
|
29
|
-
listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, role: "list-conditional" })
|
|
29
|
+
listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, role: "list-conditional", keyedBlock: entry.keyedBlock })
|
|
30
30
|
const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
|
|
31
31
|
const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
|
|
32
32
|
return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
|
|
@@ -44,16 +44,16 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
44
44
|
const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), initial)
|
|
45
45
|
return entry.field
|
|
46
46
|
? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
|
|
47
|
-
: compileListExpression(read, expression, entry.item, entry.index, entry.states)
|
|
47
|
+
: compileListExpression(read, expression, entry.item, entry.index, entry.states, entry.keyedBlock)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function compileReactiveBinding(expression, { setters, importBindings = new Map() }) {
|
|
50
|
+
function compileReactiveBinding(expression, { setters, importBindings = new Map(), keyedBlock }) {
|
|
51
51
|
const parts = conditionalParts(expression)
|
|
52
52
|
const state = parts && directStateIdentifier(parts.condition, setters)
|
|
53
53
|
if (state && isPrimitiveLiteral(parts.truthy) && isPrimitiveLiteral(parts.falsy)) {
|
|
54
54
|
return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
|
|
55
55
|
}
|
|
56
|
-
return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings))
|
|
56
|
+
return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings, keyedBlock))
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function compileConditional(kind, expression, truthy, falsy, setters) {
|
|
@@ -64,14 +64,14 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
64
64
|
return factory.createCallExpression(factory.createIdentifier("__kConditional"), undefined, [factory.createStringLiteral(kind), initial, thunk(truthy), thunk(falsy), ...descriptor])
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function compileReactiveExpression(expression, setters, importBindings = new Map()) {
|
|
67
|
+
function compileReactiveExpression(expression, setters, importBindings = new Map(), keyedBlock) {
|
|
68
68
|
const usedStates = referencedStateNames(expression, setters)
|
|
69
69
|
const importedNames = referencedImportedBindings(expression, importBindings)
|
|
70
70
|
const imports = [...importedNames].map(name => importBindings.get(name))
|
|
71
71
|
registerClientImports(imports)
|
|
72
72
|
const captures = new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
|
|
73
73
|
const exportName = `binding${reactiveBindings.length}`
|
|
74
|
-
reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding" })
|
|
74
|
+
reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding", keyedBlock })
|
|
75
75
|
const states = [...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
|
|
76
76
|
const scope = [...captures].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
|
|
77
77
|
const stateNames = new Set(usedStates)
|
|
@@ -91,13 +91,13 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
91
91
|
]
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function compileEvent(expression, { owner = "module", stateOwners = new Map(), setters, reducers, functions, listItem, importBindings }) {
|
|
94
|
+
function compileEvent(expression, { owner = "module", stateOwners = new Map(), setters, reducers, functions, listItem, keyedBlock, importBindings }) {
|
|
95
95
|
if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
|
|
96
96
|
if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
|
|
97
|
-
const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner)
|
|
97
|
+
const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock)
|
|
98
98
|
if (optimized) return optimized
|
|
99
99
|
rejectWorkerConstructions(expression)
|
|
100
|
-
const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem })
|
|
100
|
+
const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem, keyedBlock })
|
|
101
101
|
return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
|
|
102
102
|
factory.createStringLiteral(handlerUrl), factory.createStringLiteral(descriptor.exportName), descriptor.states, descriptor.scope
|
|
103
103
|
])
|
|
@@ -107,7 +107,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
107
107
|
return compileNativeCallback(expression, { ...options, entries: effectHandlers, prefix: "effect", role: "effect" })
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
|
|
110
|
+
function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
|
|
111
111
|
const allCaptures = nativeCaptureNames(expression, setters)
|
|
112
112
|
const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
|
|
113
113
|
const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
|
|
@@ -120,7 +120,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
120
120
|
if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction)) usedStates.add(state)
|
|
121
121
|
}
|
|
122
122
|
const exportName = `${prefix}${entries.length}`
|
|
123
|
-
entries.push({ exportName, expression, captures, deferValues, imports, listItem, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates })
|
|
123
|
+
entries.push({ exportName, expression, captures, deferValues, imports, listItem, keyedBlock, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates })
|
|
124
124
|
const value = name => deferValues
|
|
125
125
|
? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
|
|
126
126
|
: factory.createIdentifier(name)
|
|
@@ -136,13 +136,14 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function compileOptimizedEvent(expression, setters, stateOwners, owner) {
|
|
139
|
+
function compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock) {
|
|
140
140
|
const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
|
|
141
141
|
const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters) : undefined)
|
|
142
142
|
if (!commands.length || commands.some(command => !command)) return undefined
|
|
143
143
|
const original = ts.getOriginalNode(expression)
|
|
144
144
|
const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
|
|
145
145
|
const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, owner: stateOwners.get(command.state) ?? owner })), source, owner)
|
|
146
|
+
if (keyedBlock !== undefined) handler.keyedBlock = keyedBlock
|
|
146
147
|
return generateCommandBehavior(moduleIR, handler, factory)
|
|
147
148
|
}
|
|
148
149
|
|
|
@@ -155,6 +156,14 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
155
156
|
return registerDerived(moduleIR, { kind, [kind]: normalized, states: [...states], ...(source(node) ? { source: source(node) } : {}) })
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
function registerKeyedBlockResult(descriptor) {
|
|
160
|
+
return registerKeyedBlock(moduleIR, descriptor)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function registerEffectResult(handler, descriptor) {
|
|
164
|
+
return registerEffect(moduleIR, { ...descriptor, setup: { exportName: handler.exportName } })
|
|
165
|
+
}
|
|
166
|
+
|
|
158
167
|
function finalize() {
|
|
159
168
|
const callbacks = [...nativeHandlers, ...effectHandlers]
|
|
160
169
|
for (const entry of callbacks) {
|
|
@@ -162,6 +171,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
162
171
|
const setters = Map.groupBy(entry.setters, ([, state]) => state)
|
|
163
172
|
registerModuleHandler(moduleIR, {
|
|
164
173
|
role: entry.role,
|
|
174
|
+
...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
|
|
165
175
|
exportName: entry.exportName,
|
|
166
176
|
async: Boolean(entry.expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)),
|
|
167
177
|
generator: Boolean(entry.expression.asteriskToken),
|
|
@@ -184,6 +194,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
184
194
|
}
|
|
185
195
|
for (const entry of reactiveBindings) registerBinding(moduleIR, {
|
|
186
196
|
role: entry.role,
|
|
197
|
+
...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
|
|
187
198
|
exportName: entry.exportName,
|
|
188
199
|
parameters: ["__k"],
|
|
189
200
|
states: [...entry.states],
|
|
@@ -194,6 +205,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
194
205
|
})
|
|
195
206
|
for (const entry of listExpressions) registerBinding(moduleIR, {
|
|
196
207
|
role: entry.role,
|
|
208
|
+
...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
|
|
197
209
|
exportName: entry.exportName,
|
|
198
210
|
parameters: [entry.item, entry.index ?? "__kIndex", "__k"],
|
|
199
211
|
states: [...(entry.states ?? [])],
|
|
@@ -202,6 +214,11 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
202
214
|
code: handlerLowering.lowerListExpression(entry),
|
|
203
215
|
...(source(entry.expression) ? { source: source(entry.expression) } : {})
|
|
204
216
|
})
|
|
217
|
+
for (const effect of moduleIR.effects) {
|
|
218
|
+
const handler = moduleIR.handlers.find(candidate => candidate.kind === "module-export" && candidate.role === "effect" && candidate.exportName === effect.setup.exportName)
|
|
219
|
+
if (!handler) throw new Error(`Effect handler ${JSON.stringify(effect.setup.exportName)} was not finalized`)
|
|
220
|
+
effect.setup = { handler: handler.slot }
|
|
221
|
+
}
|
|
205
222
|
const imports = [...callbacks, ...reactiveBindings].flatMap(entry => entry.imports ?? []).map(importRecord)
|
|
206
223
|
moduleIR.imports = [...new Map(imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry])).values()]
|
|
207
224
|
moduleIR.clientModules = [...clientModules]
|
|
@@ -215,7 +232,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
215
232
|
|
|
216
233
|
const importRecord = entry => ({ target: entry.target, kind: entry.kind, local: entry.local, ...(entry.imported ? { imported: entry.imported } : {}), package: Boolean(entry.package) })
|
|
217
234
|
|
|
218
|
-
return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult }
|
|
235
|
+
return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult }
|
|
219
236
|
}
|
|
220
237
|
|
|
221
238
|
function directStateIdentifier(expression, setters) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import ts from "typescript"
|
|
2
|
+
import { nearestFunction, referencesIdentifier, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
|
+
import { collectionExpression } from "./collection-analysis.mjs"
|
|
4
|
+
import { referencedStateNames } from "./descriptor-session.mjs"
|
|
5
|
+
|
|
6
|
+
export function analyzeEffectDependencies({ dependencies, node, listEffect, keyedItem, setters, localDeclarations, factory, fail }) {
|
|
7
|
+
const itemDependencies = []
|
|
8
|
+
const ordinaryDependencies = []
|
|
9
|
+
let dependencyItem = listEffect ? keyedItem : undefined
|
|
10
|
+
for (const dependency of dependencies.elements) {
|
|
11
|
+
const value = unwrapExpression(dependency)
|
|
12
|
+
if (!dependencyItem && ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && isDestructuredParameter(value.expression, nearestFunction(node))) dependencyItem = value.expression.text
|
|
13
|
+
const field = dependencyItem && directProperty(dependency, dependencyItem)
|
|
14
|
+
if (field) {
|
|
15
|
+
if (["__proto__", "constructor", "prototype"].includes(field)) fail(dependency, `useEffect() keyed item property "${field}" is not supported`)
|
|
16
|
+
itemDependencies.push(field)
|
|
17
|
+
} else if (dependencyItem && referencesIdentifier(dependency, dependencyItem)) {
|
|
18
|
+
fail(dependency, "useEffect() keyed item dependencies must be direct item.<field> properties")
|
|
19
|
+
} else {
|
|
20
|
+
ordinaryDependencies.push(dependency)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
|
|
24
|
+
if (invalidDependency) fail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
|
|
25
|
+
|
|
26
|
+
const entries = []
|
|
27
|
+
const dependencyStates = new Map()
|
|
28
|
+
const substitutions = new Map()
|
|
29
|
+
const subscriptions = []
|
|
30
|
+
let hasDerived = false
|
|
31
|
+
const stateNames = new Set(setters.values())
|
|
32
|
+
for (const dependency of ordinaryDependencies) {
|
|
33
|
+
const declarations = localDeclarations?.get(dependency.text)
|
|
34
|
+
const initializer = declarations?.length === 1 ? declarations[0].initializer : undefined
|
|
35
|
+
const directAlias = initializer && ts.isIdentifier(unwrapExpression(initializer)) && stateNames.has(unwrapExpression(initializer).text)
|
|
36
|
+
const derivedStates = initializer && !directAlias ? referencedStateNames(initializer, setters) : new Set()
|
|
37
|
+
if (derivedStates.size) {
|
|
38
|
+
const usedStates = new Set()
|
|
39
|
+
const expression = collectionExpression(initializer, { fail, stateNames, selectorStates: usedStates })
|
|
40
|
+
if (!usedStates.size) fail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
|
|
41
|
+
entries.push({ kind: "derived", name: dependency.text, expression, states: usedStates, source: initializer })
|
|
42
|
+
for (const name of usedStates) {
|
|
43
|
+
subscriptions.push(factory.createIdentifier(name))
|
|
44
|
+
dependencyStates.set(name, factory.createIdentifier(name))
|
|
45
|
+
}
|
|
46
|
+
substitutions.set(dependency.text, initializer)
|
|
47
|
+
hasDerived = true
|
|
48
|
+
} else {
|
|
49
|
+
subscriptions.push(dependency)
|
|
50
|
+
entries.push({ kind: "signal", name: dependency.text })
|
|
51
|
+
dependencyStates.set(dependency.text, dependency)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!hasDerived) dependencyStates.clear()
|
|
55
|
+
return { dependencyItem, itemDependencies, ordinaryDependencies, entries, dependencyStates, substitutions, subscriptions, hasDerived }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateEffectOwnedBrowserResources(callback, returns, fail) {
|
|
59
|
+
const observers = []
|
|
60
|
+
const frameAssignments = []
|
|
61
|
+
const cancellations = new Set()
|
|
62
|
+
const disconnected = new Set()
|
|
63
|
+
const insideCleanup = node => returns.cleanups.some(cleanup => {
|
|
64
|
+
for (let current = node; current; current = current.parent) if (current === cleanup) return true
|
|
65
|
+
return false
|
|
66
|
+
})
|
|
67
|
+
const visit = node => {
|
|
68
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(unwrapExpression(node.initializer)) && ts.isIdentifier(unwrapExpression(node.initializer).expression) && unwrapExpression(node.initializer).expression.text === "IntersectionObserver") observers.push(node)
|
|
69
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(unwrapExpression(node.left)) && ts.isCallExpression(unwrapExpression(node.right)) && ts.isIdentifier(unwrapExpression(node.right).expression) && unwrapExpression(node.right).expression.text === "requestAnimationFrame") frameAssignments.push(node)
|
|
70
|
+
if (insideCleanup(node) && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "cancelAnimationFrame" && node.arguments.length === 1 && ts.isIdentifier(unwrapExpression(node.arguments[0]))) cancellations.add(unwrapExpression(node.arguments[0]).text)
|
|
71
|
+
if (insideCleanup(node) && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.name.text === "disconnect" && node.arguments.length === 0) disconnected.add(node.expression.expression.text)
|
|
72
|
+
ts.forEachChild(node, visit)
|
|
73
|
+
}
|
|
74
|
+
visit(callback.body)
|
|
75
|
+
for (const observer of observers) if (!disconnected.has(observer.name.text)) fail(observer, `IntersectionObserver effects must disconnect ${JSON.stringify(observer.name.text)} in cleanup`)
|
|
76
|
+
for (const assignment of frameAssignments) {
|
|
77
|
+
const name = unwrapExpression(assignment.left).text
|
|
78
|
+
if (!cancellations.has(name)) fail(assignment, `Animation loop effects must cancel ${JSON.stringify(name)} in cleanup`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isDestructuredParameter(identifier, fn) {
|
|
83
|
+
return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function directProperty(expression, objectName) {
|
|
87
|
+
const value = unwrapExpression(expression)
|
|
88
|
+
return ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && value.expression.text === objectName ? value.name.text : undefined
|
|
89
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function createModuleIR(file) {
|
|
2
|
-
return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], imports: [], clientModules: [] }
|
|
2
|
+
return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], effects: [], keyedBlocks: [], imports: [], clientModules: [] }
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
|
|
@@ -39,3 +39,15 @@ export function registerDerived(moduleIR, descriptor) {
|
|
|
39
39
|
moduleIR.derived.push(derived)
|
|
40
40
|
return derived
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
export function registerEffect(moduleIR, descriptor) {
|
|
44
|
+
const effect = { slot: moduleIR.effects.length, ...descriptor }
|
|
45
|
+
moduleIR.effects.push(effect)
|
|
46
|
+
return effect
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerKeyedBlock(moduleIR, descriptor) {
|
|
50
|
+
const block = { slot: moduleIR.keyedBlocks.length, ...descriptor }
|
|
51
|
+
moduleIR.keyedBlocks.push(block)
|
|
52
|
+
return block
|
|
53
|
+
}
|
|
@@ -44,7 +44,8 @@ export function createWorkerCompiler({
|
|
|
44
44
|
return { worker, url, specifier: specifierNode.text, options }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const rewriteEffect = (callback, file, sourceFile, sourceFiles,
|
|
47
|
+
const rewriteEffect = (callback, file, sourceFile, sourceFiles, factory, context) => {
|
|
48
|
+
const workers = []
|
|
48
49
|
const visit = node => {
|
|
49
50
|
const value = candidate(node, sourceFile)
|
|
50
51
|
if (value) {
|
|
@@ -56,12 +57,17 @@ export function createWorkerCompiler({
|
|
|
56
57
|
if (!sourceFiles.has(target)) throw sourceNodeError(url.arguments[0], sourceFile, `Relative TypeScript Worker ${JSON.stringify(specifier)} must resolve to an existing .worker.ts file under src/`)
|
|
57
58
|
const identity = `${sourceRelative.replaceAll(sep, "/")}:${ts.getOriginalNode(node).getStart(sourceFile)}`
|
|
58
59
|
const placeholder = `/__kudzu_worker_${createHash("sha256").update(identity).digest("hex").slice(0, 16)}__.js`
|
|
59
|
-
|
|
60
|
+
const original = ts.getOriginalNode(node)
|
|
61
|
+
workers.push({
|
|
62
|
+
root: sourceRelative.replaceAll(sep, "/"),
|
|
63
|
+
placeholder,
|
|
64
|
+
source: { file: relative(root, sourceFile.fileName).replaceAll(sep, "/"), start: original.getStart(sourceFile), end: original.end }
|
|
65
|
+
})
|
|
60
66
|
return factory.updateNewExpression(worker, worker.expression, worker.typeArguments, [factory.createStringLiteral(placeholder), options])
|
|
61
67
|
}
|
|
62
68
|
return ts.visitEachChild(node, visit, context)
|
|
63
69
|
}
|
|
64
|
-
return ts.visitEachChild(callback, visit, context)
|
|
70
|
+
return { callback: ts.visitEachChild(callback, visit, context), workers }
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const rejectConstructions = (expression, sourceFile, message) => {
|
|
@@ -124,7 +130,7 @@ export function createWorkerCompiler({
|
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
const emit = async (references, sourceFiles, assetsDirectory, base, minify) => {
|
|
127
|
-
const roots = [...new Set(references.map(reference => reference.root))].sort()
|
|
133
|
+
const roots = [...new Set(references.map(reference => resolve(sourceDirectory, reference.root)))].sort()
|
|
128
134
|
if (!roots.length) return new Map()
|
|
129
135
|
await validateGraphs(roots, sourceFiles)
|
|
130
136
|
const workerDirectory = resolve(assetsDirectory, "workers")
|
|
@@ -150,12 +156,12 @@ export function createWorkerCompiler({
|
|
|
150
156
|
for (const [output, metadata] of Object.entries(result.metafile.outputs)) {
|
|
151
157
|
if (!metadata.entryPoint) continue
|
|
152
158
|
const entry = resolve(root, metadata.entryPoint)
|
|
153
|
-
const rootReferences = references.filter(reference => reference.root === entry)
|
|
159
|
+
const rootReferences = references.filter(reference => resolve(sourceDirectory, reference.root) === entry)
|
|
154
160
|
const outputFile = resolve(root, output)
|
|
155
161
|
const url = assetPath(base, relative(outputDirectory, outputFile).replaceAll(sep, "/"))
|
|
156
162
|
for (const reference of rootReferences) emitted.set(reference.placeholder, url)
|
|
157
163
|
}
|
|
158
|
-
for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${
|
|
164
|
+
for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${reference.root}`)
|
|
159
165
|
return emitted
|
|
160
166
|
}
|
|
161
167
|
|
package/framework/core.d.ts
CHANGED
|
@@ -80,6 +80,57 @@ export type MetadataContext<Props = Record<string, unknown>> = {
|
|
|
80
80
|
props: Props
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export type ListSourceDescriptor =
|
|
84
|
+
| { state: string; truthy?: unknown; falsy?: unknown }
|
|
85
|
+
| {
|
|
86
|
+
module: string
|
|
87
|
+
handler: string
|
|
88
|
+
states: Record<string, string>
|
|
89
|
+
scope: Record<string, unknown>
|
|
90
|
+
scopeStates: Record<string, string>
|
|
91
|
+
scopeBindings: Record<string, unknown>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type ChildListDescriptor = {
|
|
95
|
+
id: string
|
|
96
|
+
field: string
|
|
97
|
+
key: string | null
|
|
98
|
+
selector?: unknown[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type ListDescriptor = {
|
|
102
|
+
id: string
|
|
103
|
+
state: string
|
|
104
|
+
key: string | null
|
|
105
|
+
keys: Array<string | number>
|
|
106
|
+
svg?: true
|
|
107
|
+
static?: true
|
|
108
|
+
source?: ListSourceDescriptor
|
|
109
|
+
ownerField?: string
|
|
110
|
+
selector?: unknown[]
|
|
111
|
+
selectorStates?: Record<string, string>
|
|
112
|
+
indexed?: true
|
|
113
|
+
reducer?: true
|
|
114
|
+
children?: ChildListDescriptor[]
|
|
115
|
+
mount?: true
|
|
116
|
+
nested?: true
|
|
117
|
+
effects?: true
|
|
118
|
+
conditions?: true
|
|
119
|
+
conditionHandlers?: true
|
|
120
|
+
textRanges?: true
|
|
121
|
+
attributes?: true
|
|
122
|
+
events?: true
|
|
123
|
+
expressions?: true
|
|
124
|
+
expressionAttributes?: true
|
|
125
|
+
expressionStates?: string[]
|
|
126
|
+
rowStates?: Array<{ id: string; initialValue: unknown }>
|
|
127
|
+
rowConditions?: string[]
|
|
128
|
+
rowRefs?: string[]
|
|
129
|
+
fastRelease?: true
|
|
130
|
+
seed?: Record<string, string | number | boolean | null>
|
|
131
|
+
valueSeed?: Record<string, string | number | boolean | null>
|
|
132
|
+
}
|
|
133
|
+
|
|
83
134
|
export function renderPage<Props = Record<string, never>>(
|
|
84
135
|
component: (props: Props) => unknown | Promise<unknown>,
|
|
85
136
|
metadata?: PageMetadata,
|
|
@@ -105,7 +156,21 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
105
156
|
commands?: Array<[string, string, unknown]>
|
|
106
157
|
native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
|
|
107
158
|
}>
|
|
108
|
-
effects: Array<{
|
|
159
|
+
effects: Array<{
|
|
160
|
+
module: string
|
|
161
|
+
handler: string
|
|
162
|
+
states: Record<string, string>
|
|
163
|
+
scope: Record<string, unknown>
|
|
164
|
+
lifetime?: "layout" | "route"
|
|
165
|
+
dependencies?: string[]
|
|
166
|
+
dependencyExpressions?: unknown[]
|
|
167
|
+
dependencyStates?: Record<string, string>
|
|
168
|
+
itemDependencies?: string[]
|
|
169
|
+
listState?: string
|
|
170
|
+
cleanup?: true
|
|
171
|
+
owner?: string
|
|
172
|
+
list?: true
|
|
173
|
+
}>
|
|
109
174
|
bindings: Array<{
|
|
110
175
|
target: string
|
|
111
176
|
state?: string
|
|
@@ -117,6 +182,6 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
117
182
|
scopeBindings?: Record<string, unknown>
|
|
118
183
|
}>
|
|
119
184
|
conditions: Array<Record<string, unknown>>
|
|
120
|
-
lists:
|
|
185
|
+
lists: ListDescriptor[]
|
|
121
186
|
}
|
|
122
187
|
}>
|