@kudzujs/core 0.8.61 → 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 +37 -1
- package/PERFORMANCE.md +79 -1
- package/README.md +2 -2
- package/RELEASES.md +58 -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 +6 -4
- package/docs/next-architecture/versioning.md +3 -2
- package/framework/README.md +3 -1
- 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/outside-click-pass.mjs +79 -0
- package/framework/compiler/react-migration-pass.mjs +27 -1
- 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 +407 -74
- 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
|
|
|
@@ -379,7 +379,7 @@ function increment(value) { setCount(value + 1) }; increment(count)
|
|
|
379
379
|
After the relevant P0 foundations, investigate capabilities in this order:
|
|
380
380
|
|
|
381
381
|
1. Property-level derived dependencies over ordinary object state. **Completed in `0.8.40`:** direct property paths and top-level immutable primitive locals over object state reuse tagged DerivedIR, subscribe to the source signal, and compare selected values with `Object.is`; whole-object and dynamic dependencies remain rejected.
|
|
382
|
-
2. Multi-boundary component/prop/callback/ref/context dataflow. **Three-boundary callback/ref ownership completed in `0.8.43`; collision-free Context action-private state completed in `0.8.44`; action-only Provider setter exposure removed in `0.8.46`; direct primitive prop state initialization completed in `0.8.47`; repeated direct leaf-handler callback use completed in `0.8.48`; direct child callback fan-out completed in `0.8.49`; direct plain-object prop state initialization completed in `0.8.57`; direct keyed item draft initialization completed in `0.8.58`; direct array prop draft initialization completed in `0.8.59`; matching array-draft setter effects completed in `0.8.60`; parameterized primitive debounce hooks completed in `0.8.61`:** forwarding preserves parent SignalIR, Context action lowering uses compiler-owned aliases when consumer locals reuse Provider state/setter names, action-required setters may remain compiler-only when their state is publicly exposed, specialized children may seed local state from a direct parent signal authored with a serializable primitive, plain-object, or array literal, keyed rows may seed object draft state from their direct item prop, and one callback may branch through multiple component `on*` props and intrinsic handlers. A direct first-boundary parent setter may also retain its authored matching `set*` prop name; additional forwarding remains restricted to `on*` props. The ClimateCompatibleGrowth-derived dropdown proves independent array drafts
|
|
382
|
+
2. Multi-boundary component/prop/callback/ref/context dataflow. **Three-boundary callback/ref ownership completed in `0.8.43`; collision-free Context action-private state completed in `0.8.44`; action-only Provider setter exposure removed in `0.8.46`; direct primitive prop state initialization completed in `0.8.47`; repeated direct leaf-handler callback use completed in `0.8.48`; direct child callback fan-out completed in `0.8.49`; direct plain-object prop state initialization completed in `0.8.57`; direct keyed item draft initialization completed in `0.8.58`; direct array prop draft initialization completed in `0.8.59`; matching array-draft setter effects completed in `0.8.60`; parameterized primitive debounce hooks completed in `0.8.61`; direct `createRef()` outside-click hooks completed in `0.8.62`:** forwarding preserves parent SignalIR, Context action lowering uses compiler-owned aliases when consumer locals reuse Provider state/setter names, action-required setters may remain compiler-only when their state is publicly exposed, specialized children may seed local state from a direct parent signal authored with a serializable primitive, plain-object, or array literal, keyed rows may seed object draft state from their direct item prop, and one callback may branch through multiple component `on*` props and intrinsic handlers. A direct first-boundary parent setter may also retain its authored matching `set*` prop name; additional forwarding remains restricted to `on*` props. The ClimateCompatibleGrowth-derived dropdown proves independent array drafts, exact direct setter-effect synchronization, parameterized debounce ownership, and one exact outside-click listener over an intrinsic DOM ref. A fourth callback boundary, callback aliases/non-handler uses, dynamic debounce delays, non-primitive debounce inputs, dynamic outside-click events, mismatched cleanup/state-setter pairs, fully hidden Context state, additional `set*` forwarding, keyed item aliases, property paths, and composed expressions remain fail-closed. Broader prop, callback, ref, and Context graphs remain migration-led work.
|
|
383
383
|
3. Package-neutral shared state/actions and migration of current Zustand internals. **Completed in `0.8.50`:** Zustand source normalization produces one generic shared-state adapter descriptor; selectors and handlers register JSON-safe SharedStateIR/SharedActionIR records, handler lowering consumes package-neutral actions, and existing RouteIR, layout ownership, same-turn updates, navigation persistence, and browser output remain unchanged. Redux/RTK and public adapter APIs remain unsupported.
|
|
384
384
|
4. Browser-only package imports in owned effect/resource modules. **Completed in `0.8.51` for effects:** direct package references in inline effect setup/cleanup callbacks use existing package import records and route-owned effect ESM bundling; build-time component modules and static siblings omit the package. Helper-indirect, render-time, dynamic-import, and ResourceIR package graphs remain unsupported.
|
|
385
385
|
5. ResourceIR from at least two independent WebSocket/SSE/SDK fixtures with the same semantics. **Completed in `0.8.52` without ResourceIR for private ownership:** the E2B terminal and route-owned WebSocket fixtures lower refs used exclusively by one inline effect to invocation-private closure objects, while existing effect ownership supplies replacement, cleanup, stale setter invalidation, navigation, and BFCache disposal. ResourceIR remains unapproved and now requires independent cross-owner transport/subscription fixtures that cannot fit this narrower model.
|
|
@@ -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,
|
|
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Planned Version Sequence
|
|
2
2
|
|
|
3
|
-
This is an execution sequence, not release history. `0.8.16` through `0.8.
|
|
3
|
+
This is an execution sequence, not release history. `0.8.16` through `0.8.62` are completed scopes represented by package/release records.
|
|
4
4
|
|
|
5
5
|
Keep each patch behavior-preserving and independently reviewable. If a boundary proves inseparable, revise this plan before combining releases; do not silently broaden a patch.
|
|
6
6
|
|
|
@@ -52,6 +52,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
52
52
|
| `0.8.59` | Initialize specialized child draft state from one direct parent array-state prop and accept a direct matching `set*` setter prop. | A ClimateCompatibleGrowth-derived dropdown preserves independent array drafts, explicit parent commit, source naming, nearby diagnostics, and a static zero-JavaScript sibling without runtime changes. |
|
|
53
53
|
| `0.8.60` | Synchronize direct prop-derived array draft state through its matching parent setter in a dependency effect. | The ClimateCompatibleGrowth-derived dropdown preserves its exact direct effect/dependency shape, stable setter erasure, array identity comparison, independent parent replacement, nearby diagnostics, and a static zero-JavaScript sibling without runtime changes. |
|
|
54
54
|
| `0.8.61` | Specialize one parameterized relative primitive debounce hook through existing state and dependency-effect ownership. | The ClimateCompatibleGrowth-derived hook preserves direct state initialization, literal delay scope, timeout replacement/cleanup, conditional release, latest-value commit, nearby diagnostics, and a static zero-JavaScript sibling without runtime changes. |
|
|
55
|
+
| `0.8.62` | Lower direct React `createRef()` and one parameterized outside-click hook through existing DOM-ref and effect ownership. | The ClimateCompatibleGrowth-derived hook preserves inside/outside behavior, serializable setter scope, exact listener cleanup, conditional release/remount, nearby diagnostics, and a static zero-JavaScript sibling without runtime changes. |
|
|
55
56
|
|
|
56
57
|
## Sequence Rules
|
|
57
58
|
|
|
@@ -64,7 +65,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
64
65
|
|
|
65
66
|
## Generator Versions
|
|
66
67
|
|
|
67
|
-
`create-kudzu@0.1.
|
|
68
|
+
`create-kudzu@0.1.103` retains the explicit install instructions and generates projects with `@kudzujs/core@^0.9.0`.
|
|
68
69
|
|
|
69
70
|
## Release Boundary
|
|
70
71
|
|
package/framework/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Migration source may retain conventional `react` imports for supported named or
|
|
|
6
6
|
|
|
7
7
|
Compilation begins from page entries and follows relative runtime imports, re-exports, and validated Worker references; unreachable TypeScript migration files are not transformed. Direct maps over imported immutable JSON-safe arrays fold to literals for zero-JavaScript static rows. Synchronous relative calculation functions may return objects whose direct static fields feed reactive JSX bindings; build rendering uses current signal values and route-specific binding ESM reevaluates the same helper after state commits. One direct array field may instead feed a keyed intrinsic map: its evaluator refreshes a compiler-owned array anchor before the existing list reconciler runs, preserving keyed DOM and SVG identity without a calculation runtime. That field must remain a JSON-safe array after every source-state commit. Package imports have a separate narrow boundary: direct references inside intrinsic JSX event callbacks are erased from build modules and bundled into route handler ESM, while render-time, effect, helper-indirect, and mixed package use fails.
|
|
8
8
|
|
|
9
|
-
Native platform work remains ordinary source. A direct async handler or directly returned relative custom-hook callback may call `navigator.clipboard.writeText()` and update application-owned success/failure state; Kudzu emits only its existing route handler ESM. Debounced synchronization uses a dependency effect that creates `setTimeout()` work and directly returns `clearTimeout()` cleanup, reusing dependency, conditional, keyed, and route ownership. A relative `useDebounce(value, delay)` hook may accept one direct primitive state and numeric literal delay, initialize and return one debounced state, and reuse that same effect ownership. One directly returned relative custom-hook callback may own one `null`-initialized private timeout ref when it directly clears the previous value, assigns a numeric-literal-delay `setTimeout()`, and an empty-dependency effect directly clears the timer on cleanup. Kudzu lowers that ref to compiler-owned state shared by existing handler and effect contexts. An ordinary inline effect may exclusively own top-level `useRef(null)` and `useRef(0)` values through direct `.current` references; Kudzu moves them into the setup invocation's closure for browser SDK handles, WebSockets, generation tokens, and animation frames without serialized captures or a resource runtime. Multiple timers, dynamic delays, intervals, ref aliases, cross-effect/event mutable refs, unowned delayed writes, and arbitrary timed callback graphs remain unsupported.
|
|
9
|
+
Native platform work remains ordinary source. A direct async handler or directly returned relative custom-hook callback may call `navigator.clipboard.writeText()` and update application-owned success/failure state; Kudzu emits only its existing route handler ESM. Debounced synchronization uses a dependency effect that creates `setTimeout()` work and directly returns `clearTimeout()` cleanup, reusing dependency, conditional, keyed, and route ownership. A relative `useDebounce(value, delay)` hook may accept one direct primitive state and numeric literal delay, initialize and return one debounced state, and reuse that same effect ownership. Direct React `createRef()` input may lower to an intrinsic DOM ref passed with one direct literal setter callback into an exact outside-click listener effect; ref and setter serialization reuse existing effect scope and cleanup. One directly returned relative custom-hook callback may own one `null`-initialized private timeout ref when it directly clears the previous value, assigns a numeric-literal-delay `setTimeout()`, and an empty-dependency effect directly clears the timer on cleanup. Kudzu lowers that ref to compiler-owned state shared by existing handler and effect contexts. An ordinary inline effect may exclusively own top-level `useRef(null)` and `useRef(0)` values through direct `.current` references; Kudzu moves them into the setup invocation's closure for browser SDK handles, WebSockets, generation tokens, and animation frames without serialized captures or a resource runtime. Multiple timers, dynamic delays, intervals, ref aliases, cross-effect/event mutable refs, unowned delayed writes, and arbitrary timed callback graphs remain unsupported.
|
|
10
10
|
|
|
11
11
|
Imperative canvas migrations use the same effect ownership rather than a component or canvas runtime. One `null`-initialized canvas DOM ref may feed an inline effect whose local variables persist across a recursive animation-frame callback, an `IntersectionObserver`, and native canvas/window listeners; the returned cleanup must cancel the latest frame, disconnect the observer, and remove every listener. Bare `IntersectionObserver` and `performance` identifiers remain browser globals in emitted effect ESM. Component-level mutable value refs and callbacks shared across effects or JSX handlers remain unsupported; move resource-private state and listeners into the owning effect.
|
|
12
12
|
|
|
@@ -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) {
|