@kudzujs/core 0.8.62 → 0.9.0
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 +36 -1
- package/PERFORMANCE.md +79 -1
- package/README.md +2 -2
- package/RELEASES.md +29 -0
- package/bin/kudzu.mjs +10 -1
- package/docs/next-architecture/0.9-baseline.md +1199 -0
- package/docs/next-architecture/0.9-benchmark-contracts.md +507 -0
- package/docs/next-architecture/0.9-component-property-contract.md +89 -0
- package/docs/next-architecture/0.9-compression-ledger.md +227 -0
- package/docs/next-architecture/0.9-final-proof-audit.md +176 -0
- package/docs/next-architecture/0.9-implementation-plan.md +1819 -0
- package/docs/next-architecture/0.9-resource-lifecycle.md +118 -0
- package/docs/next-architecture/0.9-semantic-compression.md +384 -0
- package/docs/next-architecture/README.md +16 -12
- package/docs/next-architecture/compiler-current-architecture.md +7 -7
- package/docs/next-architecture/large-application-ai-native-roadmap.md +5 -3
- package/docs/next-architecture/versioning.md +1 -1
- package/framework/README.md +2 -0
- package/framework/binding-runtime.js +4 -4
- package/framework/build.mjs +135 -30
- package/framework/compiler/ast-helpers.mjs +5 -0
- package/framework/compiler/browser-signal-passes.mjs +2 -7
- package/framework/compiler/collection-analysis.mjs +4 -0
- package/framework/compiler/descriptor-session.mjs +36 -12
- package/framework/compiler/effect-analysis.mjs +28 -8
- package/framework/compiler/effect-codegen.mjs +79 -36
- package/framework/compiler/effect-private-ref-pass.mjs +4 -8
- package/framework/compiler/handler-lowering.mjs +12 -7
- package/framework/compiler/ir/module-ir.mjs +26 -4
- package/framework/compiler/list-runtime-codegen.mjs +4 -2
- package/framework/compiler/optimize/command-specialization.mjs +4 -7
- package/framework/compiler/route-artifact-report.mjs +4 -3
- package/framework/compiler/route-build-record.mjs +12 -0
- package/framework/compiler/route-capability-planner.mjs +3 -3
- package/framework/compiler/route-ir.mjs +27 -11
- package/framework/compiler/runtime-codegen.mjs +2 -2
- package/framework/compiler/source-compiler.mjs +359 -78
- package/framework/core.d.ts +1 -0
- package/framework/core.mjs +18 -5
- package/framework/dependency-runtime.js +1 -1
- package/framework/effect-runtime.js +2 -2
- package/framework/list-runtime.js +67 -24
- package/framework/native-runtime.js +12 -9
- package/framework/runtime.js +1 -1
- package/framework/serialization.js +13 -6
- package/framework/shared-runtime.js +14 -12
- package/package.json +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Completed compiler-foundation record and longer-term plan after `0.8.35`. The active post-`0.8.62` execution queue is [`0.9-semantic-compression.md`](./0.9-semantic-compression.md). This document does not mark any remaining planned capability as supported and does not authorize a React runtime, VDOM, hydration, retained browser component tree, generic rerenderer, public store/query/resource API, SPA router, or islands.
|
|
6
6
|
|
|
7
|
-
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains authoritative for product invariants and fixture-driven feature selection.
|
|
7
|
+
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains authoritative for product invariants and fixture-driven feature selection. [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) is authoritative for current work order and evidence; this plan retains the completed foundation, deferred program, and long-term production gates. If implementation evidence changes either boundary, update the relevant document before broadening a patch.
|
|
8
8
|
|
|
9
9
|
## Product Outcome
|
|
10
10
|
|
|
@@ -453,6 +453,8 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
453
453
|
|
|
454
454
|
## Production Gates Before 1.0
|
|
455
455
|
|
|
456
|
+
- The active `0.9.0` cross-framework gate proves lower browser cost and matched user-facing performance against React + Vite, Vue, Svelte, and Astro before AI productivity claims are considered.
|
|
457
|
+
- The maintained AI delivery suite uses the same model, tools, requirements, budgets, and acceptance checks across frameworks, includes failed attempts, and establishes the highest success rate plus lowest median cost per successful task before `1.0.0`.
|
|
456
458
|
- Async native and effect work cannot write after ownership release.
|
|
457
459
|
- Build output is staged, collision-safe, and rollback/recovery guarded.
|
|
458
460
|
- Source maps connect generated route code to TS/TSX diagnostics.
|
|
@@ -481,4 +483,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
481
483
|
|
|
482
484
|
## Immediate Decision
|
|
483
485
|
|
|
484
|
-
PR 1 through PR 12, the `0.8.40` property-dependency slice, the `0.8.41` and `0.8.43` direct multi-boundary callback/ref slices, the `0.8.44` Context alias slice, the `0.8.46` action-only Provider setter slice, the `0.8.47` direct primitive, `0.8.57` plain-object prop, `0.8.58` direct keyed item, `0.8.59` direct array prop initializer, `0.8.60` matching array-draft setter-effect, `0.8.61` parameterized primitive debounce-hook, and `0.8.62` direct-ref outside-click-hook slices, the `0.8.48` repeated direct leaf-handler callback slice, the `0.8.49` direct child callback fan-out slice, the `0.8.50` package-neutral shared-state/action slice, the `0.8.51` owned-effect package import slice, the `0.8.52` effect-private mutable-ref slice, the `0.8.53` route/layout CSS closure slice, the `0.8.54` structural per-route capability/chunk report, the `0.8.55` signature-keyed runtime families, item 7 incremental source and affected-route builds, and the `0.8.42` measured route-output optimization are complete. Continue
|
|
486
|
+
PR 1 through PR 12, the `0.8.40` property-dependency slice, the `0.8.41` and `0.8.43` direct multi-boundary callback/ref slices, the `0.8.44` Context alias slice, the `0.8.46` action-only Provider setter slice, the `0.8.47` direct primitive, `0.8.57` plain-object prop, `0.8.58` direct keyed item, `0.8.59` direct array prop initializer, `0.8.60` matching array-draft setter-effect, `0.8.61` parameterized primitive debounce-hook, and `0.8.62` direct-ref outside-click-hook slices, the `0.8.48` repeated direct leaf-handler callback slice, the `0.8.49` direct child callback fan-out slice, the `0.8.50` package-neutral shared-state/action slice, the `0.8.51` owned-effect package import slice, the `0.8.52` effect-private mutable-ref slice, the `0.8.53` route/layout CSS closure slice, the `0.8.54` structural per-route capability/chunk report, the `0.8.55` signature-keyed runtime families, item 7 incremental source and affected-route builds, and the `0.8.42` measured route-output optimization are complete. Continue with `0.9.0-01` in the active Semantic Compression plan; keep ResourceIR limited to qualifying independent fixtures, and do not add range ownership, virtualization, optimistic transactions, a public adapter/store API, or a router before evidence justifies them.
|
|
@@ -65,7 +65,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
65
65
|
|
|
66
66
|
## Generator Versions
|
|
67
67
|
|
|
68
|
-
`create-kudzu@0.1.
|
|
68
|
+
`create-kudzu@0.1.103` retains the explicit install instructions and generates projects with `@kudzujs/core@^0.9.0`.
|
|
69
69
|
|
|
70
70
|
## Release Boundary
|
|
71
71
|
|
package/framework/README.md
CHANGED
|
@@ -94,4 +94,6 @@ A direct setter or inline/simple `const` setter callback may cross one same-file
|
|
|
94
94
|
|
|
95
95
|
`kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
|
|
96
96
|
|
|
97
|
+
A direct object-state prop may expose one-segment static fields when its same-file or relative-imported child directly maps an array field. Scalar bindings and that selected array effect dependency reuse the parent signal, existing `Object.is` comparison, and binding-backed keyed ownership without field state or a browser component. Dynamic paths, aliases, mutation, child state/ref/ID hooks, and opaque call-site values remain unsupported.
|
|
98
|
+
|
|
97
99
|
Cross-framework performance tables are historical snapshots from excluded workspaces and are not current rankings. `npm run benchmark` tracks the Worker build/graph fixture; `RUNS=21 npm run benchmark:keyed` tracks large keyed restoration; `BASELINE_ROOT=... npm run benchmark:native` tracks native dispatch and exact artifact changes. Current methodology, raw arrays, external-fixture limits, and artifact deltas live in `PERFORMANCE.md` and the web docs.
|
|
@@ -56,10 +56,10 @@ function commitBindings(id) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
registerCommitter(commitBindings)
|
|
59
|
-
registerMountHook(mountBindings)
|
|
60
|
-
registerMountHook(mountConditions)
|
|
61
|
-
registerUnmountHook(unmountBindings)
|
|
62
|
-
registerUnmountHook(unmountConditions)
|
|
59
|
+
registerMountHook(mountBindings, "bindings")
|
|
60
|
+
registerMountHook(mountConditions, "conditions")
|
|
61
|
+
registerUnmountHook(unmountBindings, "bindings")
|
|
62
|
+
registerUnmountHook(unmountConditions, "conditions")
|
|
63
63
|
registerStateReleaseHook(releaseBindings)
|
|
64
64
|
|
|
65
65
|
if (typeof document !== "undefined") mountDom(document)
|
package/framework/build.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
|
|
|
7
7
|
import { generateListRuntime } from "./compiler/list-runtime-codegen.mjs"
|
|
8
8
|
import { assetPath, browserPath, relativeModulePath, withBase } from "./compiler/path-helpers.mjs"
|
|
9
9
|
import { createProjectSession } from "./compiler/project-session.mjs"
|
|
10
|
-
import { createRouteBuildRecord, planRouteArtifacts } from "./compiler/route-build-record.mjs"
|
|
10
|
+
import { createRouteBuildRecord, planRouteArtifacts, releaseRouteBuildRecordPlan } from "./compiler/route-build-record.mjs"
|
|
11
11
|
import { createRouteArtifactReport } from "./compiler/route-artifact-report.mjs"
|
|
12
12
|
import { planRuntimeFamilies } from "./compiler/runtime-family-planner.mjs"
|
|
13
13
|
import { createSourceCompiler } from "./compiler/source-compiler.mjs"
|
|
@@ -33,10 +33,10 @@ async function loadConfig(root) {
|
|
|
33
33
|
|
|
34
34
|
export async function build({ quiet = false, minify = true, root: projectRoot = process.cwd() } = {}) {
|
|
35
35
|
const project = createProjectSession(projectRoot)
|
|
36
|
-
return buildWithSession(project, { quiet, minify })
|
|
36
|
+
return buildWithSession(project, { quiet, minify, retainCache: false })
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export async function buildWithSession(project, { changedFiles, quiet = false, minify = true } = {}) {
|
|
39
|
+
export async function buildWithSession(project, { changedFiles, quiet = false, minify = true, retainCache = true } = {}) {
|
|
40
40
|
const { root, outputDirectory } = project
|
|
41
41
|
const stagedOutput = join(root, ".kudzu-dist-staging")
|
|
42
42
|
const backupOutput = join(root, ".kudzu-dist-backup")
|
|
@@ -45,9 +45,9 @@ export async function buildWithSession(project, { changedFiles, quiet = false, m
|
|
|
45
45
|
try {
|
|
46
46
|
await recoverOutput(outputDirectory, backupOutput)
|
|
47
47
|
await rm(stagedOutput, { recursive: true, force: true })
|
|
48
|
-
const { result, pageCount, behaviorCount, cache } = await buildInto(project, stagedOutput, { changedFiles, minify })
|
|
48
|
+
const { result, pageCount, behaviorCount, cache } = await buildInto(project, stagedOutput, { changedFiles, minify, retainCache })
|
|
49
49
|
await promoteOutput(stagedOutput, outputDirectory, backupOutput)
|
|
50
|
-
project.buildCache = cache
|
|
50
|
+
project.buildCache = retainCache ? cache : undefined
|
|
51
51
|
if (!quiet) console.log(`Built ${pageCount} page(s), ${behaviorCount} interactive page(s) into dist/`)
|
|
52
52
|
return result
|
|
53
53
|
} finally {
|
|
@@ -63,12 +63,12 @@ export async function buildWithSession(project, { changedFiles, quiet = false, m
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
66
|
+
async function buildInto(project, outputDirectory, { changedFiles, minify, retainCache }) {
|
|
67
67
|
const { root, sourceDirectory, pagesDirectory, workDirectory } = project
|
|
68
|
-
const previous = project.buildCache
|
|
68
|
+
const previous = retainCache ? project.buildCache : undefined
|
|
69
69
|
project.buildGeneration = (project.buildGeneration ?? 0) + 1
|
|
70
70
|
project.buildDirectory = project.buildGeneration > 1 ? join(workDirectory, "build", String(project.buildGeneration)) : workDirectory
|
|
71
|
-
const { collectClientModules, compileClientModule, compiledPath,
|
|
71
|
+
const { collectClientModules, compileClientModule, compiledPath, compileSourceAsync, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles } = createSourceCompiler(project)
|
|
72
72
|
const config = await loadConfig(root)
|
|
73
73
|
const base = normalizeBase(config.base)
|
|
74
74
|
const configuredStyles = normalizeStyles(config.styles, base, project)
|
|
@@ -122,7 +122,7 @@ async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
|
122
122
|
if (file.endsWith(".worker.ts")) continue
|
|
123
123
|
let result = !affectedSources.has(file) ? previous?.sourceResults.get(file) : undefined
|
|
124
124
|
if (!result) {
|
|
125
|
-
result =
|
|
125
|
+
result = await compileSourceAsync(file, sourceFileSet, sourceIndex, staticFiles, cssModules, base)
|
|
126
126
|
compiledModules++
|
|
127
127
|
}
|
|
128
128
|
result = { ...result, buildModule: { ...result.buildModule, path: relative(root, compiledPath(file)).replaceAll(sep, "/") } }
|
|
@@ -139,12 +139,14 @@ async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
|
139
139
|
if (!handler || handler.kind !== "module-export" || handler.role !== "effect") throw new Error(`EffectIR ${effect.slot} has no effect HandlerIR`)
|
|
140
140
|
return effect.workers.map(worker => ({ ...worker, module: assetPath(base, `assets/${result.handlerModule.path}`), handler: handler.exportName }))
|
|
141
141
|
}))
|
|
142
|
+
globalThis.gc?.()
|
|
142
143
|
|
|
143
144
|
let routeRecords = []
|
|
144
145
|
const routeDrafts = []
|
|
145
146
|
const routeEntryTransforms = new Map()
|
|
146
147
|
const routeEntrySources = new Map()
|
|
147
148
|
const routeEntryPaths = new Map()
|
|
149
|
+
const routePlanPools = Object.fromEntries(["states", "params", "searchParams", "effects", "conditions", "lists", "commands", "nativeStates", "nativeScope", "bindingStates", "bindingScope", "scopeStates", "scopeBindings"].map(name => [name, new Map()]))
|
|
148
150
|
const rewrites = []
|
|
149
151
|
const emittedRoutes = new Set()
|
|
150
152
|
const emittedApplicationRoutes = new Set()
|
|
@@ -249,8 +251,8 @@ async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
|
249
251
|
navigationGroup.hasEffects ||= result.hasEffects
|
|
250
252
|
navigationGroup.hasParams ||= result.hasParams
|
|
251
253
|
}
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
+
const plan = internRoutePlan({ route: routePath, ...result.plan }, routePlanPools)
|
|
255
|
+
const usesDependencyRuntime = usesRouteDependencyRuntime({ plan, navigable, hasBindings: result.hasBindings, hasLists: result.hasLists }, false)
|
|
254
256
|
const entries = {
|
|
255
257
|
...(result.hasParams ? { param: paramPath } : {}),
|
|
256
258
|
...(result.hasEffects ? { effect: effectPath } : {}),
|
|
@@ -279,7 +281,13 @@ async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
|
279
281
|
})
|
|
280
282
|
routeRecords.push(record)
|
|
281
283
|
if (navigationGroup) navigationGroup.buildRecords.push(record)
|
|
282
|
-
routeDrafts.push({ record,
|
|
284
|
+
routeDrafts.push({ record, runtimeSchema, navigationGroup, applicationRoute, effectPath, nativePath, paramPath })
|
|
285
|
+
if (!retainCache && config.afterBuild === undefined) {
|
|
286
|
+
const routeDirectory = join(outputDirectory, record.output)
|
|
287
|
+
await mkdir(routeDirectory, { recursive: true })
|
|
288
|
+
await writeFile(join(routeDirectory, "index.html"), record.html)
|
|
289
|
+
record.html = ""
|
|
290
|
+
}
|
|
283
291
|
}
|
|
284
292
|
pageRenders.set(pageFile, {
|
|
285
293
|
drafts: routeDrafts.slice(draftOffset).map(({ navigationGroup: _, ...draft }) => draft),
|
|
@@ -303,14 +311,16 @@ async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
|
303
311
|
group.assetPath = assetPath(base, `assets/runtime/${group.runtimeFamily.id}/${group.assetName}`)
|
|
304
312
|
}
|
|
305
313
|
const runtimeFamilyByRecord = new Map()
|
|
306
|
-
|
|
307
|
-
|
|
314
|
+
const finalizedRecords = []
|
|
315
|
+
for (const draft of routeDrafts) {
|
|
316
|
+
const { record, runtimeSchema, navigationGroup, effectPath, nativePath, paramPath } = draft
|
|
308
317
|
const family = runtimePlan.familyByRecord.get(record)
|
|
309
318
|
if (record.capabilities.hasBehaviors && !family) throw new Error(`Interactive route has no runtime family: ${record.route}`)
|
|
310
319
|
const runtimeDirectory = family ? join(outputDirectory, "assets", "runtime", family.id) : undefined
|
|
311
320
|
const routeRuntimeName = record.capabilities.usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
|
|
312
321
|
const entries = {}
|
|
313
|
-
|
|
322
|
+
const routeFile = join(outputDirectory, record.output, "index.html")
|
|
323
|
+
let html = retainCache || config.afterBuild !== undefined ? record.html : await readFile(routeFile, "utf8")
|
|
314
324
|
if (record.capabilities.hasParams) {
|
|
315
325
|
const entry = retainRouteEntry(paramPath, output => printParamEntry(runtimeSchema, record.plan.params, record.plan.searchParams, record.plan.searchParamsWritable, output, runtimeDirectory, base, routeRuntimeName, record.capabilities.navigable), routeEntrySources, routeEntryPaths, outputDirectory)
|
|
316
326
|
entries.param = entry.path
|
|
@@ -330,25 +340,31 @@ async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
|
330
340
|
}
|
|
331
341
|
if (family) {
|
|
332
342
|
html = html.replaceAll(runtimePlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/${routeRuntimeName}`)))
|
|
333
|
-
html = html.replaceAll(bindingPlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/kudzu-binding.js`)))
|
|
334
|
-
html = html.replaceAll(listPlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/kudzu-list.js`)))
|
|
343
|
+
if (record.capabilities.hasBindings) html = html.replaceAll(bindingPlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/kudzu-binding.js`)))
|
|
344
|
+
if (record.capabilities.hasLists) html = html.replaceAll(listPlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/kudzu-list.js`)))
|
|
335
345
|
}
|
|
336
346
|
if (navigationGroup) html = html.replaceAll(escapeAttribute(navigationAssets.get(record.route)), escapeAttribute(navigationGroup.assetPath))
|
|
337
|
-
const finalRecord = createRouteBuildRecord({
|
|
347
|
+
const finalRecord = retainCache ? createRouteBuildRecord({
|
|
338
348
|
route: record.route,
|
|
339
349
|
output: record.output,
|
|
340
350
|
html,
|
|
341
351
|
plan: record.plan,
|
|
342
|
-
handlerReferences:
|
|
352
|
+
handlerReferences: record.artifacts.handlers,
|
|
343
353
|
styles: record.artifacts.styles,
|
|
344
354
|
capabilities: record.capabilities,
|
|
345
355
|
entries,
|
|
346
356
|
runtimeSchema
|
|
347
|
-
})
|
|
357
|
+
}) : Object.assign(record, { html: "", entries })
|
|
358
|
+
if (!retainCache) {
|
|
359
|
+
if ([runtimePlaceholder, bindingPlaceholder, listPlaceholder].some(placeholder => html.includes(placeholder))) throw new Error(`Runtime family placeholder survived in ${record.route}`)
|
|
360
|
+
await mkdir(dirname(routeFile), { recursive: true })
|
|
361
|
+
await writeFile(routeFile, preloadModules(html))
|
|
362
|
+
}
|
|
348
363
|
if (family) runtimeFamilyByRecord.set(finalRecord, family)
|
|
349
364
|
if (navigationGroup) navigationAssets.set(finalRecord.route, navigationGroup.assetPath)
|
|
350
|
-
|
|
351
|
-
}
|
|
365
|
+
finalizedRecords.push(finalRecord)
|
|
366
|
+
}
|
|
367
|
+
routeRecords = finalizedRecords
|
|
352
368
|
|
|
353
369
|
const assetsDirectory = join(outputDirectory, "assets")
|
|
354
370
|
await mkdir(assetsDirectory, { recursive: true })
|
|
@@ -364,9 +380,15 @@ async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
|
364
380
|
}
|
|
365
381
|
if (module.code.includes("/__kudzu_worker_")) throw new Error(`Worker URL placeholder survived in ${module.path}`)
|
|
366
382
|
}
|
|
367
|
-
const
|
|
383
|
+
const sortedRewrites = rewrites.sort((left, right) => runtimeSpecificity(right) - runtimeSpecificity(left) || left.pattern.localeCompare(right.pattern))
|
|
384
|
+
const releaseRoutePlans = !retainCache && config.afterBuild === undefined
|
|
385
|
+
const plans = releaseRoutePlans ? undefined : routeRecords.map(record => record.plan)
|
|
386
|
+
if (releaseRoutePlans) {
|
|
387
|
+
await writeRoutePlans(join(workDirectory, "kudzu-plan.json"), routeRecords, sortedRewrites, runtimeFamilyByRecord, routeDrafts)
|
|
388
|
+
globalThis.gc?.()
|
|
389
|
+
}
|
|
368
390
|
const behaviorCount = routeRecords.filter(record => record.capabilities.hasBehaviors).length
|
|
369
|
-
for (let offset = 0; offset < routeRecords.length; offset += 64) {
|
|
391
|
+
for (let offset = 0; retainCache && offset < routeRecords.length; offset += 64) {
|
|
370
392
|
await Promise.all(routeRecords.slice(offset, offset + 64).map(async record => {
|
|
371
393
|
const routeDirectory = join(outputDirectory, record.output)
|
|
372
394
|
await mkdir(routeDirectory, { recursive: true })
|
|
@@ -451,10 +473,9 @@ async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
|
451
473
|
handlerMetafile = result.metafile
|
|
452
474
|
await rm(join(assetsDirectory, "modules"), { recursive: true, force: true })
|
|
453
475
|
}
|
|
454
|
-
|
|
455
|
-
await writeFile(join(workDirectory, "kudzu-plan.json"), JSON.stringify({ routes: plans, rewrites: sortedRewrites }, null, 2))
|
|
476
|
+
if (!releaseRoutePlans) await writePrettyJson(join(workDirectory, "kudzu-plan.json"), { routes: plans, rewrites: sortedRewrites })
|
|
456
477
|
const artifacts = createRouteArtifactReport(routeRecords, { base, handlerMetafile, outputDirectory, navigationAssets, runtimeFamilies: runtimePlan.families, runtimeFamilyByRecord, workerReferences: renderedWorkerReferences, workerOutputs })
|
|
457
|
-
await
|
|
478
|
+
await writePrettyJson(join(workDirectory, "kudzu-artifacts.json"), artifacts)
|
|
458
479
|
const emittedCssFiles = new Set()
|
|
459
480
|
for (const file of cssFiles.filter(file => renderedStyleUrls.has(assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)))) {
|
|
460
481
|
const output = join(assetsDirectory, relative(sourceDirectory, file))
|
|
@@ -492,7 +513,7 @@ async function buildInto(project, outputDirectory, { changedFiles, minify }) {
|
|
|
492
513
|
const incremental = { compiledModules, renderedPages }
|
|
493
514
|
return {
|
|
494
515
|
result: { sourceResults, incremental },
|
|
495
|
-
pageCount:
|
|
516
|
+
pageCount: routeRecords.length,
|
|
496
517
|
behaviorCount,
|
|
497
518
|
cache: { pageRenders, pageSources, placeholders, sourceResults: sourceResultsByFile }
|
|
498
519
|
}
|
|
@@ -555,8 +576,8 @@ function replayPageRender(cached, state) {
|
|
|
555
576
|
if (navigationGroup) {
|
|
556
577
|
state.navigationAssets.set(draft.record.route, navigationGroup.assetPath)
|
|
557
578
|
navigationGroup.buildRecords.push(draft.record)
|
|
558
|
-
navigationGroup.hasEffects ||= draft.
|
|
559
|
-
navigationGroup.hasParams ||= draft.
|
|
579
|
+
navigationGroup.hasEffects ||= draft.record.capabilities.hasEffects
|
|
580
|
+
navigationGroup.hasParams ||= draft.record.capabilities.hasParams
|
|
560
581
|
}
|
|
561
582
|
state.routeRecords.push(draft.record)
|
|
562
583
|
state.routeDrafts.push({ ...draft, navigationGroup })
|
|
@@ -688,6 +709,7 @@ function runtimeEffects(effects, lifetimes = false) {
|
|
|
688
709
|
handler: effect.handler,
|
|
689
710
|
...(effect.dependencies ? { dependencies: effect.dependencies } : {}),
|
|
690
711
|
...(effect.dependencyExpressions ? { dependencyExpressions: effect.dependencyExpressions, dependencyStates: effect.dependencyStates } : {}),
|
|
712
|
+
...(effect.dependencyEvaluators ? { dependencyEvaluators: effect.dependencyEvaluators } : {}),
|
|
691
713
|
...(effect.itemDependencies ? { itemDependencies: effect.itemDependencies, listState: effect.listState } : {}),
|
|
692
714
|
...(effect.cleanup ? { cleanup: true } : {}),
|
|
693
715
|
...(effect.owner ? { owner: effect.owner } : {}),
|
|
@@ -698,11 +720,94 @@ function runtimeEffects(effects, lifetimes = false) {
|
|
|
698
720
|
}))
|
|
699
721
|
}
|
|
700
722
|
|
|
723
|
+
function internRoutePlan(plan, pools) {
|
|
724
|
+
for (const field of ["states", "params", "searchParams", "effects", "conditions", "lists"]) plan[field] = internJson(plan[field], pools[field])
|
|
725
|
+
for (const event of plan.events) {
|
|
726
|
+
if (event.commands) event.commands = internJson(event.commands, pools.commands)
|
|
727
|
+
if (event.native) {
|
|
728
|
+
event.native.states = internJson(event.native.states, pools.nativeStates)
|
|
729
|
+
event.native.scope = internJson(event.native.scope, pools.nativeScope)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
for (const binding of plan.bindings) {
|
|
733
|
+
if (binding.states) binding.states = internJson(binding.states, pools.bindingStates)
|
|
734
|
+
if (binding.scope) binding.scope = internJson(binding.scope, pools.bindingScope)
|
|
735
|
+
if (binding.scopeStates) binding.scopeStates = internJson(binding.scopeStates, pools.scopeStates)
|
|
736
|
+
if (binding.scopeBindings) binding.scopeBindings = internJson(binding.scopeBindings, pools.scopeBindings)
|
|
737
|
+
}
|
|
738
|
+
return plan
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function internJson(value, pool) {
|
|
742
|
+
const encoded = JSON.stringify(value)
|
|
743
|
+
const existing = pool.get(encoded)
|
|
744
|
+
if (existing !== undefined) return existing
|
|
745
|
+
pool.set(encoded, value)
|
|
746
|
+
return value
|
|
747
|
+
}
|
|
748
|
+
|
|
701
749
|
async function writeJavaScript(file, source, minify, define) {
|
|
702
750
|
const code = minify || define ? (await transform(source, { define, format: "esm", legalComments: "none", minify, target: "es2022" })).code : source
|
|
703
751
|
await writeFile(file, code)
|
|
704
752
|
}
|
|
705
753
|
|
|
754
|
+
async function writePrettyJson(file, value) {
|
|
755
|
+
const entries = Object.entries(value)
|
|
756
|
+
if (!entries.some(([, entry]) => Array.isArray(entry) && entry.length > 2048)) {
|
|
757
|
+
await writeFile(file, JSON.stringify(value, null, 2))
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
const output = await open(file, "w")
|
|
761
|
+
try {
|
|
762
|
+
await output.write("{\n")
|
|
763
|
+
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
|
|
764
|
+
const [key, entry] = entries[entryIndex]
|
|
765
|
+
const comma = entryIndex === entries.length - 1 ? "" : ","
|
|
766
|
+
if (!Array.isArray(entry) || entry.length <= 2048) {
|
|
767
|
+
await output.write(` ${JSON.stringify(key)}: ${JSON.stringify(entry, null, 2).replaceAll("\n", "\n ")}${comma}\n`)
|
|
768
|
+
continue
|
|
769
|
+
}
|
|
770
|
+
await output.write(` ${JSON.stringify(key)}: [\n`)
|
|
771
|
+
for (let offset = 0; offset < entry.length; offset += 64) {
|
|
772
|
+
const batch = entry.slice(offset, offset + 64).map((item, index) => {
|
|
773
|
+
const separator = offset + index === entry.length - 1 ? "" : ","
|
|
774
|
+
return ` ${JSON.stringify(item, null, 2).replaceAll("\n", "\n ")}${separator}\n`
|
|
775
|
+
}).join("")
|
|
776
|
+
await output.write(batch)
|
|
777
|
+
}
|
|
778
|
+
await output.write(` ]${comma}\n`)
|
|
779
|
+
}
|
|
780
|
+
await output.write("}")
|
|
781
|
+
} finally {
|
|
782
|
+
await output.close()
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function writeRoutePlans(file, records, rewrites, familyByRecord, drafts) {
|
|
787
|
+
const output = await open(file, "w")
|
|
788
|
+
try {
|
|
789
|
+
await output.write(records.length ? '{\n "routes": [\n' : '{\n "routes": []')
|
|
790
|
+
for (let offset = 0; offset < records.length; offset += 64) {
|
|
791
|
+
const batch = records.slice(offset, offset + 64).map((record, index) => {
|
|
792
|
+
const recordIndex = offset + index
|
|
793
|
+
const separator = recordIndex === records.length - 1 ? "" : ","
|
|
794
|
+
const encoded = ` ${JSON.stringify(record.plan, null, 2).replaceAll("\n", "\n ")}${separator}\n`
|
|
795
|
+
const family = familyByRecord.get(record)
|
|
796
|
+
if (family && !family.navigation) {
|
|
797
|
+
releaseRouteBuildRecordPlan(record)
|
|
798
|
+
if (drafts[recordIndex].record !== record) releaseRouteBuildRecordPlan(drafts[recordIndex].record)
|
|
799
|
+
}
|
|
800
|
+
return encoded
|
|
801
|
+
}).join("")
|
|
802
|
+
await output.write(batch)
|
|
803
|
+
if ((offset + 64) % 512 === 0) globalThis.gc?.()
|
|
804
|
+
}
|
|
805
|
+
await output.write(`${records.length ? " ]" : ""},\n "rewrites": ${JSON.stringify(rewrites, null, 2).replaceAll("\n", "\n ")}\n}`)
|
|
806
|
+
} finally {
|
|
807
|
+
await output.close()
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
706
811
|
export async function writeRouteEntry(file, source, minify, transforms, transformSource = transform, write = writeFile) {
|
|
707
812
|
let code = transforms.get(source)
|
|
708
813
|
if (code === undefined) {
|
|
@@ -160,6 +160,11 @@ export function sourceLocation(node, fallbackSource) {
|
|
|
160
160
|
return `${sourceFile.fileName}:${position.line + 1}:${position.character + 1}`
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
export function isNodeWithin(node, root) {
|
|
164
|
+
for (let current = node; current; current = current.parent) if (current === root) return true
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
|
|
163
168
|
export function effectReturns(callback) {
|
|
164
169
|
let cleanup = false
|
|
165
170
|
let invalid
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript"
|
|
2
|
-
import { bindingNames, containsJsx, functionVarDeclaresName, importDeclarationNames, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
2
|
+
import { bindingNames, containsJsx, functionVarDeclaresName, importDeclarationNames, isNodeWithin, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
3
|
|
|
4
4
|
export function normalizeMediaQueryExternalStores(sourceFile, factory, context) {
|
|
5
5
|
const imports = sourceFile.statements.filter(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react" && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings))
|
|
@@ -58,7 +58,7 @@ export function normalizeMediaQueryExternalStores(sourceFile, factory, context)
|
|
|
58
58
|
}
|
|
59
59
|
inspect(sourceFile)
|
|
60
60
|
const references = referenceIdentifiers(sourceFile, "useSyncExternalStore")
|
|
61
|
-
if (references.length !== candidates.size) throw sourceNodeError(references.find(reference => ![...candidates.values()].some(candidate =>
|
|
61
|
+
if (references.length !== candidates.size) throw sourceNodeError(references.find(reference => ![...candidates.values()].some(candidate => isNodeWithin(reference, candidate.declaration.initializer))) ?? externalStoreImport.entry, sourceFile, "useSyncExternalStore is supported only for direct static media query declarations")
|
|
62
62
|
const directHooks = new Set(imports.flatMap(statement => statement.importClause.namedBindings.elements.filter(entry => !entry.propertyName).map(entry => entry.name.text)))
|
|
63
63
|
const missingHooks = ["useEffect", "useState"].filter(name => !directHooks.has(name))
|
|
64
64
|
for (const name of missingHooks) {
|
|
@@ -105,11 +105,6 @@ export function normalizeMediaQueryExternalStores(sourceFile, factory, context)
|
|
|
105
105
|
return ts.visitNode(sourceFile, visitor)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function insideNode(node, root) {
|
|
109
|
-
for (let current = node; current; current = current.parent) if (current === root) return true
|
|
110
|
-
return false
|
|
111
|
-
}
|
|
112
|
-
|
|
113
108
|
export function normalizeNavigatorCapabilityConditions(sourceFile, factory, context) {
|
|
114
109
|
const candidates = new Map()
|
|
115
110
|
let index = 0
|
|
@@ -45,6 +45,10 @@ export function analyzeCollectionPipeline(expression, options) {
|
|
|
45
45
|
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) {
|
|
46
46
|
const calculation = calculatedCollection?.(value)
|
|
47
47
|
if (calculation) return { calculation, selector: [], selectorStates: new Set() }
|
|
48
|
+
if (stateNames.has(value.expression.text)) {
|
|
49
|
+
if (["__proto__", "constructor", "prototype"].includes(value.name.text)) fail(value, `Rendered collection property "${value.name.text}" is not supported`)
|
|
50
|
+
return { state: value.expression, calculation: value, selector: [], selectorStates: new Set() }
|
|
51
|
+
}
|
|
48
52
|
return { state: undefined, ownerField: value.name.text, selector: [], parentItem: value.expression.text }
|
|
49
53
|
}
|
|
50
54
|
if (ts.isCallExpression(value) && ts.isIdentifier(value.expression) && importedCollectionTransforms.has(value.expression.text)) {
|
|
@@ -75,14 +75,30 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
75
75
|
: compileListExpression(read, expression, entry.item, entry.index, entry.states, entry.keyedBlock, indexed ? bindingIndex : undefined)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function compileReactiveBinding(expression, { setters, importBindings = new Map(), keyedBlock }) {
|
|
78
|
+
function compileReactiveBinding(expression, { setters, importBindings = new Map(), keyedBlock, derived }) {
|
|
79
79
|
const parts = conditionalParts(expression)
|
|
80
80
|
const state = parts && directStateIdentifier(parts.condition, setters, bindingIndex)
|
|
81
81
|
if (state && isPrimitiveLiteral(parts.truthy) && isPrimitiveLiteral(parts.falsy)) {
|
|
82
82
|
return { node: factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy]) }
|
|
83
83
|
}
|
|
84
84
|
const binding = reactiveBindings.length
|
|
85
|
-
|
|
85
|
+
const node = factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings, keyedBlock))
|
|
86
|
+
if (derived) reactiveBindings[binding].derived = derived
|
|
87
|
+
return { node, binding }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function compileDerivedEvaluator(expression, { setters, importBindings = new Map() }) {
|
|
91
|
+
const binding = reactiveBindings.length
|
|
92
|
+
const [, module, handler, states, scope] = compileReactiveExpression(expression, setters, importBindings)
|
|
93
|
+
return {
|
|
94
|
+
binding,
|
|
95
|
+
descriptor: factory.createObjectLiteralExpression([
|
|
96
|
+
factory.createPropertyAssignment("module", module),
|
|
97
|
+
factory.createPropertyAssignment("handler", handler),
|
|
98
|
+
factory.createPropertyAssignment("states", states),
|
|
99
|
+
factory.createPropertyAssignment("scope", scope)
|
|
100
|
+
])
|
|
101
|
+
}
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
function compileConditional(kind, expression, truthy, falsy, setters) {
|
|
@@ -125,12 +141,18 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
function compileEvent(expression, { owner = "module", stateOwners = new Map(), setters, reducers, functions, listItem, keyedBlock, importBindings }) {
|
|
128
|
-
|
|
144
|
+
const directFunction = ts.isIdentifier(expression) ? functions.get(expression.text) : undefined
|
|
145
|
+
const directReducer = ts.isIdentifier(expression) ? reducers.get(expression.text) : undefined
|
|
146
|
+
const directImplementation = directReducer?.sharedAction?.directImplementation
|
|
147
|
+
const directSource = directFunction && ts.getOriginalNode(directFunction)
|
|
148
|
+
const actionSource = directImplementation && ts.getOriginalNode(directImplementation)
|
|
149
|
+
const directAction = directSource?.pos >= 0 && directSource.pos === actionSource?.pos && directSource.end === actionSource.end && directSource.getSourceFile().fileName === actionSource.getSourceFile().fileName ? directReducer.sharedAction : undefined
|
|
150
|
+
if (ts.isIdentifier(expression)) expression = directFunction
|
|
129
151
|
if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
|
|
130
|
-
const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock)
|
|
152
|
+
const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock, directAction ? [directAction.slot] : [])
|
|
131
153
|
if (optimized) return optimized
|
|
132
154
|
rejectWorkerConstructions(expression)
|
|
133
|
-
const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem, keyedBlock, stateOwners })
|
|
155
|
+
const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem, keyedBlock, stateOwners, actionSlots: directAction ? [directAction.slot] : [] })
|
|
134
156
|
return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
|
|
135
157
|
factory.createStringLiteral(handlerUrl), factory.createStringLiteral(descriptor.exportName), descriptor.states, descriptor.scope
|
|
136
158
|
])
|
|
@@ -140,7 +162,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
140
162
|
return compileNativeCallback(expression, { ...options, entries: effectHandlers, prefix: "effect", role: "effect" })
|
|
141
163
|
}
|
|
142
164
|
|
|
143
|
-
function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, stateOwners, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
|
|
165
|
+
function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, stateOwners, actionSlots = [], deferValues = false, snapshotNested = false, liveStates = new Set() }) {
|
|
144
166
|
const indexedBindingIndex = indexedReferences(bindingIndex, expression, expression) ? bindingIndex : undefined
|
|
145
167
|
const allCaptures = nativeCaptureNames(expression, setters, indexedBindingIndex)
|
|
146
168
|
const usedReducers = referencedReducerDispatches(expression.body, reducers, expression, indexedBindingIndex)
|
|
@@ -151,10 +173,11 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
151
173
|
const usedStates = referencedStateNames(expression.body, setters, expression, indexedBindingIndex)
|
|
152
174
|
for (const name of usedReducers) {
|
|
153
175
|
const reducer = reducers.get(name)
|
|
154
|
-
|
|
176
|
+
const direct = reducer.directImplementation ?? reducer.sharedAction?.directImplementation
|
|
177
|
+
if (direct) for (const state of referencedStateNames(direct.body, reducer.states ?? reducer.sharedAction.states, direct, bindingIndex)) usedStates.add(state)
|
|
155
178
|
}
|
|
156
179
|
const exportName = `${prefix}${entries.length}`
|
|
157
|
-
const entry = { exportName, expression, captures, deferValues, imports, listItem, keyedBlock, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates, signalRefs: new Map([...usedStates].map(name => [name, signal(name, expression, stateOwners, [...setters].filter(([, state]) => state === name).map(([setter]) => setter))])), bindingIndex: indexedBindingIndex }
|
|
180
|
+
const entry = { actionSlots, exportName, expression, captures, deferValues, imports, listItem, keyedBlock, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates, signalRefs: new Map([...usedStates].map(name => [name, signal(name, expression, stateOwners, [...setters].filter(([, state]) => state === name).map(([setter]) => setter))])), bindingIndex: indexedBindingIndex }
|
|
158
181
|
entries.push(entry)
|
|
159
182
|
const value = name => deferValues
|
|
160
183
|
? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
|
|
@@ -172,14 +195,14 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
172
195
|
}
|
|
173
196
|
}
|
|
174
197
|
|
|
175
|
-
function compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock) {
|
|
198
|
+
function compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock, actions = []) {
|
|
176
199
|
const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
|
|
177
200
|
let commands = statements.map(statement => ts.isExpressionStatement(statement) && canSpecializeCommand(statement.expression, expression, setters) ? compileEventCommand(statement.expression, setters) : undefined)
|
|
178
201
|
if ((!commands.length || commands.some(command => !command)) && compileEventCommand.handler) commands = compileEventCommand.handler(expression, setters, bindingIndex)
|
|
179
202
|
if (!commands?.length || commands.some(command => !command)) return undefined
|
|
180
203
|
const original = ts.getOriginalNode(expression)
|
|
181
204
|
const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
|
|
182
|
-
const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, reference: stateOwners.get(command.state) ?? stateReferences(expression).get(command.state) })), source)
|
|
205
|
+
const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, reference: stateOwners.get(command.state) ?? stateReferences(expression).get(command.state) })), source, actions)
|
|
183
206
|
if (keyedBlock !== undefined) handler.keyedBlock = keyedBlock
|
|
184
207
|
return generateCommandBehavior(moduleIR, handler, factory)
|
|
185
208
|
}
|
|
@@ -244,7 +267,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
244
267
|
snapshot: lowered.captureSnapshots.includes(name)
|
|
245
268
|
})),
|
|
246
269
|
imports: entry.imports.map(entry => importSlot(importRecord(entry))),
|
|
247
|
-
actions: [...entry.reducers.values()].flatMap(reducer => reducer.sharedAction ? [reducer.sharedAction.slot] : []),
|
|
270
|
+
actions: [...new Set([...entry.actionSlots, ...[...entry.reducers.values()].flatMap(reducer => reducer.sharedAction ? [reducer.sharedAction.slot] : [])])],
|
|
248
271
|
code: lowered.code,
|
|
249
272
|
...(source(entry.expression) ? { source: source(entry.expression) } : {})
|
|
250
273
|
})
|
|
@@ -258,6 +281,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
258
281
|
signals: [...entry.states].map(name => entry.signalRefs.get(name)),
|
|
259
282
|
captures: [...entry.captures].map(name => ({ ...(symbolSlot(entry, name) !== undefined ? { symbol: symbolSlot(entry, name) } : {}), name, source: "scope" })),
|
|
260
283
|
imports: entry.imports.map(entry => importSlot(importRecord(entry))),
|
|
284
|
+
...(entry.derived ? { derived: entry.derived } : {}),
|
|
261
285
|
code: handlerLowering.lowerReactiveBinding(entry),
|
|
262
286
|
...(source(entry.expression) ? { source: source(entry.expression) } : {})
|
|
263
287
|
})
|
|
@@ -293,7 +317,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
293
317
|
|
|
294
318
|
const importRecord = entry => ({ target: entry.target, kind: entry.kind, local: entry.local, ...(entry.imported ? { imported: entry.imported } : {}), package: Boolean(entry.package) })
|
|
295
319
|
|
|
296
|
-
return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult, signal }
|
|
320
|
+
return { compileConditional, compileDerivedEvaluator, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerEffect: registerEffectResult, registerKeyedBlock: registerKeyedBlockResult, signal }
|
|
297
321
|
}
|
|
298
322
|
|
|
299
323
|
function bindingIdentifier(name, names) {
|