@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.
- package/MIGRATION_ROADMAP.md +13 -0
- package/PERFORMANCE.md +48 -0
- package/README.md +1 -1
- package/RELEASES.md +71 -0
- package/docs/next-architecture/README.md +2 -2
- package/docs/next-architecture/compiler-current-architecture.md +19 -17
- package/docs/next-architecture/goal-a-compiler-foundation.md +15 -18
- package/docs/next-architecture/versioning.md +2 -2
- package/framework/README.md +8 -4
- package/framework/build.mjs +90 -401
- package/framework/compiler/descriptor-session.mjs +11 -2
- package/framework/compiler/effect-analysis.mjs +89 -0
- package/framework/compiler/ir/module-ir.mjs +7 -1
- package/framework/compiler/list-runtime-codegen.mjs +95 -0
- package/framework/compiler/param-codegen.mjs +72 -0
- package/framework/compiler/route-capability-planner.mjs +35 -0
- package/framework/compiler/runtime-codegen.mjs +146 -0
- package/framework/compiler/worker-compiler.mjs +12 -6
- package/framework/core.d.ts +41 -24
- package/framework/core.mjs +2 -1
- package/package.json +1 -1
package/framework/build.mjs
CHANGED
|
@@ -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
|
|
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
|
|
220
|
+
const capabilityIR = planRouteCapabilities(plans, { routes: routeCapabilities, navigationRouteCount: navigationRoutes.length })
|
|
212
221
|
const {
|
|
213
|
-
routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount,
|
|
214
|
-
events: { command: commandEvents,
|
|
215
|
-
bindings: { count: bindingCount
|
|
216
|
-
lists
|
|
217
|
-
|
|
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
|
-
} =
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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 ||
|
|
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
|
-
|
|
282
|
-
|
|
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 (
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
378
|
-
|
|
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
|
-
|
|
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('"', """).replaceAll("'", "'")
|
|
679
424
|
}
|
|
680
425
|
|
|
681
|
-
async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2142
|
-
const
|
|
2143
|
-
const specializedEffect = listEffect
|
|
2144
|
-
|
|
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
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
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 =
|
|
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
|
-
|
|
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 ?
|
|
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(
|
|
1992
|
+
factory.createArrayLiteralExpression(effect.subscriptions.map(name => factory.createIdentifier(name))),
|
|
2271
1993
|
factory.createStringLiteral(handlerUrl),
|
|
2272
|
-
factory.createStringLiteral(
|
|
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
|
-
|
|
2277
|
-
factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
|
|
2278
|
-
hasDerivedDependency ? jsonExpression(
|
|
2279
|
-
factory.createArrayLiteralExpression(
|
|
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: []
|
|
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
|
-
|
|
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))
|