@kudzujs/core 0.8.20 → 0.8.22

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.
@@ -11,21 +11,26 @@ import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditio
11
11
  import { analyzeCollectionPipeline, collectionExpression, collectionParameters, isArrayFromCall, mutatingCollectionMethods as mutatingListMethods, pureCollectionMathMethods as pureMathMethods, pureCollectionMethods as pureListMethods } from "./compiler/collection-analysis.mjs"
12
12
  import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.mjs"
13
13
  import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
14
+ import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "./compiler/effect-analysis.mjs"
14
15
  import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
15
16
  import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
16
17
  import { createHandlerLowering } from "./compiler/handler-lowering.mjs"
18
+ import { generateListRuntime } from "./compiler/list-runtime-codegen.mjs"
17
19
  import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
18
20
  import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
21
+ import { createParamCodegen } from "./compiler/param-codegen.mjs"
19
22
  import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
20
23
  import { normalizeRenderControlFlow } from "./compiler/render-control-pass.mjs"
21
24
  import { createRouterPass } from "./compiler/router-pass.mjs"
22
25
  import { planRouteCapabilities, usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
26
+ import { generateBindingRuntime, generateCoreRuntime, generateEffectRuntime, generateNativeRuntime, generateNavigationRuntime, specializeRuntime } from "./compiler/runtime-codegen.mjs"
23
27
  import { createWorkerCompiler } from "./compiler/worker-compiler.mjs"
24
28
  import { createZustandPass } from "./compiler/zustand-pass.mjs"
25
29
  import { renderPage } from "./core.mjs"
26
30
  import { parseDevHost, parseDevPort, startDevServer } from "./dev-server.mjs"
27
31
 
28
32
  export { parseDevHost, parseDevPort }
33
+ export { specializeRuntime }
29
34
 
30
35
  const root = process.cwd()
31
36
  const sourceDirectory = join(root, "src")
@@ -75,12 +80,16 @@ export async function build({ quiet = false, minify = true } = {}) {
75
80
  const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base)
76
81
 
77
82
  const sourceResults = []
78
- const workerReferences = []
79
83
  for (const file of sourceFiles) {
80
84
  if (file.endsWith(".worker.ts")) continue
81
- sourceResults.push(await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences))
85
+ sourceResults.push(await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base))
82
86
  }
83
87
  const handlerModules = sourceResults.flatMap(result => result.handlerModule ? [result.handlerModule] : [])
88
+ const workerReferences = sourceResults.flatMap(result => result.moduleIR.effects.flatMap(effect => {
89
+ const handler = result.moduleIR.handlers[effect.setup.handler]
90
+ if (!handler || handler.kind !== "module-export" || handler.role !== "effect") throw new Error(`EffectIR ${effect.slot} has no effect HandlerIR`)
91
+ return effect.workers.map(worker => ({ ...worker, module: assetPath(base, `assets/${result.handlerModule.path}`), handler: handler.exportName }))
92
+ }))
84
93
 
85
94
  const plans = []
86
95
  const routeCapabilities = new Map()
@@ -208,41 +217,16 @@ export async function build({ quiet = false, minify = true } = {}) {
208
217
  }
209
218
  if (module.code.includes("/__kudzu_worker_")) throw new Error(`Worker URL placeholder survived in ${module.path}`)
210
219
  }
211
- const capabilityManifest = planRouteCapabilities(plans, { routes: routeCapabilities, navigationRouteCount: navigationRoutes.length })
220
+ const capabilityIR = planRouteCapabilities(plans, { routes: routeCapabilities, navigationRouteCount: navigationRoutes.length })
212
221
  const {
213
- routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount, regularStateSeeds: regularStateSeedCount, dependencyStateSeeds: dependencyStateSeedCount },
214
- events: { command: commandEvents, native: nativeEvents, hasNativeHandlers },
215
- bindings: { count: bindingCount, text: hasTextBindings, svgConditions: hasSvgConditions },
216
- lists: {
217
- count: listCount,
218
- styleCount: listStyleCount,
219
- conditions: hasListConditions,
220
- svg: hasSvgLists,
221
- deepConditions: hasDeepListConditions,
222
- textRanges: hasListTextRanges,
223
- attributes: hasListAttributes,
224
- events: hasListEvents,
225
- expressions: hasListExpressions,
226
- expressionAttributes: hasListExpressionAttributes,
227
- seeds: hasListSeeds,
228
- effects: hasListEffects,
229
- rowHooks: hasListRowHooks,
230
- rowRefs: hasListRowRefs,
231
- complexRowState: hasComplexListRowState,
232
- nested: hasNestedLists,
233
- selectors: hasCollectionSelectors,
234
- calculated: hasCalculatedCollections,
235
- static: hasStaticCollections,
236
- indexes: hasListIndexes,
237
- stableFastPaths: hasListStableFastPaths,
238
- generalRowHooks: hasGeneralListRowHooks,
239
- asyncParts: hasListAsyncParts,
240
- mounts: hasListMounts
241
- },
242
- effects: { any: hasEffects, derivedDependencies: hasDerivedEffectDependencies, itemDependencies: hasItemDependencies, captures: hasEffectCaptures, navigable: hasNavigableEffects, navigableOwners: hasNavigableOwners },
222
+ routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount, dependencyStateSeeds: dependencyStateSeedCount },
223
+ events: { command: commandEvents, hasNativeHandlers },
224
+ bindings: { count: bindingCount },
225
+ lists,
226
+ effects: { any: hasEffects, derivedDependencies: hasDerivedEffectDependencies, captures: hasEffectCaptures },
243
227
  captures: { nestedState: hasNestedStateCaptures, setter: hasSetterCaptures },
244
228
  runtime: { shared: hasSharedRuntime, dependency: hasDependencyRuntime }
245
- } = capabilityManifest
229
+ } = capabilityIR
246
230
  const runtimeName = usesDependencyRuntime => usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
247
231
  for (const entry of pageEntries) {
248
232
  const routeDirectory = join(outputDirectory, entry.route)
@@ -252,12 +236,7 @@ export async function build({ quiet = false, minify = true } = {}) {
252
236
  }
253
237
  if (navigationRoutes.length || behaviorCount && (hasSharedRuntime || regularBehaviorCount)) {
254
238
  const runtimeFile = hasSharedRuntime ? "./shared-runtime.js" : "./runtime.js"
255
- let runtime = specializeRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), commandEvents, regularStateSeedCount > 0)
256
- if (!hasItemDependencies) runtime = runtime.replace(/\/\* list-item-hooks \*\/[\s\S]*?\/\* list-item-hooks-end \*\/\n/, "")
257
- if (hasNavigableEffects) runtime = runtime.replace("export function registerCommitter(commit) {\n committers.push(commit)\n}", "export function registerCommitter(commit) {\n committers.push(commit)\n return () => {\n const index = committers.indexOf(commit)\n if (index !== -1) committers.splice(index, 1)\n }\n}")
258
- if (hasNavigableOwners) runtime = runtime
259
- .replace("export function registerMountHook(mount) {\n mountHooks.push(mount)\n}", "export function registerMountHook(mount) {\n mountHooks.push(mount)\n return () => {\n const index = mountHooks.indexOf(mount)\n if (index !== -1) mountHooks.splice(index, 1)\n }\n}")
260
- .replace("export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n}", "export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n return () => {\n const index = unmountHooks.indexOf(unmount)\n if (index !== -1) unmountHooks.splice(index, 1)\n }\n}")
239
+ const runtime = generateCoreRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), capabilityIR)
261
240
  await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
262
241
  }
263
242
  if (hasDependencyRuntime) {
@@ -269,129 +248,30 @@ export async function build({ quiet = false, minify = true } = {}) {
269
248
  "globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
270
249
  })
271
250
  if (hasEffects) {
272
- let effectRuntime = await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8")
273
- effectRuntime = hasEffectCaptures ? effectRuntime.replace('"./serialization.js"', '"./kudzu-serialization.js"') : effectRuntime.replace(/^import[^\n]+\n/, "")
274
- await writeBundledJavaScript(join(assetsDirectory, "kudzu-effect.js"), effectRuntime, minify, {
275
- "globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures),
276
- "globalThis.__KUDZU_EFFECT_CAPTURES__": String(hasEffectCaptures)
277
- })
251
+ const generated = generateEffectRuntime(await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8"), capabilityIR)
252
+ await writeBundledJavaScript(join(assetsDirectory, "kudzu-effect.js"), generated.source, minify, generated.define)
278
253
  }
279
- if (bindingCount || listStyleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
254
+ if (bindingCount || lists.styleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
280
255
  if (bindingCount) {
281
- let bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
282
- .replace('"./shared-runtime.js"', '"./kudzu.js"')
283
- .replace('"./serialization.js"', '"./kudzu-serialization.js"')
284
- .replace('"./style.js"', '"./kudzu-style.js"')
285
- if (navigationRoutes.length) bindingRuntime = specializeNavigationTextDescriptors(bindingRuntime)
286
- await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify, {
287
- "globalThis.__KUDZU_TEXT_BINDINGS__": String(hasTextBindings),
288
- "globalThis.__KUDZU_SVG_CONDITIONS__": String(hasSvgConditions),
289
- "globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures)
290
- })
256
+ const generated = generateBindingRuntime(await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"), capabilityIR, navigationRoutes.length > 0)
257
+ await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), generated.source, minify, generated.define)
291
258
  }
292
259
  if (hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
293
- if (listCount) {
294
- if (hasCollectionSelectors && !hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
295
- let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
296
- .replace('"./shared-runtime.js"', '"./kudzu.js"')
297
- listRuntime = hasCalculatedCollections
298
- ? listRuntime.replace('"./binding-runtime.js"', '"./kudzu-binding.js"')
299
- : listRuntime.replace(/^const loadListEvaluator[^\n]+\n/m, "")
300
- listRuntime = hasCollectionSelectors
301
- ? listRuntime.replace('"./collection-selector.js"', '"./kudzu-collection-selector.js"')
302
- : listRuntime.replace(/^import \{ selectCollection \}[^\n]+\n/m, "")
303
- if (!hasListIndexes) listRuntime = listRuntime
304
- .replace("for (const [index, item] of items.entries()) {", "for (const item of items) {")
305
- .replace("const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]", "const key = item?.[list.descriptor.key]")
306
- .replace("entries.push({ item, index, key, token, value:", "entries.push({ item, key, token, value:")
307
- .replace("for (const { item, index, key, token, value } of entries) {", "for (const { item, key, token, value } of entries) {")
308
- .replaceAll("fillListItem(node, item, list.descriptor.nested, index)", "fillListItem(node, item, list.descriptor.nested)")
309
- .replace("fillListItem(node, item, list.descriptor.nested, index, mapListItemParts", "fillListItem(node, item, list.descriptor.nested, 0, mapListItemParts")
310
- .replace("function addListRoot(list, { item, index = list.roots.size, key, token, value })", "function addListRoot(list, { item, key, token, value })")
311
- .replace("fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)", "fillListParts(root, listItemParts(root), listItems.get(owner), 0)")
312
- .replace("function fillListItem(root, item, nested = false, index = 0)", "function fillListItem(root, item, nested = false)")
313
- .replace("fillListParts(root, parts, item, revision, index, previous)", "fillListParts(root, parts, item, revision, previous)")
314
- .replace("function fillListParts(root, parts, item, revision, index = 0, previous)", "function fillListParts(root, parts, item, revision, previous)")
315
- .replace("fillListExpressions(root, parts, item, revision, index)", "fillListExpressions(root, parts, item, revision)")
316
- .replaceAll('value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value', 'value?.type === "list-item" ? serializeItem(item) : value')
317
- .replaceAll("evaluate(descriptor, item, index)", "evaluate(descriptor, item)")
318
- .replaceAll("evaluate({ module, handler }, item, index)", "evaluate({ module, handler }, item)")
319
- .replace("updateListCondition(marker, descriptor.kind, value, item, index)", "updateListCondition(marker, descriptor.kind, value, item)")
320
- .replace("function updateListCondition(marker, kind, value, item, index)", "function updateListCondition(marker, kind, value, item)")
321
- .replace("fillListParts(marker, listItemParts(fragment), item, revision, index)", "fillListParts(marker, listItemParts(fragment), item, revision)")
322
- .replace("function evaluate(descriptor, item, index)", "function evaluate(descriptor, item)")
323
- .replace("exports[descriptor.handler](item, index)", "exports[descriptor.handler](item)")
324
- .replace("exports[descriptor.handler](item, index, {", "exports[descriptor.handler](item, undefined, {")
325
- if (!hasCollectionSelectors) listRuntime = listRuntime.replaceAll(" && !list.descriptor.selector", "")
326
- if (!hasListIndexes) listRuntime = listRuntime
327
- .replaceAll(" && !list.descriptor.indexed", "")
328
- .replaceAll(" && list.descriptor.key !== null", "")
329
- .replaceAll("list.descriptor.key !== null && !list.descriptor.indexed && ", "")
330
- .replace("list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && list.values.size", "list.values.size")
331
- .replace("(referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value) || list.descriptor.indexed || list.descriptor.key === null", "referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value")
332
- if (hasListRowHooks && !hasGeneralListRowHooks) listRuntime = listRuntime
333
- .replace(/\/\* general-row-hooks \*\/[\s\S]*?\/\* general-row-hooks-end \*\/\n/, "")
334
- .replaceAll("initializeGeneralRowHooks", "initializeRowStates")
335
- .replace("if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index], roots[index], nested?.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])")
336
- .replaceAll("if (__KUDZU_LIST_ROW_HOOKS__) initializeRowStates(list.descriptor, key, node, list.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)")
337
- .replace("for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))", "for (const token of registration.list.roots.keys()) deleteFlatRowStates(registration.list.descriptor, token)")
338
- .replaceAll("deleteRowStates(list.descriptor, ownershipPaths.get(node))", "deleteFlatRowStates(list.descriptor, token)")
339
- .replace(" if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))\n", "")
340
- .replace(" if (!replacements) return\n", "")
341
- if (!hasItemDependencies) listRuntime = listRuntime.replace(", notifyListItem", "")
342
- if (!hasListStableFastPaths) listRuntime = listRuntime.replace(/\/\* stable-list-fast-path \*\/[\s\S]*?\/\* stable-list-fast-path-end \*\/\n/, "")
343
- const stylePatch = ` if (target === "style") {
344
- const style = serializeStyle(value)
345
- if (style) node.setAttribute("style", style)
346
- else node.removeAttribute("style")
347
- return
348
- }`
349
- listRuntime = listRuntime.replace(" /* list-style */", listStyleCount ? stylePatch : "")
350
- if (listStyleCount) listRuntime = `import { serializeStyle } from "./kudzu-style.js"\n${listRuntime}`
351
- await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), listRuntime, minify, {
352
- __KUDZU_LIST_CONDITIONS__: String(hasListConditions),
353
- __KUDZU_DEEP_LIST_CONDITIONS__: String(hasDeepListConditions),
354
- __KUDZU_LIST_TEXT_RANGES__: String(hasListTextRanges),
355
- __KUDZU_LIST_ATTRIBUTES__: String(hasListAttributes),
356
- __KUDZU_LIST_EVENTS__: String(hasListEvents),
357
- __KUDZU_LIST_EXPRESSIONS__: String(hasListExpressions),
358
- __KUDZU_LIST_EXPRESSION_ATTRIBUTES__: String(hasListExpressionAttributes),
359
- __KUDZU_LIST_SEEDS__: String(hasListSeeds),
360
- __KUDZU_LIST_EFFECTS__: String(hasListEffects),
361
- __KUDZU_LIST_ASYNC_PARTS__: String(hasListAsyncParts),
362
- __KUDZU_LIST_MOUNTS__: String(hasListMounts),
363
- __KUDZU_LIST_ITEM_HOOKS__: String(hasItemDependencies),
364
- __KUDZU_LIST_ROW_HOOKS__: String(hasListRowHooks),
365
- __KUDZU_LIST_ROW_REFS__: String(hasListRowRefs),
366
- __KUDZU_COMPLEX_LIST_ROW_STATE__: String(hasComplexListRowState),
367
- __KUDZU_NESTED_LISTS__: String(hasNestedLists),
368
- __KUDZU_COLLECTION_SELECTORS__: String(hasCollectionSelectors),
369
- __KUDZU_STATIC_COLLECTIONS__: String(hasStaticCollections),
370
- __KUDZU_LIST_INDEXES__: String(hasListIndexes),
371
- __KUDZU_LIST_STABLE_FAST_PATHS__: String(hasListStableFastPaths),
372
- __KUDZU_SVG_LISTS__: String(hasSvgLists)
373
- })
374
- if (hasCollectionSelectors && !hasDerivedEffectDependencies) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
260
+ if (lists.count) {
261
+ if (lists.selectors && !hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
262
+ const generated = generateListRuntime(await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"), capabilityIR)
263
+ await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), generated.source, minify, generated.define)
264
+ if (lists.selectors && !hasDerivedEffectDependencies) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
375
265
  }
376
266
  if (hasNativeHandlers) {
377
- const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
378
- .replace('"./shared-runtime.js"', '"./kudzu.js"')
379
- .replace('"./serialization.js"', '"./kudzu-serialization.js"')
380
- await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeEvents(nativeRuntime, nativeEvents), minify, {
381
- "globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
382
- })
267
+ const generated = generateNativeRuntime(await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"), capabilityIR)
268
+ await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), generated.source, minify, generated.define)
383
269
  for (const entry of nativeEntries) await printNativeEntry(entry, assetsDirectory, base, minify)
384
270
  }
385
271
  if (navigationGroups.length) {
386
272
  const navigationSource = await readFile(new URL("./navigation-runtime.js", import.meta.url), "utf8")
387
273
  for (const group of navigationGroups) {
388
- let navigationRuntime = navigationSource
389
- .replace("__KUDZU_NAVIGATION_ROUTES__", inlineJson(group.records))
390
- .replace("__KUDZU_APPLICATION_ID__", JSON.stringify(group.applicationId))
391
- .replace("__KUDZU_LAYOUT_ID__", JSON.stringify(group.layoutId))
392
- .replace('"./shared-runtime.js"', '"./kudzu.js"')
393
- navigationRuntime = specializeNavigationPatterns(navigationRuntime, group.records.some(record => record.segments))
394
- await writeJavaScript(join(assetsDirectory, group.assetName), specializeNavigationEffects(navigationRuntime, group.hasEffects || group.hasParams), minify)
274
+ await writeJavaScript(join(assetsDirectory, group.assetName), generateNavigationRuntime(navigationSource, group), minify)
395
275
  }
396
276
  }
397
277
  for (const handlerModule of emittedHandlerModules) {
@@ -478,64 +358,6 @@ function preloadModules(html) {
478
358
  return html.replace(scripts[0][0], `${links}${scripts[0][0]}`)
479
359
  }
480
360
 
481
- function specializeEvents(source, events) {
482
- return source.replace(/const eventNames = \[[^\n]+\]/, `const eventNames = ${JSON.stringify(events)}`)
483
- }
484
-
485
- function specializeNavigationEffects(source, enabled) {
486
- if (enabled) return source
487
- return source
488
- .replace("const noDispose = async () => {}\nlet routeDispose = noDispose\nlet layoutDispose = noDispose\nconst ready = mountInitial()\n", "")
489
- .replace(`addEventListener("pagehide", event => {
490
- if (event.persisted) return
491
- ++revision
492
- request?.abort()
493
- void (async () => {
494
- await routeDispose()
495
- await layoutDispose()
496
- })()
497
- })
498
- `, "")
499
- .replace(`
500
- async function mountInitial() {
501
- try {
502
- const record = matchRoute(location.pathname)
503
- if (!record) throw new Error("Initial navigation route does not match")
504
- const capabilities = await loadCapabilities(validate(document, record))
505
- capabilities.params?.(location.pathname, location.search)
506
- layoutDispose = await capabilities.effects?.mountLayoutEffects?.() ?? noDispose
507
- routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose
508
- } catch (error) {
509
- console.error(error)
510
- }
511
- }
512
- `, "")
513
- .replace(" await ready\n", "")
514
- .replace(" const { incoming, parsed, capabilities } = documentResult\n", " const { incoming, parsed } = documentResult\n")
515
- .replace(" await routeDispose()\n if (current !== revision) return\n", "")
516
- .replace(" commit(incoming, parsed.nodes, capabilities.params, url.pathname, url.search)\n", " commit(incoming, parsed.nodes)\n")
517
- .replace(" routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose\n", "")
518
- .replace(" return { incoming, parsed, capabilities: await loadCapabilities(parsed), record }\n", " await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))\n return { incoming, parsed, record }\n")
519
- .replace(`
520
- async function loadCapabilities(parsed) {
521
- const modules = await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))
522
- const params = modules.filter(module => typeof module.initializeParams === "function")
523
- const effects = modules.filter(module => typeof module.mountRouteEffects === "function")
524
- if (params.length > 1 || effects.length > 1) throw new Error("Navigation document has duplicate route capabilities")
525
- return { params: params[0]?.initializeParams, effects: effects[0] }
526
- }
527
- `, "")
528
- }
529
-
530
- function specializeNavigationPatterns(source, enabled) {
531
- if (enabled) return source
532
- return source.replace(/function matchRoute\(pathname\) \{[\s\S]+?\n\}\n\nfunction fallback/, `function matchRoute(pathname) {
533
- return routes.find(record => record.path === pathname)
534
- }
535
-
536
- function fallback`)
537
- }
538
-
539
361
  async function printNativeEntry(entry, assetsDirectory, base, minify) {
540
362
  const output = join(assetsDirectory, entry.path)
541
363
  await mkdir(dirname(output), { recursive: true })
@@ -561,83 +383,6 @@ function runtimeEffects(effects, lifetimes = false) {
561
383
  }))
562
384
  }
563
385
 
564
- function printParamEntry(schema, params, searchParams, searchParamsWritable, output, assetsDirectory, base, runtimeName, navigable) {
565
- const hasSearch = searchParams.length || searchParamsWritable
566
- const signature = hasSearch ? "pathname, search" : "pathname"
567
- const prefix = navigable ? `export function initializeParams(${signature}) {\n${searchParamsWritable ? "globalThis.__kSetSearchParams = setSearchParams\n" : ""}` : `${schema ? "let pathname = location.pathname\n" : ""}${hasSearch ? "let search = location.search\n" : ""}`
568
- const suffix = navigable ? "\n}" : ""
569
- const pathname = schema ? `const base = ${inlineJson(browserPath(base).slice(1).split("/").filter(Boolean).map(segment => decodeURIComponent(segment)))}
570
- const schema = ${inlineJson(schema.segments)}
571
- const params = ${inlineJson(params)}
572
- let path = pathname
573
- if (base.length) {
574
- const pathSegments = path.slice(1).split("/")
575
- if (pathSegments.length < base.length || base.some((segment, index) => decodeSegment(pathSegments[index], false) !== segment)) throw new Error("Runtime route is outside the configured base")
576
- path = "/" + pathSegments.slice(base.length).join("/")
577
- }
578
- if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1)
579
- const segments = path.slice(1).split("/")
580
- if (segments.length !== schema.length) throw new Error("Runtime route does not match its fallback pattern")
581
- const values = Object.create(null)
582
- for (let index = 0; index < schema.length; index++) {
583
- const segment = schema[index]
584
- const value = decodeSegment(segments[index], Boolean(segment.param))
585
- if (segment.literal !== undefined && value !== segment.literal) throw new Error("Runtime route literal does not match")
586
- if (segment.param) values[segment.param] = value
587
- }
588
- for (const param of params) {
589
- const value = values[param.name]
590
- browserState.set(param.id, value)
591
- commitDom(param.id, value)
592
- }
593
- function decodeSegment(raw, param) {
594
- if (param && /%(?:2f|5c)/i.test(raw)) throw new Error("Runtime route parameter contains an encoded separator")
595
- let value
596
- try { value = decodeURIComponent(raw) } catch { throw new Error("Runtime route parameter has malformed encoding") }
597
- const decodedDots = value.replace(/%2e/gi, ".")
598
- if (param && (!value || value === "." || value === ".." || decodedDots === "." || decodedDots === ".." || /[\\/?#]/.test(value) || [...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159) || /%(?:2f|5c)/i.test(value))) throw new Error("Runtime route parameter is invalid")
599
- return value
600
- }
601
- ` : ""
602
- const searchInitializer = searchParamsWritable && searchParams.length ? `function initializeSearch(search) {
603
- const query = new URLSearchParams(search)
604
- for (const param of ${inlineJson(searchParams)}) {
605
- const value = query.get(param.name)
606
- browserState.set(param.id, value)
607
- commitDom(param.id, value)
608
- }
609
- }
610
- ` : ""
611
- const query = searchParams.length ? searchParamsWritable ? "initializeSearch(search)\n" : `const query = new URLSearchParams(search)
612
- for (const param of ${inlineJson(searchParams)}) {
613
- const value = query.get(param.name)
614
- browserState.set(param.id, value)
615
- commitDom(param.id, value)
616
- }
617
- ` : ""
618
- const writer = searchParamsWritable ? `
619
- function setSearchParams(update, replace) {
620
- const next = update(new URLSearchParams(location.search))
621
- if (!(next instanceof URLSearchParams)) throw new Error("React Router search parameter updater must return URLSearchParams")
622
- const url = new URL(location.href)
623
- url.search = next.toString()
624
- history[replace ? "replaceState" : "pushState"](null, "", url)
625
- ${searchParams.length ? "initializeSearch(location.search)" : ""}
626
- }
627
- ${navigable ? "" : `globalThis.__kSetSearchParams = setSearchParams
628
- addEventListener("popstate", () => ${searchParams.length ? "initializeSearch(location.search)" : "undefined"})`}` : ""
629
- return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}
630
- ${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}`
631
- }
632
-
633
- export function specializeRuntime(source, events, hasStateSeed) {
634
- const specialized = specializeEvents(source, events)
635
- if (hasStateSeed) return specialized
636
- return specialized
637
- .replace(" const initialState = document.body.dataset.kState\n", "")
638
- .replace(/^ if \(initialState\).*\n/m, "")
639
- }
640
-
641
386
  async function writeJavaScript(file, source, minify, define) {
642
387
  const code = minify || define ? (await transform(source, { define, format: "esm", legalComments: "none", minify, target: "es2022" })).code : source
643
388
  await writeFile(file, code)
@@ -678,7 +423,7 @@ function escapeAttribute(value) {
678
423
  return escapeHtml(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;")
679
424
  }
680
425
 
681
- async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences) {
426
+ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base) {
682
427
  const source = sourceIndex.get(file)
683
428
  const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
684
429
  const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
@@ -690,7 +435,7 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
690
435
  jsx: ts.JsxEmit.ReactJSX,
691
436
  jsxImportSource: "@kudzujs/core"
692
437
  },
693
- transformers: { before: [createKudzuTransformer({ semantic, handlerUrl: assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences })] },
438
+ transformers: { before: [createKudzuTransformer({ semantic, handlerUrl: assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base })] },
694
439
  reportDiagnostics: true
695
440
  })
696
441
 
@@ -882,7 +627,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
882
627
  return { sourceFile, customHookTimerStates }
883
628
  }
884
629
 
885
- function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences }) {
630
+ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base }) {
886
631
  const { moduleIR } = semantic
887
632
  return context => sourceFile => {
888
633
  const hasLinkElements = /<link/i.test(sourceFile.text)
@@ -954,7 +699,6 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
954
699
  const jsxLocalsByFunction = new Map()
955
700
  const listLocalDeclarations = []
956
701
  const listLocalUses = []
957
- const componentEffectEntries = new WeakMap()
958
702
  const analysisSource = node => {
959
703
  const original = ts.getOriginalNode(node)
960
704
  return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
@@ -1724,8 +1468,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1724
1468
  const effectStatements = specialization.effects.map(entry => {
1725
1469
  const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
1726
1470
  synthesizeTree(effectCall)
1727
- const effectSource = entry.source.getSourceFile()
1728
- componentEffectEntries.set(effectCall, { source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
1471
+ ts.setOriginalNode(effectCall, entry.source)
1729
1472
  return factory.createExpressionStatement(effectCall)
1730
1473
  })
1731
1474
  const helper = factory.createFunctionDeclaration(
@@ -1956,15 +1699,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1956
1699
  return expanded
1957
1700
  }
1958
1701
  const preparedRenderedLists = []
1959
- const prepareListCallback = (callback, root, specialization, item, effectEntries) => {
1702
+ const prepareListCallback = (callback, root, specialization) => {
1960
1703
  const statements = [...specialization.hookDeclarations]
1961
1704
  if (specialization.effects.length) {
1962
1705
  usesListEffects = true
1963
1706
  statements.push(...specialization.effects.map(entry => {
1964
1707
  const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
1965
1708
  synthesizeTree(call)
1966
- const effectSource = entry.source.getSourceFile()
1967
- effectEntries.push({ node: call, item, source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
1709
+ ts.setOriginalNode(call, entry.source)
1968
1710
  return factory.createExpressionStatement(call)
1969
1711
  }))
1970
1712
  }
@@ -1995,13 +1737,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1995
1737
  ts.setParentRecursive(callback, false)
1996
1738
  callback.parent = originalParts.callback.parent
1997
1739
  }
1998
- const effectEntries = []
1999
- callback = prepareListCallback(callback, root, specialization, originalParts.item, effectEntries)
1740
+ callback = prepareListCallback(callback, root, specialization)
2000
1741
  const parts = {
2001
1742
  ...originalParts,
2002
1743
  root,
2003
1744
  callback,
2004
- effectEntries,
2005
1745
  specializations: [specialization.analysis?.slot, ...(specialization.specializations ?? [])].filter(slot => slot !== undefined),
2006
1746
  rowStates: [...specialization.rowStates, ...specialization.ordinaryStates],
2007
1747
  rowRefs: specialization.rowRefs,
@@ -2138,10 +1878,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2138
1878
  return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
2139
1879
  }
2140
1880
 
2141
- const listEffect = ts.isCallExpression(node) ? keyedEntry(activeKeyedBlock?.analysis.effects ?? [], node) : undefined
2142
- const componentEffect = ts.isCallExpression(node) ? componentEffectEntries.get(node) : undefined
2143
- const specializedEffect = listEffect ?? componentEffect
2144
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && node.expression.text === "useEffect" || specializedEffect)) {
1881
+ const effectAlias = ts.isCallExpression(node) && ts.isIdentifier(node.expression) ? node.expression.text : undefined
1882
+ const listEffect = effectAlias === "__kListUseEffect"
1883
+ const specializedEffect = listEffect || effectAlias === "__kComponentUseEffect" ? (() => {
1884
+ const source = ts.getOriginalNode(node)
1885
+ const sourceFile = source.getSourceFile()
1886
+ return { source, sourceFile, imports: clientImportBindings(sourceFile, sourceFile.fileName, sourceFiles) }
1887
+ })() : undefined
1888
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && effectAlias === "useEffect" || specializedEffect)) {
2145
1889
  const effectFail = (target, message) => {
2146
1890
  if (specializedEffect) throw sourceNodeError(specializedEffect.source, specializedEffect.sourceFile, message)
2147
1891
  fail(target, message)
@@ -2162,58 +1906,18 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2162
1906
  if (callback.asteriskToken) effectFail(callback, "useEffect() callback cannot be a generator")
2163
1907
  if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
2164
1908
  if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
2165
- const itemDependencies = []
2166
- const ordinaryDependencies = []
2167
1909
  const setters = settersForNode(node, settersByFunction)
2168
- let dependencyItem = listEffect?.item
2169
- for (const dependency of dependencies.elements) {
2170
- const value = unwrapExpression(dependency)
2171
- if (!dependencyItem && ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && isDestructuredParameter(value.expression, nearestFunction(node))) dependencyItem = value.expression.text
2172
- const field = dependencyItem && directProperty(dependency, dependencyItem)
2173
- if (field) {
2174
- if (["__proto__", "constructor", "prototype"].includes(field)) effectFail(dependency, `useEffect() keyed item property "${field}" is not supported`)
2175
- itemDependencies.push(field)
2176
- } else if (dependencyItem && referencesIdentifier(dependency, dependencyItem)) {
2177
- effectFail(dependency, "useEffect() keyed item dependencies must be direct item.<field> properties")
2178
- } else {
2179
- ordinaryDependencies.push(dependency)
2180
- }
2181
- }
2182
- const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
2183
- if (invalidDependency) effectFail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
2184
- const dependencyDerived = []
2185
- const dependencyStates = new Map()
2186
- const dependencySubstitutions = new Map()
2187
- const subscriptionDependencies = []
2188
- let hasDerivedDependency = false
2189
- const stateNames = new Set(setters.values())
2190
- const localDeclarations = jsxLocalDeclarations.get(nearestFunction(node))
2191
- for (const dependency of ordinaryDependencies) {
2192
- const entries = localDeclarations?.get(dependency.text)
2193
- const initializer = entries?.length === 1 ? entries[0].initializer : undefined
2194
- const directAlias = initializer && ts.isIdentifier(unwrapExpression(initializer)) && stateNames.has(unwrapExpression(initializer).text)
2195
- const derivedStates = initializer && !directAlias ? referencedStateNames(initializer, setters) : new Set()
2196
- if (derivedStates.size) {
2197
- const usedStates = new Set()
2198
- const expression = collectionExpression(initializer, { fail: effectFail, stateNames, selectorStates: usedStates })
2199
- if (!usedStates.size) effectFail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
2200
- dependencyDerived.push({ expression, states: usedStates, source: initializer })
2201
- for (const name of usedStates) {
2202
- subscriptionDependencies.push(factory.createIdentifier(name))
2203
- dependencyStates.set(name, factory.createIdentifier(name))
2204
- }
2205
- dependencySubstitutions.set(dependency.text, initializer)
2206
- hasDerivedDependency = true
2207
- } else {
2208
- subscriptionDependencies.push(dependency)
2209
- dependencyDerived.push({ expression: ["state", dependency.text], states: [dependency.text], source: dependency })
2210
- dependencyStates.set(dependency.text, dependency)
2211
- }
2212
- }
2213
- if (!hasDerivedDependency) {
2214
- dependencyDerived.length = 0
2215
- dependencyStates.clear()
2216
- }
1910
+ const dependencyAnalysis = analyzeEffectDependencies({
1911
+ dependencies,
1912
+ node,
1913
+ listEffect,
1914
+ keyedItem: activeKeyedBlock?.parts.item,
1915
+ setters,
1916
+ localDeclarations: jsxLocalDeclarations.get(nearestFunction(node)),
1917
+ factory,
1918
+ fail: effectFail
1919
+ })
1920
+ const { dependencyItem, itemDependencies, ordinaryDependencies, entries: dependencyEntries, dependencyStates, substitutions: dependencySubstitutions, subscriptions: subscriptionDependencies, hasDerived: hasDerivedDependency } = dependencyAnalysis
2217
1921
  if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
2218
1922
  if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
2219
1923
  const cleanupSubstitutions = new Map()
@@ -2239,17 +1943,19 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2239
1943
  validateEffectOwnedBrowserResources(callback, returns, effectFail)
2240
1944
  const callbackSource = specializedEffect?.sourceFile ?? sourceFile
2241
1945
  const callbackFile = callbackSource.fileName
2242
- const workerStart = workerReferences.length
2243
1946
  let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
2244
1947
  if (compiledCallback !== callback) {
2245
1948
  ts.setParentRecursive(compiledCallback, false)
2246
1949
  compiledCallback.parent = callback.parent
2247
1950
  }
1951
+ let workers = []
2248
1952
  if (listEffect && callbackFile !== file) {
2249
- const originalCallback = listEffect.source.arguments[0]
1953
+ const originalCallback = specializedEffect.source.arguments[0]
2250
1954
  workerCompiler.rejectConstructions(originalCallback, callbackSource, "Relative TypeScript Worker construction in imported keyed-row effects is not supported; construct the Worker in a directly compiled page or local component effect")
2251
1955
  } else {
2252
- compiledCallback = workerCompiler.rewriteEffect(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
1956
+ const rewritten = workerCompiler.rewriteEffect(compiledCallback, callbackFile, callbackSource, sourceFiles, factory, context)
1957
+ compiledCallback = rewritten.callback
1958
+ workers = rewritten.workers
2253
1959
  }
2254
1960
  const descriptor = descriptors.compileEffectCallback(compiledCallback, {
2255
1961
  setters,
@@ -2261,22 +1967,38 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2261
1967
  snapshotNested: returns.cleanup,
2262
1968
  liveStates: customHookTimerStates
2263
1969
  })
2264
- for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
2265
1970
  usesListItem ||= Boolean(itemDependencies.length && !listEffect)
2266
1971
  usesBehavior = true
2267
- const derivedDependencies = hasDerivedDependency ? dependencyDerived.map(entry => descriptors.registerDerived("expression", entry.expression, entry.states, entry.source)) : []
1972
+ const derivedDependencies = hasDerivedDependency ? dependencyEntries.map(entry => entry.kind === "derived" ? descriptors.registerDerived("expression", entry.expression, entry.states, entry.source) : undefined) : []
1973
+ const effectSource = specializedEffect?.source ?? node
1974
+ const lexicalOwner = nearestFunction(effectSource)
1975
+ const effect = descriptors.registerEffect(descriptor, {
1976
+ cleanup: returns.cleanup,
1977
+ dependencies: hasDerivedDependency ? dependencyEntries.map((entry, index) => entry.kind === "derived" ? { kind: "derived", derived: derivedDependencies[index].slot, sources: [...entry.states] } : { kind: "signal", name: entry.name }) : ordinaryDependencies.map(dependency => ({ kind: "signal", name: dependency.text })),
1978
+ subscriptions: (hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies).map(dependency => dependency.text),
1979
+ dependencyStates: [...dependencyStates.keys()],
1980
+ itemDependencies,
1981
+ ownership: {
1982
+ kind: activeKeyedBlock ? "keyed" : "component",
1983
+ ...(activeKeyedBlock ? { keyedBlock: activeKeyedBlock.block.slot } : {}),
1984
+ ...(lexicalOwner ? { component: { name: ownerName(lexicalOwner), ...(analysisSource(lexicalOwner) ? { source: analysisSource(lexicalOwner) } : {}) } } : {})
1985
+ },
1986
+ workers,
1987
+ ...(analysisSource(effectSource) ? { source: analysisSource(effectSource) } : {})
1988
+ })
1989
+ const dependencyExpressions = effect.dependencies.map(dependency => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependency.name])
2268
1990
  return factory.updateCallExpression(node, node.expression, node.typeArguments, [
2269
1991
  callback,
2270
- factory.createArrayLiteralExpression(hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies),
1992
+ factory.createArrayLiteralExpression(effect.subscriptions.map(name => factory.createIdentifier(name))),
2271
1993
  factory.createStringLiteral(handlerUrl),
2272
- factory.createStringLiteral(descriptor.exportName),
1994
+ factory.createStringLiteral(effect.setup.exportName),
2273
1995
  descriptor.states,
2274
1996
  descriptor.scope,
2275
1997
  factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
2276
- returns.cleanup ? factory.createTrue() : factory.createFalse(),
2277
- factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
2278
- hasDerivedDependency ? jsonExpression(derivedDependencies.map(entry => entry.expression), factory) : factory.createArrayLiteralExpression(),
2279
- factory.createArrayLiteralExpression([...dependencyStates].map(([name, state]) => factory.createArrayLiteralExpression([factory.createStringLiteral(name), state])))
1998
+ effect.cleanup ? factory.createTrue() : factory.createFalse(),
1999
+ factory.createArrayLiteralExpression(effect.itemDependencies.map(field => factory.createStringLiteral(field))),
2000
+ hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
2001
+ factory.createArrayLiteralExpression(effect.dependencyStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)])))
2280
2002
  ])
2281
2003
  }
2282
2004
 
@@ -2618,7 +2340,7 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
2618
2340
  const fail = (node, message) => {
2619
2341
  throw sourceNodeError(node, sourceFile, message)
2620
2342
  }
2621
- const analysis = { values: [], conditions: [], nested: [], effects: parts.effectEntries ?? [] }
2343
+ const analysis = { values: [], conditions: [], nested: [] }
2622
2344
  const root = parts.root
2623
2345
  const item = parts.item
2624
2346
  const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
@@ -2657,8 +2379,7 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
2657
2379
  ts.setParentRecursive(callback, false)
2658
2380
  callback.parent = nested.callback.parent
2659
2381
  }
2660
- const effectEntries = []
2661
- callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item, effectEntries)
2382
+ callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] })
2662
2383
  const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
2663
2384
  const nestedParts = {
2664
2385
  ...nested,
@@ -2666,7 +2387,6 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
2666
2387
  callback,
2667
2388
  state: parts.state,
2668
2389
  nested: true,
2669
- effectEntries,
2670
2390
  specializations: [specialization?.analysis?.slot, ...(specialization?.specializations ?? [])].filter(slot => slot !== undefined),
2671
2391
  rowStates: specializedStates,
2672
2392
  rowRefs: specialization?.rowRefs ?? [],
@@ -3429,30 +3149,6 @@ function localComponentDeclaration(sourceFile, name) {
3429
3149
  return undefined
3430
3150
  }
3431
3151
 
3432
- function validateEffectOwnedBrowserResources(callback, returns, fail) {
3433
- const observers = []
3434
- const frameAssignments = []
3435
- const cancellations = new Set()
3436
- const disconnected = new Set()
3437
- const insideCleanup = node => returns.cleanups.some(cleanup => {
3438
- for (let current = node; current; current = current.parent) if (current === cleanup) return true
3439
- return false
3440
- })
3441
- const visit = node => {
3442
- 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)
3443
- 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)
3444
- 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)
3445
- 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)
3446
- ts.forEachChild(node, visit)
3447
- }
3448
- visit(callback.body)
3449
- for (const observer of observers) if (!disconnected.has(observer.name.text)) fail(observer, `IntersectionObserver effects must disconnect ${JSON.stringify(observer.name.text)} in cleanup`)
3450
- for (const assignment of frameAssignments) {
3451
- const name = unwrapExpression(assignment.left).text
3452
- if (!cancellations.has(name)) fail(assignment, `Animation loop effects must cancel ${JSON.stringify(name)} in cleanup`)
3453
- }
3454
- }
3455
-
3456
3152
  async function collectClientModules(entries, sourceFiles) {
3457
3153
  const modules = new Set()
3458
3154
  const queue = [...new Set(entries)]
@@ -3910,14 +3606,6 @@ function navigationDomainsOverlap(left, right) {
3910
3606
  return left.segments.length === right.segments.length && left.segments.every((segment, index) => segment === null || right.segments[index] === null || segment === right.segments[index])
3911
3607
  }
3912
3608
 
3913
- function specializeNavigationTextDescriptors(source) {
3914
- const dynamic = source
3915
- .replace("const textDescriptors = globalThis.__KUDZU_TEXT_BINDINGS__ && typeof document !== \"undefined\" ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []", "const textDescriptors = () => globalThis.__KUDZU_TEXT_BINDINGS__ ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []")
3916
- .replace("const descriptor = textDescriptors[Number(node.data.slice(\"k-text:\".length))]", "const descriptor = textDescriptors()[Number(node.data.slice(\"k-text:\".length))]")
3917
- if (dynamic === source) throw new Error("Navigation text descriptor specialization did not match binding-runtime.js")
3918
- return dynamic
3919
- }
3920
-
3921
3609
  function normalizeBase(value) {
3922
3610
  if (value == null || value === "" || value === "/") return ""
3923
3611
  if (typeof value !== "string" || !value.startsWith("/") || /[?#\0]/.test(value) || /%(?:2f|5c)/i.test(value)) throw new Error("kudzu.config base must be a root-relative path")
@@ -3950,6 +3638,7 @@ const workerCompiler = createWorkerCompiler({
3950
3638
  })
3951
3639
 
3952
3640
  const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
3641
+ const printParamEntry = createParamCodegen({ browserPath, inlineJson, relativeModulePath })
3953
3642
  const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
3954
3643
  const printHandlerModule = createHandlerCodegen({
3955
3644
  resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))