@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.
@@ -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, workerReferences, factory, context) => {
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
- workerReferences.push({ root: target, placeholder })
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: ${relative(root, reference.root)}`)
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
 
@@ -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<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown>; lifetime?: "layout" | "route"; dependencies?: string[]; itemDependencies?: string[]; listState?: string; cleanup?: true; owner?: string; list?: true }>
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: Array<Record<string, unknown>>
185
+ lists: ListDescriptor[]
121
186
  }
122
187
  }>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.19",
3
+ "version": "0.8.21",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",