@kudzujs/core 0.8.36 → 0.8.38

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.
@@ -3,23 +3,45 @@ import { createComponentAnalysis } from "./analysis/component-analysis.mjs"
3
3
  import { knownGlobalNames } from "./analysis/binding-index.mjs"
4
4
  import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
5
5
  import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
6
- import { assertModuleIRReferences, createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler } from "./ir/module-ir.mjs"
6
+ import { assertModuleIRReferences, createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler, registerSignal } from "./ir/module-ir.mjs"
7
7
 
8
8
  export function createSemanticArtifact(file) {
9
9
  return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file) }
10
10
  }
11
11
 
12
- export function createDescriptorSession({ semantic, handlerUrl, factory, context, bindingIndex, compileEventCommand, handlerLowering, isPrimitiveLiteral, rejectWorkerConstructions, sourceName = source => source.fileName }) {
12
+ export function createDescriptorSession({ semantic, handlerUrl, factory, context, bindingIndex, compileEventCommand, handlerLowering, isPrimitiveLiteral, rejectWorkerConstructions, stateReferences = () => new Map(), symbolReference, sourceName = source => source.fileName }) {
13
13
  const { moduleIR } = semantic
14
+ moduleIR.symbols = bindingIndex.bindings()
14
15
  const nativeHandlers = []
15
16
  const effectHandlers = []
16
17
  const reactiveBindings = []
17
18
  const listExpressions = []
19
+ const pendingEffects = []
18
20
  const clientModules = new Set()
19
21
 
22
+ const signal = (name, node, references, aliases = []) => {
23
+ let reference = references?.get(name) ?? stateReferences(node).get(name)
24
+ if (!reference && bindingIndex) {
25
+ const original = ts.getOriginalNode(node)
26
+ const names = new Set([name, ...aliases])
27
+ let symbol = bindingIndex.references(original, original)?.find(entry => names.has(entry.debugName))?.slot
28
+ if (symbol === undefined) {
29
+ for (let current = node; current && symbol === undefined; current = current.parent) if (isFunctionLike(current)) {
30
+ const parameter = current.parameters.map(parameter => bindingIdentifier(parameter.name, names)).find(Boolean)
31
+ symbol = parameter && bindingIndex.resolveBinding(parameter)?.slot
32
+ break
33
+ }
34
+ }
35
+ if (symbol !== undefined) reference = { kind: "symbol", symbol }
36
+ }
37
+ reference ??= symbolReference?.(name, node, aliases)
38
+ if (!reference) throw new Error(`ModuleIR state ${JSON.stringify(name)} has no resolved StateRef`)
39
+ return registerSignal(moduleIR, reference, name).slot
40
+ }
41
+
20
42
  function compileListExpression(read, expression, item, index, states = new Set(), keyedBlock, indexedBindingIndex) {
21
43
  const exportName = `listExpression${listExpressions.length}`
22
- listExpressions.push({ exportName, expression, item, index, states, role: "list-expression", keyedBlock, bindingIndex: indexedBindingIndex })
44
+ listExpressions.push({ exportName, expression, item, index, states, signalRefs: new Map([...states].map(name => [name, signal(name, expression)])), role: "list-expression", keyedBlock, bindingIndex: indexedBindingIndex })
23
45
  const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
24
46
  if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
25
47
  return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
@@ -28,7 +50,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
28
50
  function compileListConditional(entry) {
29
51
  const exportName = `listExpression${listExpressions.length}`
30
52
  const indexed = indexedReferences(bindingIndex, entry.condition, entry.condition)
31
- listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, role: "list-conditional", keyedBlock: entry.keyedBlock, bindingIndex: indexed ? bindingIndex : undefined })
53
+ listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, states: new Set(), signalRefs: new Map(), role: "list-conditional", keyedBlock: entry.keyedBlock, bindingIndex: indexed ? bindingIndex : undefined })
32
54
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
33
55
  const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
34
56
  return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
@@ -57,9 +79,10 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
57
79
  const parts = conditionalParts(expression)
58
80
  const state = parts && directStateIdentifier(parts.condition, setters, bindingIndex)
59
81
  if (state && isPrimitiveLiteral(parts.truthy) && isPrimitiveLiteral(parts.falsy)) {
60
- return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
82
+ return { node: factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy]) }
61
83
  }
62
- return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings, keyedBlock))
84
+ const binding = reactiveBindings.length
85
+ return { node: factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings, keyedBlock)), binding }
63
86
  }
64
87
 
65
88
  function compileConditional(kind, expression, truthy, falsy, setters) {
@@ -81,7 +104,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
81
104
  ? new Set(indexed.filter(reference => ["capture", "unresolved"].includes(reference.kind) && !setters.has(reference.debugName) && !allStateNames.has(reference.debugName) && !importedNames.has(reference.debugName)).map(reference => reference.debugName))
82
105
  : new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
83
106
  const exportName = `binding${reactiveBindings.length}`
84
- reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding", keyedBlock, ...(indexed ? { bindingIndex } : {}) })
107
+ reactiveBindings.push({ slot: reactiveBindings.length, exportName, expression, captures, states: usedStates, signalRefs: new Map([...usedStates].map(name => [name, signal(name, expression)])), imports, role: "binding", keyedBlock, ...(indexed ? { bindingIndex } : {}) })
85
108
  const states = [...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
86
109
  const scope = [...captures].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
87
110
  const stateNames = new Set(usedStates)
@@ -107,7 +130,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
107
130
  const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock)
108
131
  if (optimized) return optimized
109
132
  rejectWorkerConstructions(expression)
110
- const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem, keyedBlock })
133
+ const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem, keyedBlock, stateOwners })
111
134
  return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
112
135
  factory.createStringLiteral(handlerUrl), factory.createStringLiteral(descriptor.exportName), descriptor.states, descriptor.scope
113
136
  ])
@@ -117,7 +140,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
117
140
  return compileNativeCallback(expression, { ...options, entries: effectHandlers, prefix: "effect", role: "effect" })
118
141
  }
119
142
 
120
- function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
143
+ function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, stateOwners, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
121
144
  const indexedBindingIndex = indexedReferences(bindingIndex, expression, expression) ? bindingIndex : undefined
122
145
  const allCaptures = nativeCaptureNames(expression, setters, indexedBindingIndex)
123
146
  const usedReducers = referencedReducerDispatches(expression.body, reducers, expression, indexedBindingIndex)
@@ -131,11 +154,13 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
131
154
  if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction, bindingIndex)) usedStates.add(state)
132
155
  }
133
156
  const exportName = `${prefix}${entries.length}`
134
- 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, bindingIndex: indexedBindingIndex })
157
+ const entry = { 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, signalRefs: new Map([...usedStates].map(name => [name, signal(name, expression, stateOwners, [...setters].filter(([, state]) => state === name).map(([setter]) => setter))])), bindingIndex: indexedBindingIndex }
158
+ entries.push(entry)
135
159
  const value = name => deferValues
136
160
  ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
137
161
  : factory.createIdentifier(name)
138
162
  return {
163
+ entry,
139
164
  exportName,
140
165
  states: factory.createArrayLiteralExpression([...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), value(name)]))),
141
166
  scope: factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
@@ -154,7 +179,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
154
179
  if (!commands?.length || commands.some(command => !command)) return undefined
155
180
  const original = ts.getOriginalNode(expression)
156
181
  const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
157
- const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, owner: stateOwners.get(command.state) ?? owner })), source, owner)
182
+ const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, reference: stateOwners.get(command.state) ?? stateReferences(expression).get(command.state) })), source)
158
183
  if (keyedBlock !== undefined) handler.keyedBlock = keyedBlock
159
184
  return generateCommandBehavior(moduleIR, handler, factory)
160
185
  }
@@ -176,7 +201,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
176
201
 
177
202
  function registerDerivedResult(kind, value, states = [], node) {
178
203
  const normalized = JSON.parse(JSON.stringify(value))
179
- return registerDerived(moduleIR, { kind, [kind]: normalized, states: [...states], ...(source(node) ? { source: source(node) } : {}) })
204
+ return registerDerived(moduleIR, { kind, [kind]: normalized, signals: [...states].map(name => signal(name, node)), ...(source(node) ? { source: source(node) } : {}) })
180
205
  }
181
206
 
182
207
  function registerKeyedBlockResult(descriptor) {
@@ -184,68 +209,79 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
184
209
  }
185
210
 
186
211
  function registerEffectResult(handler, descriptor) {
187
- return registerEffect(moduleIR, { ...descriptor, setup: { exportName: handler.exportName } })
212
+ const effect = { ...descriptor, setup: handler }
213
+ pendingEffects.push(effect)
214
+ return effect
188
215
  }
189
216
 
190
217
  function finalize() {
191
218
  const callbacks = [...nativeHandlers, ...effectHandlers]
219
+ const imports = [...callbacks, ...reactiveBindings].flatMap(entry => entry.imports ?? []).map(importRecord)
220
+ moduleIR.imports = [...new Map(imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry])).values()].map((entry, slot) => ({ slot, ...entry }))
221
+ const importSlots = new Map(moduleIR.imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry.slot]))
222
+ const importSlot = entry => importSlots.get(`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`)
192
223
  for (const entry of callbacks) {
193
224
  const lowered = handlerLowering.lowerNativeHandler(entry)
194
225
  const setters = Map.groupBy(entry.setters, ([, state]) => state)
195
- registerModuleHandler(moduleIR, {
226
+ entry.handler = registerModuleHandler(moduleIR, {
196
227
  role: entry.role,
197
228
  ...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
198
229
  exportName: entry.exportName,
199
230
  async: Boolean(entry.expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)),
200
231
  generator: Boolean(entry.expression.asteriskToken),
201
232
  signals: [...entry.usedStates].map(name => ({
233
+ signal: entry.signalRefs.get(name),
202
234
  name,
203
235
  setters: (setters.get(name) ?? []).map(([setter]) => setter),
204
236
  value: entry.deferValues ? "deferred" : "direct",
205
237
  snapshot: lowered.stateSnapshots.includes(name)
206
238
  })),
207
239
  captures: [...entry.captures].map(name => ({
240
+ ...(symbolSlot(entry, name) !== undefined ? { symbol: symbolSlot(entry, name) } : {}),
208
241
  name,
209
242
  source: name === (typeof entry.listItem === "string" ? entry.listItem : entry.listItem?.item) ? "list-item" : name === entry.listItem?.index ? "list-index" : "scope",
210
243
  value: entry.deferValues ? "deferred" : "direct",
211
244
  snapshot: lowered.captureSnapshots.includes(name)
212
245
  })),
213
- imports: entry.imports.map(importRecord),
246
+ imports: entry.imports.map(entry => importSlot(importRecord(entry))),
214
247
  code: lowered.code,
215
248
  ...(source(entry.expression) ? { source: source(entry.expression) } : {})
216
249
  })
217
250
  }
218
251
  for (const entry of reactiveBindings) registerBinding(moduleIR, {
252
+ slot: entry.slot,
219
253
  role: entry.role,
220
254
  ...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
221
255
  exportName: entry.exportName,
222
256
  parameters: ["__k"],
223
- states: [...entry.states],
224
- captures: [...entry.captures].map(name => ({ name, source: "scope" })),
225
- imports: entry.imports.map(importRecord),
257
+ signals: [...entry.states].map(name => entry.signalRefs.get(name)),
258
+ captures: [...entry.captures].map(name => ({ ...(symbolSlot(entry, name) !== undefined ? { symbol: symbolSlot(entry, name) } : {}), name, source: "scope" })),
259
+ imports: entry.imports.map(entry => importSlot(importRecord(entry))),
226
260
  code: handlerLowering.lowerReactiveBinding(entry),
227
261
  ...(source(entry.expression) ? { source: source(entry.expression) } : {})
228
262
  })
229
- for (const entry of listExpressions) registerBinding(moduleIR, {
263
+ for (const [index, entry] of listExpressions.entries()) registerBinding(moduleIR, {
264
+ slot: reactiveBindings.length + index,
230
265
  role: entry.role,
231
266
  ...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
232
267
  exportName: entry.exportName,
233
268
  parameters: [entry.item, entry.index ?? "__kIndex", "__k"],
234
- states: [...(entry.states ?? [])],
269
+ signals: [...(entry.states ?? [])].map(name => entry.signalRefs.get(name)),
235
270
  captures: [],
236
271
  imports: [],
237
272
  code: handlerLowering.lowerListExpression(entry),
238
273
  ...(source(entry.expression) ? { source: source(entry.expression) } : {})
239
274
  })
240
- for (const effect of moduleIR.effects) {
241
- const handler = moduleIR.handlers.find(candidate => candidate.kind === "module-export" && candidate.role === "effect" && candidate.exportName === effect.setup.exportName)
242
- if (!handler) throw new Error(`Effect handler ${JSON.stringify(effect.setup.exportName)} was not finalized`)
243
- effect.setup = { handler: handler.slot }
275
+ for (const effect of pendingEffects) {
276
+ if (!effect.setup.entry.handler) throw new Error(`Effect handler ${JSON.stringify(effect.setup.exportName)} was not finalized`)
277
+ registerEffect(moduleIR, { ...effect, setup: { handler: effect.setup.entry.handler.slot } })
244
278
  }
245
- const imports = [...callbacks, ...reactiveBindings].flatMap(entry => entry.imports ?? []).map(importRecord)
246
- moduleIR.imports = [...new Map(imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry])).values()]
247
279
  moduleIR.clientModules = [...clientModules]
248
- assertModuleIRReferences(moduleIR)
280
+ assertModuleIRReferences(moduleIR, semantic.componentAnalysis)
281
+ }
282
+
283
+ function symbolSlot(entry, name) {
284
+ return entry.bindingIndex?.references(entry.expression, entry.expression)?.find(reference => reference.debugName === name)?.slot
249
285
  }
250
286
 
251
287
  function source(node) {
@@ -256,7 +292,15 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
256
292
 
257
293
  const importRecord = entry => ({ target: entry.target, kind: entry.kind, local: entry.local, ...(entry.imported ? { imported: entry.imported } : {}), package: Boolean(entry.package) })
258
294
 
259
- return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult }
295
+ return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult, signal }
296
+ }
297
+
298
+ function bindingIdentifier(name, names) {
299
+ if (ts.isIdentifier(name)) return names.has(name.text) ? name : undefined
300
+ for (const element of name.elements) if (ts.isBindingElement(element)) {
301
+ const identifier = bindingIdentifier(element.name, names)
302
+ if (identifier) return identifier
303
+ }
260
304
  }
261
305
 
262
306
  function directStateIdentifier(expression, setters, bindingIndex) {
@@ -1,48 +1,159 @@
1
1
  export function createModuleIR(file) {
2
- return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], effects: [], keyedBlocks: [], imports: [], clientModules: [] }
2
+ return { version: 2, file, symbols: [], signals: [], handlers: [], bindings: [], derived: [], effects: [], keyedBlocks: [], imports: [], clientModules: [] }
3
3
  }
4
4
 
5
- export function assertModuleIRReferences(moduleIR) {
6
- const slot = (records, value, label) => {
7
- if (!Number.isInteger(value) || value < 0 || value >= records.length) throw new Error(`ModuleIR ${label} references missing slot ${JSON.stringify(value)}`)
5
+ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
6
+ if (moduleIR?.version !== 2) throw new Error(`Unsupported ModuleIR version: ${JSON.stringify(moduleIR?.version)}`)
7
+ if (componentAnalysis && componentAnalysis.version !== 2) throw new Error(`Unsupported ComponentAnalysis version: ${JSON.stringify(componentAnalysis.version)}`)
8
+ const slot = (records, value, label, kind) => {
9
+ if (!Number.isInteger(value) || value < 0 || value >= records.length) throw new Error(`${label} references missing ${kind} slot ${JSON.stringify(value)}`)
10
+ return records[value]
8
11
  }
9
- for (const [name, records] of [["signal", moduleIR.signals], ["handler", moduleIR.handlers], ["binding", moduleIR.bindings], ["derived", moduleIR.derived], ["effect", moduleIR.effects], ["keyed block", moduleIR.keyedBlocks]]) {
12
+ const indexed = (name, records) => {
10
13
  records.forEach((record, index) => {
11
- if (record.slot !== index) throw new Error(`ModuleIR ${name} slot ${JSON.stringify(record.slot)} must equal its index ${index}`)
14
+ if (record.slot !== index) throw new Error(`${name} slot ${JSON.stringify(record.slot)} must equal its index ${index}`)
12
15
  })
13
16
  }
17
+ indexed("SymbolRef", moduleIR.symbols)
18
+ indexed("SignalIR", moduleIR.signals)
19
+ indexed("HandlerIR", moduleIR.handlers)
20
+ indexed("BindingIR", moduleIR.bindings)
21
+ indexed("DerivedIR", moduleIR.derived)
22
+ indexed("EffectIR", moduleIR.effects)
23
+ indexed("KeyedBlockIR", moduleIR.keyedBlocks)
24
+ indexed("ImportIR", moduleIR.imports)
25
+ if (componentAnalysis) {
26
+ indexed("Component owner", componentAnalysis.owners)
27
+ indexed("Component specialization", componentAnalysis.specializations)
28
+ for (const owner of componentAnalysis.owners) {
29
+ indexed(`Component owner ${owner.slot} state`, owner.states)
30
+ indexed(`Component owner ${owner.slot} ref`, owner.refs)
31
+ indexed(`Component owner ${owner.slot} ID`, owner.ids)
32
+ for (const setter of owner.setters) slot(owner.states, setter.signal, `Component owner ${owner.slot} setter ${JSON.stringify(setter.name)}`, "state")
33
+ for (const state of owner.states) if (state.owner !== undefined) ownerRef(state.owner, `Component owner ${owner.slot} state ${state.slot} external owner`)
34
+ }
35
+ for (const specialization of componentAnalysis.specializations) {
36
+ indexed(`Component specialization ${specialization.slot} state`, specialization.states)
37
+ indexed(`Component specialization ${specialization.slot} ref`, specialization.refs)
38
+ indexed(`Component specialization ${specialization.slot} ID`, specialization.ids)
39
+ if (specialization.owner !== undefined) ownerRef(specialization.owner, `Component specialization ${specialization.slot} owner`)
40
+ for (const prop of specialization.props ?? []) for (const signal of prop.signals ?? []) slot(moduleIR.signals, signal, `Component specialization ${specialization.slot} prop ${JSON.stringify(prop.name)}`, "SignalIR")
41
+ }
42
+ }
43
+ function moduleSymbol(symbol, label) {
44
+ if (!symbol || typeof symbol.id !== "string" || typeof symbol.module !== "string" || typeof symbol.site !== "string" || typeof symbol.name !== "string") throw new Error(`${label} must be a ModuleSymbol`)
45
+ }
46
+ function ownerRef(reference, label) {
47
+ if (!reference || typeof reference !== "object") throw new Error(`${label} must be an OwnerRef`)
48
+ if (reference.kind === "component") return slot(componentAnalysis?.owners ?? [], reference.slot, label, "component owner")
49
+ if (reference.kind === "specialization") return slot(componentAnalysis?.specializations ?? [], reference.slot, label, "component specialization")
50
+ if (reference.kind === "module-symbol") return moduleSymbol(reference.symbol, label)
51
+ if (reference.kind !== "module") throw new Error(`${label} has invalid kind ${JSON.stringify(reference.kind)}`)
52
+ }
53
+ function stateRef(reference, label) {
54
+ if (!reference || typeof reference !== "object") throw new Error(`${label} must be a StateRef`)
55
+ if (reference.kind === "module-symbol") return moduleSymbol(reference.symbol, label)
56
+ if (reference.kind === "symbol") return slot(moduleIR.symbols, reference.symbol, label, "SymbolRef")
57
+ if (reference.kind !== "state") throw new Error(`${label} has invalid kind ${JSON.stringify(reference.kind)}`)
58
+ const owner = ownerRef(reference.owner, `${label} owner`)
59
+ const states = reference.owner.kind === "component" || reference.owner.kind === "specialization" ? owner.states : []
60
+ slot(states, reference.slot, label, "owner state")
61
+ }
62
+ const exports = new Map()
63
+ const exported = (record, label) => {
64
+ if (record.kind !== "module-export") return
65
+ if (typeof record.exportName !== "string" || !record.exportName) throw new Error(`${label} requires an export name`)
66
+ const previous = exports.get(record.exportName)
67
+ if (previous) throw new Error(`ModuleIR export ${JSON.stringify(record.exportName)} is declared by both ${previous} and ${label}`)
68
+ exports.set(record.exportName, label)
69
+ }
70
+ for (const signal of moduleIR.signals) stateRef(signal.reference, `SignalIR ${signal.slot}`)
14
71
  for (const handler of moduleIR.handlers) {
15
- for (const command of handler.commands ?? []) slot(moduleIR.signals, command.signal, `handler ${handler.slot} command signal`)
16
- if (handler.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, handler.keyedBlock, `handler ${handler.slot} keyed block`)
72
+ exported(handler, `HandlerIR ${handler.slot}`)
73
+ for (const [index, command] of (handler.commands ?? []).entries()) slot(moduleIR.signals, command.signal, `HandlerIR ${handler.slot} command ${index}`, "SignalIR")
74
+ for (const [index, signal] of (handler.signals ?? []).entries()) slot(moduleIR.signals, signal.signal, `HandlerIR ${handler.slot} signal ${index}`, "SignalIR")
75
+ for (const [index, imported] of (handler.imports ?? []).entries()) slot(moduleIR.imports, imported, `HandlerIR ${handler.slot} import ${index}`, "ImportIR")
76
+ for (const [index, capture] of (handler.captures ?? []).entries()) if (capture.symbol !== undefined) slot(moduleIR.symbols, capture.symbol, `HandlerIR ${handler.slot} capture ${index}`, "SymbolRef")
77
+ if (handler.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, handler.keyedBlock, `HandlerIR ${handler.slot} keyed block`, "KeyedBlockIR")
17
78
  }
18
- for (const binding of moduleIR.bindings) if (binding.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, binding.keyedBlock, `binding ${binding.slot} keyed block`)
79
+ for (const binding of moduleIR.bindings) {
80
+ exported(binding, `BindingIR ${binding.slot}`)
81
+ for (const [index, signal] of (binding.signals ?? []).entries()) slot(moduleIR.signals, signal, `BindingIR ${binding.slot} signal ${index}`, "SignalIR")
82
+ for (const [index, imported] of (binding.imports ?? []).entries()) slot(moduleIR.imports, imported, `BindingIR ${binding.slot} import ${index}`, "ImportIR")
83
+ for (const [index, capture] of (binding.captures ?? []).entries()) if (capture.symbol !== undefined) slot(moduleIR.symbols, capture.symbol, `BindingIR ${binding.slot} capture ${index}`, "SymbolRef")
84
+ if (binding.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, binding.keyedBlock, `BindingIR ${binding.slot} keyed block`, "KeyedBlockIR")
85
+ }
86
+ for (const derived of moduleIR.derived) for (const [index, signal] of (derived.signals ?? []).entries()) slot(moduleIR.signals, signal, `DerivedIR ${derived.slot} signal ${index}`, "SignalIR")
19
87
  for (const effect of moduleIR.effects) {
20
- slot(moduleIR.handlers, effect.setup?.handler, `effect ${effect.slot} setup handler`)
21
- for (const dependency of effect.dependencies ?? []) if (dependency.kind === "derived") slot(moduleIR.derived, dependency.derived, `effect ${effect.slot} derived dependency`)
22
- if (effect.ownership?.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, effect.ownership.keyedBlock, `effect ${effect.slot} keyed block`)
88
+ const handler = slot(moduleIR.handlers, effect.setup?.handler, `EffectIR ${effect.slot} setup`, "HandlerIR")
89
+ if (handler.kind !== "module-export" || handler.role !== "effect") throw new Error(`EffectIR ${effect.slot} setup HandlerIR ${handler.slot} must have role "effect"`)
90
+ for (const [index, dependency] of (effect.dependencies ?? []).entries()) {
91
+ if (dependency.kind === "signal") slot(moduleIR.signals, dependency.signal, `EffectIR ${effect.slot} dependency ${index}`, "SignalIR")
92
+ else if (dependency.kind === "derived") {
93
+ slot(moduleIR.derived, dependency.derived, `EffectIR ${effect.slot} dependency ${index}`, "DerivedIR")
94
+ for (const [sourceIndex, signal] of (dependency.sources ?? []).entries()) slot(moduleIR.signals, signal, `EffectIR ${effect.slot} dependency ${index} source ${sourceIndex}`, "SignalIR")
95
+ } else throw new Error(`EffectIR ${effect.slot} dependency ${index} has invalid kind ${JSON.stringify(dependency.kind)}`)
96
+ }
97
+ for (const [index, signal] of (effect.subscriptions ?? []).entries()) slot(moduleIR.signals, signal, `EffectIR ${effect.slot} subscription ${index}`, "SignalIR")
98
+ for (const [index, signal] of (effect.dependencySignals ?? []).entries()) slot(moduleIR.signals, signal, `EffectIR ${effect.slot} dependency signal ${index}`, "SignalIR")
99
+ if (effect.ownership?.owner) ownerRef(effect.ownership.owner, `EffectIR ${effect.slot} ownership`)
100
+ if (effect.ownership?.keyedBlock !== undefined) {
101
+ slot(moduleIR.keyedBlocks, effect.ownership.keyedBlock, `EffectIR ${effect.slot} ownership`, "KeyedBlockIR")
102
+ if (handler.keyedBlock !== effect.ownership.keyedBlock) throw new Error(`EffectIR ${effect.slot} and HandlerIR ${handler.slot} must reference the same KeyedBlockIR`)
103
+ }
23
104
  }
24
105
  for (const block of moduleIR.keyedBlocks) {
25
- if (block.parent !== undefined) slot(moduleIR.keyedBlocks, block.parent, `keyed block ${block.slot} parent`)
26
- for (const child of block.children ?? []) slot(moduleIR.keyedBlocks, child, `keyed block ${block.slot} child`)
27
- if (block.selector !== undefined) slot(moduleIR.derived, block.selector, `keyed block ${block.slot} selector`)
106
+ if (block.collection?.kind === "signal") slot(moduleIR.signals, block.collection.signal, `KeyedBlockIR ${block.slot} collection`, "SignalIR")
107
+ else if (block.collection?.kind === "binding") slot(moduleIR.bindings, block.collection.binding, `KeyedBlockIR ${block.slot} collection`, "BindingIR")
108
+ else if (block.collection?.kind === "symbol") slot(moduleIR.symbols, block.collection.symbol, `KeyedBlockIR ${block.slot} collection`, "SymbolRef")
109
+ else if (block.collection?.kind !== "static") throw new Error(`KeyedBlockIR ${block.slot} collection has invalid kind ${JSON.stringify(block.collection?.kind)}`)
110
+ if (block.parent !== undefined) {
111
+ const parent = slot(moduleIR.keyedBlocks, block.parent, `KeyedBlockIR ${block.slot} parent`, "KeyedBlockIR")
112
+ if (!(parent.children ?? []).includes(block.slot)) throw new Error(`KeyedBlockIR ${block.slot} parent ${parent.slot} does not reciprocally list child ${block.slot}`)
113
+ }
114
+ for (const childSlot of block.children ?? []) {
115
+ const child = slot(moduleIR.keyedBlocks, childSlot, `KeyedBlockIR ${block.slot} child`, "KeyedBlockIR")
116
+ if (child.parent !== block.slot) throw new Error(`KeyedBlockIR ${block.slot} child ${child.slot} does not reciprocally reference parent ${block.slot}`)
117
+ }
118
+ if (new Set(block.children ?? []).size !== (block.children ?? []).length) throw new Error(`KeyedBlockIR ${block.slot} has duplicate children`)
119
+ if (block.selector !== undefined) slot(moduleIR.derived, block.selector, `KeyedBlockIR ${block.slot} selector`, "DerivedIR")
120
+ for (const [index, signal] of (block.selectorSignals ?? []).entries()) slot(moduleIR.signals, signal, `KeyedBlockIR ${block.slot} selector signal ${index}`, "SignalIR")
121
+ for (const [index, specialization] of (block.specializations ?? []).entries()) slot(componentAnalysis?.specializations ?? [], specialization, `KeyedBlockIR ${block.slot} specialization ${index}`, "component specialization")
122
+ for (const [index, row] of (block.rowStates ?? []).entries()) slot(moduleIR.signals, row.signal, `KeyedBlockIR ${block.slot} row state ${index}`, "SignalIR")
123
+ for (const [index, row] of (block.rowRefs ?? []).entries()) {
124
+ const specialization = slot(componentAnalysis?.specializations ?? [], row.specialization, `KeyedBlockIR ${block.slot} row ref ${index}`, "component specialization")
125
+ slot(specialization.refs, row.ref, `KeyedBlockIR ${block.slot} row ref ${index}`, "specialization ref")
126
+ }
127
+ }
128
+ const visiting = new Set()
129
+ const visited = new Set()
130
+ const visit = (block, trail) => {
131
+ if (visiting.has(block.slot)) throw new Error(`KeyedBlockIR parent cycle: ${[...trail, block.slot].join(" -> ")}`)
132
+ if (visited.has(block.slot)) return
133
+ visiting.add(block.slot)
134
+ if (block.parent !== undefined) visit(moduleIR.keyedBlocks[block.parent], [...trail, block.slot])
135
+ visiting.delete(block.slot)
136
+ visited.add(block.slot)
28
137
  }
138
+ for (const block of moduleIR.keyedBlocks) visit(block, [])
29
139
  return moduleIR
30
140
  }
31
141
 
32
- export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
33
- const slots = new Map(moduleIR.signals.map(signal => [signal.key, signal.slot]))
34
- for (const { state, owner = scope } of commands) {
35
- const key = `${owner}:${state}`
36
- if (slots.has(key)) continue
37
- const slot = moduleIR.signals.length
38
- slots.set(key, slot)
39
- moduleIR.signals.push({ slot, key, debugName: state })
142
+ export function registerSignal(moduleIR, reference, debugName) {
143
+ const key = JSON.stringify(reference)
144
+ let signal = moduleIR.signals.find(entry => JSON.stringify(entry.reference) === key)
145
+ if (!signal) {
146
+ signal = { slot: moduleIR.signals.length, reference, debugName }
147
+ moduleIR.signals.push(signal)
40
148
  }
41
- const signal = command => slots.get(`${command.owner ?? scope}:${command.state}`)
149
+ return signal
150
+ }
151
+
152
+ export function registerCommandHandler(moduleIR, commands, source) {
42
153
  const handler = {
43
154
  slot: moduleIR.handlers.length,
44
155
  kind: "commands",
45
- commands: commands.map(({ operation, state, owner, value, syntax }) => ({ operation, signal: signal({ state, owner }), value, ...(syntax ? { syntax } : {}) })),
156
+ commands: commands.map(({ operation, reference, state, value, syntax }) => ({ operation, signal: registerSignal(moduleIR, reference, state).slot, value, ...(syntax ? { syntax } : {}) })),
46
157
  ...(source ? { source } : {})
47
158
  }
48
159
  moduleIR.handlers.push(handler)
@@ -50,31 +161,31 @@ export function registerCommandHandler(moduleIR, commands, source, scope = "modu
50
161
  }
51
162
 
52
163
  export function registerModuleHandler(moduleIR, descriptor) {
53
- const handler = { slot: moduleIR.handlers.length, kind: "module-export", ...descriptor }
164
+ const handler = { ...descriptor, slot: moduleIR.handlers.length, kind: "module-export" }
54
165
  moduleIR.handlers.push(handler)
55
166
  return handler
56
167
  }
57
168
 
58
169
  export function registerBinding(moduleIR, descriptor) {
59
- const binding = { slot: moduleIR.bindings.length, kind: "module-export", ...descriptor }
170
+ const binding = { ...descriptor, slot: moduleIR.bindings.length, kind: "module-export" }
60
171
  moduleIR.bindings.push(binding)
61
172
  return binding
62
173
  }
63
174
 
64
175
  export function registerDerived(moduleIR, descriptor) {
65
- const derived = { slot: moduleIR.derived.length, ...descriptor }
176
+ const derived = { ...descriptor, slot: moduleIR.derived.length }
66
177
  moduleIR.derived.push(derived)
67
178
  return derived
68
179
  }
69
180
 
70
181
  export function registerEffect(moduleIR, descriptor) {
71
- const effect = { slot: moduleIR.effects.length, ...descriptor }
182
+ const effect = { ...descriptor, slot: moduleIR.effects.length }
72
183
  moduleIR.effects.push(effect)
73
184
  return effect
74
185
  }
75
186
 
76
187
  export function registerKeyedBlock(moduleIR, descriptor) {
77
- const block = { slot: moduleIR.keyedBlocks.length, ...descriptor }
188
+ const block = { ...descriptor, slot: moduleIR.keyedBlocks.length }
78
189
  moduleIR.keyedBlocks.push(block)
79
190
  return block
80
191
  }
@@ -0,0 +1,81 @@
1
+ export function createRouteBuildRecord(input) {
2
+ const record = {
3
+ version: input.version ?? 1,
4
+ route: input.route,
5
+ output: input.output,
6
+ html: input.html,
7
+ plan: input.plan,
8
+ capabilities: input.capabilities,
9
+ artifacts: {
10
+ handlers: input.handlerReferences ?? [],
11
+ effects: (input.plan?.effects ?? []).map(({ module, handler }) => ({ module, handler })),
12
+ styles: [...new Set(input.styles ?? [])]
13
+ },
14
+ entries: input.entries ?? {},
15
+ ...(input.runtimeSchema ? { runtimeSchema: input.runtimeSchema } : {})
16
+ }
17
+ return assertRouteBuildRecord(record)
18
+ }
19
+
20
+ export function assertRouteBuildRecord(record) {
21
+ if (record?.version !== 1) throw new Error(`Unsupported RouteBuildRecord version: ${JSON.stringify(record?.version)}`)
22
+ if (typeof record.route !== "string" || typeof record.output !== "string" || typeof record.html !== "string" || !isRecord(record.plan)) throw new Error("Invalid RouteBuildRecord v1 structure")
23
+ if (record.plan.route !== record.route) throw new Error(`RouteBuildRecord route ${JSON.stringify(record.route)} does not match RouteIR route ${JSON.stringify(record.plan.route)}`)
24
+ const capabilityNames = ["navigable", "usesDependencyRuntime", "hasBehaviors", "hasBindings", "hasLists", "hasListStyles", "hasStateSeed", "hasParams", "hasEffects"]
25
+ if (!isRecord(record.capabilities) || !capabilityNames.every(name => typeof record.capabilities[name] === "boolean")) throw new Error("Invalid RouteBuildRecord v1 capabilities")
26
+ if (!isRecord(record.artifacts) || !Array.isArray(record.artifacts.handlers) || !Array.isArray(record.artifacts.effects) || !Array.isArray(record.artifacts.styles) || !isRecord(record.entries)) throw new Error("Invalid RouteBuildRecord v1 artifacts")
27
+ const handlers = new Set()
28
+ for (const reference of record.artifacts.handlers) {
29
+ assertHandlerReference(reference)
30
+ const key = referenceKey(reference)
31
+ if (handlers.has(key)) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has duplicate handler reference ${key}`)
32
+ handlers.add(key)
33
+ }
34
+ for (const effect of record.artifacts.effects) {
35
+ assertHandlerReference(effect)
36
+ if (!handlers.has(referenceKey(effect))) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} effect references an unretained handler ${referenceKey(effect)}`)
37
+ }
38
+ for (const event of record.plan.events ?? []) if (event.native && !handlers.has(referenceKey(event.native))) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} event references an unretained handler ${referenceKey(event.native)}`)
39
+ for (const descriptor of [...(record.plan.bindings ?? []), ...(record.plan.conditions ?? []), ...(record.plan.lists ?? []).map(list => list.source).filter(Boolean)]) {
40
+ if (descriptor.module && !handlers.has(referenceKey(descriptor))) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} descriptor references an unretained handler ${referenceKey(descriptor)}`)
41
+ }
42
+ if (record.artifacts.styles.some(style => typeof style !== "string")) throw new Error("RouteBuildRecord styles must be strings")
43
+ for (const [kind, path] of Object.entries(record.entries)) if (!["effect", "native", "param"].includes(kind) || typeof path !== "string") throw new Error(`Invalid RouteBuildRecord entry ${JSON.stringify(kind)}`)
44
+ if (record.runtimeSchema !== undefined && !isRecord(record.runtimeSchema)) throw new Error("Invalid RouteBuildRecord runtime schema")
45
+ const hasNative = (record.plan.events ?? []).some(event => event.native)
46
+ if (Boolean(record.entries.effect) !== record.capabilities.hasEffects || record.capabilities.hasEffects !== Boolean(record.plan.effects?.length)) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent effect artifacts`)
47
+ if (Boolean(record.entries.native) !== hasNative) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent native artifacts`)
48
+ if (Boolean(record.entries.param) !== record.capabilities.hasParams) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent parameter artifacts`)
49
+ return record
50
+ }
51
+
52
+ export function planRouteArtifacts(records, handlerModules, workerReferences, moduleUrl) {
53
+ for (const record of records) assertRouteBuildRecord(record)
54
+ const modules = new Map()
55
+ for (const module of handlerModules) {
56
+ const url = moduleUrl(module)
57
+ if (modules.has(url)) throw new Error(`Duplicate compiled handler module: ${url}`)
58
+ modules.set(url, module)
59
+ }
60
+ const retainedUrls = new Set(records.flatMap(record => record.artifacts.handlers.map(reference => reference.module)))
61
+ for (const url of retainedUrls) if (!modules.has(url)) throw new Error(`Handler module was not compiled: ${url}`)
62
+ const effects = new Map()
63
+ for (const reference of records.flatMap(record => record.artifacts.effects)) {
64
+ const handlers = effects.get(reference.module) ?? new Set()
65
+ handlers.add(reference.handler)
66
+ effects.set(reference.module, handlers)
67
+ }
68
+ return {
69
+ handlerModules: handlerModules.filter(module => retainedUrls.has(moduleUrl(module))),
70
+ workerReferences: workerReferences.filter(reference => effects.get(reference.module)?.has(reference.handler)),
71
+ styles: [...new Set(records.flatMap(record => record.artifacts.styles))]
72
+ }
73
+ }
74
+
75
+ const isRecord = value => value !== null && typeof value === "object" && !Array.isArray(value)
76
+
77
+ function assertHandlerReference(reference) {
78
+ if (!isRecord(reference) || typeof reference.module !== "string" || !reference.module || typeof reference.handler !== "string" || !reference.handler) throw new Error("RouteBuildRecord handler references require module and handler strings")
79
+ }
80
+
81
+ const referenceKey = reference => `${JSON.stringify(reference.module)}#${JSON.stringify(reference.handler)}`
@@ -4,7 +4,9 @@ export function usesRouteDependencyRuntime({ plan, navigable, hasBindings, hasLi
4
4
  return !navigable && hasDependencies && !plan.effects.some(effect => effect.owner) && !hasBindings && !hasLists && !plan.events.some(event => event.native)
5
5
  }
6
6
 
7
- export function planRouteCapabilities(plans, { routes = new Map(), navigationRouteCount = 0 } = {}) {
7
+ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}) {
8
+ for (const record of records) assertRouteBuildRecord(record)
9
+ const plans = records.map(record => record.plan)
8
10
  for (const plan of plans) assertRouteIR(plan)
9
11
  const commandEvents = new Set()
10
12
  const nativeEvents = new Set()
@@ -39,7 +41,7 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
39
41
 
40
42
  for (let index = 0; index < plans.length; index++) {
41
43
  const plan = plans[index]
42
- const route = routes.get(plan.route)
44
+ const route = records[index].capabilities
43
45
  for (const event of plan.events) {
44
46
  if (event.commands) commandEvents.add(event.event)
45
47
  if (event.native) nativeEvents.add(event.event)
@@ -77,7 +79,7 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
77
79
  }
78
80
  }
79
81
 
80
- const routeEntries = [...routes.values()]
82
+ const routeEntries = records.map(record => record.capabilities)
81
83
  const routeCounts = {
82
84
  behaviors: routeEntries.filter(route => route.hasBehaviors).length,
83
85
  regularBehaviors: routeEntries.filter(route => route.hasBehaviors && !route.usesDependencyRuntime).length,
@@ -151,3 +153,4 @@ function hasNestedCaptureState(value, insideCapture = false) {
151
153
  if (value.type === "object") return value.value.some(([, entry]) => hasNestedCaptureState(entry, true))
152
154
  return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasNestedCaptureState(entry, false))
153
155
  }
156
+ import { assertRouteBuildRecord } from "./route-build-record.mjs"