@kudzujs/core 0.8.53 → 0.8.55
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 +2 -1
- package/PERFORMANCE.md +9 -1
- package/README.md +1 -1
- package/RELEASES.md +68 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +8 -8
- package/docs/next-architecture/large-application-ai-native-roadmap.md +2 -2
- package/docs/next-architecture/versioning.md +3 -1
- package/framework/README.md +4 -0
- package/framework/build.mjs +124 -77
- package/framework/compiler/effect-codegen.mjs +17 -17
- package/framework/compiler/param-codegen.mjs +2 -2
- package/framework/compiler/route-artifact-report.mjs +133 -0
- package/framework/compiler/runtime-family-planner.mjs +48 -0
- package/framework/compiler/worker-compiler.mjs +23 -4
- package/framework/core.d.ts +2 -0
- package/framework/core.mjs +2 -2
- package/framework/dev-server.mjs +1 -1
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This document is the source of truth for Kudzu's product direction, architecture invariants, and future development order. Read it before extending React-shaped syntax or browser capabilities.
|
|
4
4
|
|
|
5
|
-
The executable post-`0.8.
|
|
5
|
+
The executable post-`0.8.55` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
|
|
6
6
|
|
|
7
7
|
[`GOAL_A.md`](./GOAL_A.md) and [`GOAL_B.md`](./GOAL_B.md) are completed capability-validation records. Their commerce and realtime dashboard fixtures prove general lifecycle, navigation, async-workflow, and Worker capabilities; they are not separate product verticals or future priority lists.
|
|
8
8
|
|
|
@@ -60,6 +60,7 @@ The following are available building blocks, not future vertical roadmaps:
|
|
|
60
60
|
|
|
61
61
|
- Async build-time components, `getStaticPaths()`, runtime path parameters, metadata, base paths, public assets, CSS, CSS Modules, and post-build hooks.
|
|
62
62
|
- Relative source CSS imports close over each page's reachable TypeScript import/re-export graph; configured styles remain explicitly global. Enhanced navigation loads destination CSS before route DOM replacement, reuses shared layout links, and removes outgoing route links.
|
|
63
|
+
- Interactive routes use deterministic signature-keyed runtime families. Equal standalone capability manifests share files, while each enhanced-navigation group unions its routes into one ESM singleton family for persistent layout state and lifecycle ownership.
|
|
63
64
|
- React import normalization for supported named/aliased hooks, direct `React.*` members, fragments, same-file `memo`, inline `useCallback`, direct intrinsic `forwardRef`, top-level `useId`, and analyzable `useMemo` expressions and collections.
|
|
64
65
|
- Function components, props, children, context, direct bindings, conditions, controlled form properties, refs, and synchronous or async handlers.
|
|
65
66
|
- `useState`, independent repeated non-keyed child state with conditional mount ownership, reduced relative-imported `useReducer`, direct dispatch specialization, and reduced Zustand-shaped shared state lowered through package-neutral shared-state/action IR and proven by migration fixtures.
|
package/PERFORMANCE.md
CHANGED
|
@@ -6,7 +6,15 @@ Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm ru
|
|
|
6
6
|
|
|
7
7
|
The maintained 2026-08-13 comparison used Node 24.14.0 and an Intel Core i5-9500 Linux x64 host, one warm-up, and seven alternating fresh-process samples against clean `v0.8.44`. A narrow fast path skips Kudzu semantic transformation for 450 plain `.ts` modules whose runtime edges are exclusively resolvable relative TypeScript imports or exports; all other modules retain the existing transformer. Compile median fell from 2,323.9 ms to 1,413.2 ms (39.2%) and clean-build median from 3,325.3 ms to 2,382.4 ms (28.4%); every paired sample improved. Compile peak-RSS median fell from 571.2 MiB to 552.6 MiB, while build peak RSS was 570.9 MiB versus 568.8 MiB. Compiler scratch fell from 7,328,390 to 1,971,061 bytes. Both targets emitted the same 50 static HTML files, 10,980 bytes, and deploy SHA-256 `e107d78a7f55bc8a1af0ea6e53efeffa19b3d44d21c892484d103fa346e7ba7b`. This is a source-scale compiler comparison, not a cross-framework result.
|
|
8
8
|
|
|
9
|
-
## Current 0.8.
|
|
9
|
+
## Current 0.8.55 Release Snapshot
|
|
10
|
+
|
|
11
|
+
Kudzu 0.8.55 replaces site-wide runtime specialization with deterministic route or navigation-group capability families. Unrelated standalone capabilities no longer change another route's loaded runtime bytes or cache URL; no new timing claim is made.
|
|
12
|
+
|
|
13
|
+
## Maintained 0.8.54 Release Snapshot
|
|
14
|
+
|
|
15
|
+
Kudzu 0.8.54 retains esbuild output metadata and writes one compiler-scratch artifact report after bundling. Deploy runtime behavior and output selection are unchanged, and no new performance claim is made.
|
|
16
|
+
|
|
17
|
+
## Maintained 0.8.53 Release Snapshot
|
|
10
18
|
|
|
11
19
|
Kudzu 0.8.53 removes unrelated source stylesheet links from route HTML and reuses shared layout links during enhanced navigation. Static routes add no JavaScript, and no new performance claim is made.
|
|
12
20
|
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
14
14
|
|
|
15
15
|
> Experimental `0.8.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.8.
|
|
17
|
+
**Latest release: 0.8.55 - Signature-keyed runtime families.** Equal standalone capability signatures reuse one runtime family, different signatures stay isolated, and enhanced-navigation groups retain one union ESM singleton for layout state and lifecycle ownership. Read the [release notes](./RELEASES.md#0855---signature-keyed-runtime-families), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.55), or follow the [architecture packet](./docs/next-architecture/README.md).
|
|
18
18
|
|
|
19
19
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
20
20
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.55 - Signature-keyed runtime families
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.55 replaces site-wide runtime specialization with deterministic capability families loaded only by the routes that require them.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.55
|
|
8
|
+
|
|
9
|
+
- Equal standalone CapabilityIR signatures share one runtime family under `assets/runtime/<family>/`.
|
|
10
|
+
- Different route signatures emit isolated core, binding, list, effect, native, serialization, style, and selector modules.
|
|
11
|
+
- Route-specific parameter, effect, and native entries import their assigned family directly.
|
|
12
|
+
- Artifact report v2 records exact route signatures, assigned families, emitted family requirements, handlers, Workers, styles, and chunks.
|
|
13
|
+
- Development state restoration discovers the family core module from the rendered document.
|
|
14
|
+
|
|
15
|
+
### Navigation ownership
|
|
16
|
+
|
|
17
|
+
- Every enhanced-navigation group unions its route capabilities into one family.
|
|
18
|
+
- Persistent layout state, committers, mount hooks, unmount hooks, and native listeners retain one ESM singleton identity across group navigation.
|
|
19
|
+
- Independent groups may reuse byte-identical family files while keeping their own route matcher and layout/application identity modules.
|
|
20
|
+
|
|
21
|
+
### Output
|
|
22
|
+
|
|
23
|
+
- Static routes emit no runtime family and remain zero JavaScript.
|
|
24
|
+
- An unrelated standalone route capability no longer changes another route's loaded runtime bytes or cache URL.
|
|
25
|
+
- Legacy root-level `assets/kudzu*.js` runtime files are no longer emitted.
|
|
26
|
+
- No runtime loader, hydration layer, VDOM, component tree, or SPA router is added.
|
|
27
|
+
|
|
28
|
+
### Validation
|
|
29
|
+
|
|
30
|
+
- Focused tests cover signature deduplication, distinct capability isolation, deterministic IDs, navigation unions, static exclusion, shared handler/Worker chunks, and family-aware public collisions.
|
|
31
|
+
- Existing navigation, effect, list, native, parameter, Worker, base-path, development restoration, and browser ownership journeys pass.
|
|
32
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 221 tests and 170 generated pages.
|
|
33
|
+
|
|
34
|
+
### Upgrade
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install @kudzujs/core@^0.8.55
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 0.8.54 - Inspectable route artifact closure
|
|
41
|
+
|
|
42
|
+
Kudzu 0.8.54 projects each validated route edge through the final handler and Worker bundle graphs into one inspectable artifact contract.
|
|
43
|
+
|
|
44
|
+
### Changed in 0.8.54
|
|
45
|
+
|
|
46
|
+
- `.kudzu/kudzu-artifacts.json` records every route in deterministic order.
|
|
47
|
+
- Each route receives its exact CapabilityIR manifest, SHA-256 signature, and required runtime families.
|
|
48
|
+
- Handler entry ownership derives from retained RouteBuildRecord references rather than HTML or filename inference.
|
|
49
|
+
- Handler and Worker esbuild metafiles are followed transitively to report route-owned and shared chunks.
|
|
50
|
+
- Chunks reached by multiple routes include a sorted reverse ownership list.
|
|
51
|
+
- `afterBuild()` receives the same JSON-safe report as `artifacts`.
|
|
52
|
+
|
|
53
|
+
### Boundaries
|
|
54
|
+
|
|
55
|
+
- The report distinguishes exact route runtime requirements from the currently site-specialized runtime files.
|
|
56
|
+
- Enhanced-navigation routes still share their existing ESM singleton and lifecycle boundary.
|
|
57
|
+
- Signature-keyed runtime emission remains the next route-closure slice.
|
|
58
|
+
- No runtime loader, manifest fetch, SPA router, or browser behavior is added.
|
|
59
|
+
|
|
60
|
+
### Validation
|
|
61
|
+
|
|
62
|
+
- Focused checks cover route-only and shared handler chunks, external-import exclusion, Worker ownership, shared Worker chunks, deterministic route order, and distinct capability signatures.
|
|
63
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 220 tests and 169 generated pages.
|
|
64
|
+
|
|
65
|
+
### Upgrade
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npm install @kudzujs/core@^0.8.54
|
|
69
|
+
```
|
|
70
|
+
|
|
3
71
|
## 0.8.53 - Route-aware CSS closure
|
|
4
72
|
|
|
5
73
|
Kudzu 0.8.53 links source styles only from routes whose reachable TypeScript graph imports them.
|
|
@@ -11,7 +11,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
|
|
|
11
11
|
| C: state/resource model | Research only | Reduced fixtures expose a limitation |
|
|
12
12
|
| D: routing compatibility | Current behavior preserved | Revisit only with migration evidence and invariant review |
|
|
13
13
|
|
|
14
|
-
The active post-`0.8.
|
|
14
|
+
The active post-`0.8.55` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.12 validation, property-level object-state dependencies, direct three-boundary callback/ref ownership, direct primitive prop state initialization, repeated direct leaf-handler callback use, direct child callback fan-out, collision-free and private Context action setters, package-neutral shared-state/action IR, browser-only package imports and private mutable refs in owned effects, route/layout CSS closure, structural per-route capability/chunk reporting, signature-keyed runtime families, measured route-entry output optimization, and the plain TypeScript source-scale fast path are complete. Incremental source and affected-route builds are next; ResourceIR remains research for cross-owner transports and subscriptions. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
|
|
15
15
|
|
|
16
16
|
## Required Invariants
|
|
17
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the current `0.8.
|
|
3
|
+
This maps the current `0.8.55` architecture, built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
|
|
4
4
|
|
|
5
5
|
## Responsibility Map
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ This maps the current `0.8.53` architecture, built on the completed `0.8.23` Goa
|
|
|
19
19
|
| Main semantic analysis | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `createKudzuTransformer()` | Produces transformed source plus explicit component, shared-state/action, handler, binding, derived, keyed, and effect ownership results. Zustand package syntax is resolved by its adapter before generic shared-state consumers. |
|
|
20
20
|
| Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Produces ComponentAnalysis v2 with ordered JSON-safe owner and specialization slots for state, setters, props, refs, IDs, direct SignalIR links, structural OwnerRefs, source-local SiteIds, and source provenance. Three direct callback/ref component boundaries specialize into the same parent signal and intrinsic ownership; Context action-private setters may remain compiler-only and receive collision-free consumer-local aliases; AST identity remains private to its source-local session. |
|
|
21
21
|
| Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes ModuleIR v2 with deterministic SymbolRef, SharedStateIR, SharedActionIR, SignalIR, HandlerIR, BindingIR, DerivedIR, EffectIR, KeyedBlockIR, and ImportIR slots. One fail-closed boundary validates every source-local and component ownership edge before build-module generation. |
|
|
22
|
-
| Route artifact graph | [`framework/compiler/route-build-record.mjs`](../../framework/compiler/route-build-record.mjs), `createRouteBuildRecord()`, `planRouteArtifacts()` | Validates each rendered route's RouteIR, capabilities, entry paths, styles, and exact handler/effect references. Handler
|
|
22
|
+
| Route artifact graph | [`framework/compiler/route-build-record.mjs`](../../framework/compiler/route-build-record.mjs), `createRouteBuildRecord()`, `planRouteArtifacts()`; [`framework/compiler/route-artifact-report.mjs`](../../framework/compiler/route-artifact-report.mjs), `createRouteArtifactReport()` | Validates each rendered route's RouteIR, capabilities, entry paths, styles, and exact handler/effect references. Handler and Worker esbuild metafiles project transitive output edges back through those records into deterministic per-route capability signatures and runtime requirements plus emitted handler, Worker, stylesheet, and shared-chunk closure. |
|
|
23
23
|
| Route contract validation | [`framework/compiler/route-ir.mjs`](../../framework/compiler/route-ir.mjs), `assertRouteIR()` | Fails before artifact selection for invalid state/parameter identity, commands, native/effect captures and dependencies, reactive descriptors, conditions, keyed-list identity/ownership, marker fields, or JSON safety. Immutable in-memory contracts validate once by identity. |
|
|
24
24
|
| Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Direct commands use the existing fast path; proven immutable state aliases and one-call local helpers specialize to the same JSON-safe command ModuleIR. Recursion, escape, mutation, and dynamic helper dispatch fail explicitly, while unrelated handlers retain native ESM. Codegen emits the existing `__kBehavior` AST and command ABI. |
|
|
25
25
|
| Source compilation | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `compileSource()` | Runs TypeScript with the Kudzu transformer, rejects surviving React/Router references, and returns a JSON-safe project-relative build module, component analysis, ModuleIR, optional handler module, and imported assets without filesystem writes. |
|
|
@@ -29,10 +29,10 @@ This maps the current `0.8.53` architecture, built on the completed `0.8.23` Goa
|
|
|
29
29
|
| Worker graph | [`framework/compiler/worker-compiler.mjs`](../../framework/compiler/worker-compiler.mjs) | Returns functional Worker rewrite results and JSON-safe EffectIR edges, validates relative graphs, emits content-hashed ESM, and resolves placeholders only for rendered effects. |
|
|
30
30
|
| Shared path conversion | [`framework/compiler/path-helpers.mjs`](../../framework/compiler/path-helpers.mjs) | Converts project-relative module, browser, asset, and base paths for build and development serving. |
|
|
31
31
|
| Build-time JSX execution | [`framework/core.mjs`](../../framework/core.mjs), `renderPage()` | Executes compiled pages/layouts, allocates deterministic route/layout ownership IDs, emits complete HTML, and returns RouteIR v1 plus capability facts and exact retained handler references. |
|
|
32
|
-
| Route capability projection | [`framework/compiler/route-capability-planner.mjs`](../../framework/compiler/route-capability-planner.mjs), `planRouteCapabilities()` | Validates RouteBuildRecord and RouteIR v1,
|
|
32
|
+
| Route capability projection | [`framework/compiler/route-capability-planner.mjs`](../../framework/compiler/route-capability-planner.mjs), `planRouteCapabilities()`; [`framework/compiler/runtime-family-planner.mjs`](../../framework/compiler/runtime-family-planner.mjs), `planRuntimeFamilies()` | Validates RouteBuildRecord and RouteIR v1, projects exact route CapabilityIR, deduplicates equal standalone signatures, and unions each enhanced-navigation group at one required ESM singleton boundary. |
|
|
33
33
|
| Effect entry generation | [`framework/compiler/effect-codegen.mjs`](../../framework/compiler/effect-codegen.mjs) | Generates ordinary, dependency, owned, and navigable effect entries from rendered descriptors. |
|
|
34
34
|
| Runtime generation | [`framework/compiler/runtime-codegen.mjs`](../../framework/compiler/runtime-codegen.mjs), [`framework/compiler/list-runtime-codegen.mjs`](../../framework/compiler/list-runtime-codegen.mjs), [`framework/compiler/param-codegen.mjs`](../../framework/compiler/param-codegen.mjs) | Consumes versioned contracts, specializes authored capability sources with fail-closed anchors, and returns source/define results without filesystem ownership. |
|
|
35
|
-
| Artifact emission | `framework/build.mjs` | Selects route artifacts from RouteBuildRecord edges
|
|
35
|
+
| Artifact emission | `framework/build.mjs` | Selects route artifacts from RouteBuildRecord edges, emits each distinct runtime family under `assets/runtime/<family>/`, writes route HTML in bounded batches, bundles in a project-local staging sibling, copies public subtrees without replacing generated paths, runs `afterBuild`, then promotes with rollback. Byte-identical native, parameter, and effect entries share one file only when their family imports also match. |
|
|
36
36
|
| Browser capabilities | [`framework/*.js`](../../framework/) | Small optional modules for commands, bindings, lists, effects, native handlers, serialization, parameters, and navigation; native contexts invalidate writes and refs at DOM ownership release, with no component runtime. |
|
|
37
37
|
| Opt-in navigation | [`framework/navigation-runtime.js`](../../framework/navigation-runtime.js) plus `framework/build.mjs` navigation configuration/emission | Fetches and validates complete same-origin documents, replaces only the marked route range, manages route/layout disposal, history, focus, finite prefetch retention, and native fallback. |
|
|
38
38
|
| Development serving | [`framework/dev-server.mjs`](../../framework/dev-server.mjs) and [`framework/dev-state.js`](../../framework/dev-state.js) | Rebuild/watch/SSE and response-only short-lived state restoration; failed rebuilds show the existing error overlay while preserving the previous on-disk output. |
|
|
@@ -56,11 +56,11 @@ src/pages entries + config
|
|
|
56
56
|
-> validated RouteBuildRecord per emitted route
|
|
57
57
|
-> capability facts, route entries, styles, handler/effect artifact edges
|
|
58
58
|
-> remove unrendered handlers/effects/Workers
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-> specialize and emit
|
|
59
|
+
-> planRuntimeFamilies(RouteBuildRecord[], navigation groups)
|
|
60
|
+
-> exact route signatures plus deduplicated standalone/group CapabilityIR families
|
|
61
|
+
-> specialize and emit each selected runtime family under assets/runtime/<family>/
|
|
62
62
|
-> reuse exact generated route-entry transforms within this build
|
|
63
|
-
-> write route index.html, CSS/assets, Worker graphs, and
|
|
63
|
+
-> write route index.html, CSS/assets, Worker graphs, rewrites, and per-route artifact closure into staging/scratch
|
|
64
64
|
-> copy public paths only where they do not replace generated artifacts
|
|
65
65
|
-> optional afterBuild() against staging
|
|
66
66
|
-> rollback-safe promotion to dist; recover an interrupted backup on the next admitted build after stale-lock removal
|
|
@@ -383,7 +383,7 @@ After the relevant P0 foundations, investigate capabilities in this order:
|
|
|
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.
|
|
386
|
-
6. Route/layout capability and CSS chunk closure. **Route/layout CSS closure completed in `0.8.53`:** each page's reachable TypeScript
|
|
386
|
+
6. Route/layout capability and CSS chunk closure. **Route/layout CSS closure completed in `0.8.53`; structural reporting completed in `0.8.54`; signature-keyed runtime emission completed in `0.8.55`:** each page's reachable TypeScript graph supplies exact stylesheet edges, and `.kudzu/kudzu-artifacts.json` follows handler and Worker metafiles transitively. Equal standalone CapabilityIR signatures now share one co-located runtime family, different signatures emit isolated files, and every enhanced-navigation group uses one union family so persistent layout state and lifecycle hooks retain a single ESM identity. Static routes emit no family, route entries import their assigned family, and unrelated route capabilities no longer change loaded runtime bytes or URLs.
|
|
387
387
|
7. Incremental source and affected-route builds.
|
|
388
388
|
8. Range ownership and virtualization only after a real data-heavy fixture establishes direct DOM limits.
|
|
389
389
|
9. Optimistic shared transactions only after independent mutation fixtures establish commit/rollback semantics.
|
|
@@ -481,4 +481,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
481
481
|
|
|
482
482
|
## Immediate Decision
|
|
483
483
|
|
|
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 prop initializer slice, 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, and the `0.8.42` measured route-output optimization are complete. Continue item
|
|
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 prop initializer slice, 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, and the `0.8.42` measured route-output optimization are complete. Continue with item 7 incremental source and affected-route builds; keep ResourceIR limited to cross-owner fixtures and do not skip directly to a public adapter/store API, router, virtualization, or unsupported ecosystem package feature.
|
|
@@ -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.55` 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
|
|
|
@@ -44,6 +44,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
44
44
|
| `0.8.51` | Bundle direct browser-only package references from inline owned effect setup/cleanup callbacks. | Package code stays in referenced route effect ESM; build scratch and static siblings omit it while indirect/render-time uses fail. |
|
|
45
45
|
| `0.8.52` | Lower effect-exclusive mutable refs into setup-invocation closures. | E2B and WebSocket fixtures preserve generation, replacement, BFCache, and cleanup without captures, ResourceIR, or static-sibling JavaScript. |
|
|
46
46
|
| `0.8.53` | Close source CSS over each route's reachable TypeScript graph and reconcile managed styles during navigation. | Routes exclude unrelated CSS; destination styles load before replacement, shared layout links retain identity, and cancelled loads roll back safely. |
|
|
47
|
+
| `0.8.54` | Project per-route capability requirements and post-bundle handler/Worker chunk closure into an inspectable artifact contract. | Deterministic signatures, exact retained entries, transitive chunks, reverse shared ownership, static-route exclusion, and `afterBuild()` access pass. |
|
|
48
|
+
| `0.8.55` | Emit deduplicated signature-keyed runtime families while preserving navigation-group singleton ownership. | Equal standalone signatures share files, different signatures isolate output, grouped routes share one union family, and static routes emit no runtime. |
|
|
47
49
|
|
|
48
50
|
## Sequence Rules
|
|
49
51
|
|
package/framework/README.md
CHANGED
|
@@ -49,6 +49,8 @@ Reduced Zustand migration stores lower through package-neutral `SharedStateIR` a
|
|
|
49
49
|
- `compiler/render-control-pass.mjs`: render-function early-return and exhaustive adjacent-assignment normalization.
|
|
50
50
|
- `compiler/router-pass.mjs`: React Router import validation and native Link, pathname parameter, search parameter, and imperative navigation lowering.
|
|
51
51
|
- `compiler/route-capability-planner.mjs`: RouteIR v1 validation and pure CapabilityIR v1 projection into runtime and artifact requirements.
|
|
52
|
+
- `compiler/runtime-family-planner.mjs`: deterministic standalone capability deduplication and enhanced-navigation group unioning at the ESM singleton boundary.
|
|
53
|
+
- `compiler/route-artifact-report.mjs`: deterministic per-route capability signatures and runtime requirements plus handler, Worker, stylesheet, and transitive esbuild chunk closure reporting.
|
|
52
54
|
- `compiler/runtime-codegen.mjs`: fail-closed core, effect, binding, native, and navigation runtime generation from CapabilityIR.
|
|
53
55
|
- `compiler/list-runtime-codegen.mjs`: fail-closed keyed-list runtime generation from CapabilityIR list/effect sections.
|
|
54
56
|
- `compiler/worker-compiler.mjs`: relative TypeScript Worker candidate validation, effect rewriting, graph validation, and content-hashed ESM emission.
|
|
@@ -70,6 +72,8 @@ Reduced Zustand migration stores lower through package-neutral `SharedStateIR` a
|
|
|
70
72
|
|
|
71
73
|
Compiler ownership follows explicit stages. `build.mjs` coordinates project discovery, source compilation, RouteIR rendering, RouteBuildRecord collection, CapabilityIR planning, generator calls, and artifact emission. `compiler/normalization-pipeline.mjs` owns pass order and parent repair. The main transformer analyzes normalized source while `compiler/descriptor-session.mjs` owns one per-source semantic artifact containing HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, and client imports. `core.mjs` emits complete HTML, RouteIR v1, and exact handler references: route-local state `slot` is an internal array reference, `id` remains the browser/DOM identity, and `name` remains readable development metadata. `compiler/route-build-record.mjs` validates structural artifact edges, and `compiler/route-capability-planner.mjs` projects CapabilityIR v1 from those records. Focused codegen modules consume IR/descriptors and return route-specific source without source analysis or filesystem ownership. The completed architecture record lives in `docs/next-architecture`; it adds no runtime or accepted syntax by itself.
|
|
72
74
|
|
|
75
|
+
`.kudzu/kudzu-artifacts.json` records each route's exact CapabilityIR signature, assigned emitted runtime family, handler, Worker, stylesheet, and transitive chunk edges. Equal standalone signatures reuse `dist/assets/runtime/<family>/`; every enhanced-navigation group intentionally shares one union family. The same JSON-safe contract is available to `afterBuild()` as `artifacts`.
|
|
76
|
+
|
|
73
77
|
New syntax support belongs in an existing pass or a focused new pass only when a reduced migration fixture proves it. Passes must preserve source-located diagnostics, avoid module-global analysis state, and expose metadata through return values rather than AST-identity side channels. Build orchestration stays in `build.mjs`; feature-specific graph validation or code generation moves under `compiler/` when it has a stable input/output boundary.
|
|
74
78
|
|
|
75
79
|
Static routes receive no browser runtime. Command routes receive `runtime.js`; dependency effects use route-specific `kudzu-deps.js` unless that route already requires shared commit hooks; runtime bracket pages using `useParams()` add one route-specific pathname matcher; reactive attributes and conditions add `binding-runtime.js`; keyed lists add `list-runtime.js`; native handlers add `native-runtime.js`; effects add `effect-runtime.js` and one route-specific entry. Generated module scripts live in the document head, so cold downloads overlap HTML transfer while standard module deferral preserves execution after parsing. A single effect with one dependency compiles to a direct runner; generic maps, sets, and ordering are reserved for larger effect graphs. Dependency commits coalesce in a microtask; affected cleanups are awaited in declaration order before replacement setups run. Document cleanup integrates with shared unmount hooks when present and otherwise disposes directly on non-persisted `pagehide`. List builds remove unused text-range, attribute, event, expression, condition, seed, and mount branches. Effect builds omit capture deserialization entirely when every effect scope is empty. Capability runtimes share state and lifecycle hooks through `shared-runtime.js`. Generated evaluators and their bundled relative TypeScript helpers live under `dist/assets/handlers/`; shared helper chunks are emitted only when multiple handler entries need them. Runtime fallback rewrites are ordered by specificity in `.kudzu/kudzu-plan.json` and passed to `afterBuild()`; exact static files take precedence in development. The dev server derives stable state identities from route-unique state variable names in each route plan; every state sharing a duplicate name is omitted. It then injects its SSE reload, short-lived full-URL-scoped logical-state snapshot, and build-error client into responses only, never into `dist/`. Snapshots are consumed even when the next page is static or broken. Reload restoration covers compatible framework state, not uncontrolled DOM state, focus, selection, or imperative mutations.
|
package/framework/build.mjs
CHANGED
|
@@ -8,9 +8,11 @@ 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
10
|
import { createRouteBuildRecord, planRouteArtifacts } from "./compiler/route-build-record.mjs"
|
|
11
|
+
import { createRouteArtifactReport } from "./compiler/route-artifact-report.mjs"
|
|
12
|
+
import { planRuntimeFamilies } from "./compiler/runtime-family-planner.mjs"
|
|
11
13
|
import { createSourceCompiler } from "./compiler/source-compiler.mjs"
|
|
12
14
|
import { createParamCodegen } from "./compiler/param-codegen.mjs"
|
|
13
|
-
import {
|
|
15
|
+
import { usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
|
|
14
16
|
import { generateBindingRuntime, generateCoreRuntime, generateEffectRuntime, generateNativeRuntime, generateNavigationRuntime, specializeRuntime } from "./compiler/runtime-codegen.mjs"
|
|
15
17
|
import { renderPage } from "./core.mjs"
|
|
16
18
|
import { parseDevHost, parseDevPort, startDevServer } from "./dev-server.mjs"
|
|
@@ -66,7 +68,6 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
66
68
|
const globalStyleUrlSet = new Set(globalStyleUrls)
|
|
67
69
|
const publicDirectory = normalizePublicDirectory(config.publicDir, project)
|
|
68
70
|
const navigationGroups = normalizeNavigation(config.navigation)
|
|
69
|
-
const navigationRoutes = navigationGroups.flatMap(group => group.routes)
|
|
70
71
|
const navigationByRoute = new Map(navigationGroups.flatMap(group => group.routes.map(route => [route, group])))
|
|
71
72
|
for (const group of navigationGroups) {
|
|
72
73
|
group.assetPath = assetPath(base, `assets/${group.assetName}`)
|
|
@@ -74,6 +75,7 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
74
75
|
group.layoutId = `l-${group.id}`
|
|
75
76
|
group.records = []
|
|
76
77
|
group.routeRecords = []
|
|
78
|
+
group.buildRecords = []
|
|
77
79
|
group.hasEffects = false
|
|
78
80
|
group.hasParams = false
|
|
79
81
|
}
|
|
@@ -116,7 +118,8 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
116
118
|
return effect.workers.map(worker => ({ ...worker, module: assetPath(base, `assets/${result.handlerModule.path}`), handler: handler.exportName }))
|
|
117
119
|
}))
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
let routeRecords = []
|
|
122
|
+
const routeDrafts = []
|
|
120
123
|
const routeEntryTransforms = new Map()
|
|
121
124
|
const routeEntrySources = new Map()
|
|
122
125
|
const routeEntryPaths = new Map()
|
|
@@ -124,7 +127,10 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
124
127
|
const emittedRoutes = new Set()
|
|
125
128
|
const emittedApplicationRoutes = new Set()
|
|
126
129
|
const emittedNavigationRecords = []
|
|
130
|
+
const navigationAssets = new Map()
|
|
127
131
|
const runtimePlaceholder = `/__kudzu_runtime_${randomUUID()}.js`
|
|
132
|
+
const bindingPlaceholder = `/__kudzu_binding_${randomUUID()}.js`
|
|
133
|
+
const listPlaceholder = `/__kudzu_list_${randomUUID()}.js`
|
|
128
134
|
|
|
129
135
|
for (const pageFile of pageFiles) {
|
|
130
136
|
const sourceStyleUrls = stylesByPage.get(pageFile).map(file => assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)).filter(url => !globalStyleUrlSet.has(url))
|
|
@@ -156,6 +162,7 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
156
162
|
const pageMetadata = await resolveDocumentMetadata(module.metadata, metadataContext, `${relative(root, pageFile)} metadata`)
|
|
157
163
|
const navigationGroup = navigationByRoute.get(applicationRoute)
|
|
158
164
|
const navigable = Boolean(navigationGroup)
|
|
165
|
+
if (navigationGroup) navigationAssets.set(routePath, navigationGroup.assetPath)
|
|
159
166
|
const effectPath = `effects/${route ? `${route}/index` : "index"}.js`
|
|
160
167
|
const nativePath = `native/${route ? `${route}/index` : "index"}.js`
|
|
161
168
|
const paramPath = `params/${route ? `${route}/index` : "index"}.js`
|
|
@@ -182,6 +189,8 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
182
189
|
managedStyles: navigable ? sourceStyleUrls : [],
|
|
183
190
|
base,
|
|
184
191
|
runtimeAsset: runtimePlaceholder,
|
|
192
|
+
bindingAsset: bindingPlaceholder,
|
|
193
|
+
listAsset: listPlaceholder,
|
|
185
194
|
effectAsset: assetPath(base, `assets/${effectPath}`),
|
|
186
195
|
nativeAsset: assetPath(base, `assets/${nativePath}`),
|
|
187
196
|
paramAsset: assetPath(base, `assets/${paramPath}`),
|
|
@@ -193,30 +202,16 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
193
202
|
navigationGroup.hasParams ||= result.hasParams
|
|
194
203
|
}
|
|
195
204
|
const usesDependencyRuntime = usesRouteDependencyRuntime({ plan: result.plan, navigable, hasBindings: result.hasBindings, hasLists: result.hasLists })
|
|
196
|
-
const routeRuntimeName = usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
|
|
197
205
|
const plan = { route: routePath, ...result.plan }
|
|
198
|
-
const entries = {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
entries.param = entry.path
|
|
203
|
-
html = html.replaceAll(assetPath(base, `assets/${paramPath}`), assetPath(base, `assets/${entry.path}`))
|
|
206
|
+
const entries = {
|
|
207
|
+
...(result.hasParams ? { param: paramPath } : {}),
|
|
208
|
+
...(result.hasEffects ? { effect: effectPath } : {}),
|
|
209
|
+
...(plan.events.some(event => event.native) ? { native: nativePath } : {})
|
|
204
210
|
}
|
|
205
|
-
|
|
206
|
-
const entry = retainRouteEntry(effectPath, output => printEffectEntry(runtimeEffects(plan.effects, navigable), output, handlerModules, join(outputDirectory, "assets"), base, entries.param, routeRuntimeName, navigable), routeEntrySources, routeEntryPaths, outputDirectory)
|
|
207
|
-
entries.effect = entry.path
|
|
208
|
-
html = html.replaceAll(assetPath(base, `assets/${effectPath}`), assetPath(base, `assets/${entry.path}`))
|
|
209
|
-
}
|
|
210
|
-
if (plan.events.some(event => event.native)) {
|
|
211
|
-
const modules = [...new Set(plan.events.filter(event => event.native).map(event => event.native.module))]
|
|
212
|
-
const entry = retainRouteEntry(nativePath, () => printNativeEntrySource(modules, base), routeEntrySources, routeEntryPaths, outputDirectory)
|
|
213
|
-
entries.native = entry.path
|
|
214
|
-
html = html.replaceAll(assetPath(base, `assets/${nativePath}`), assetPath(base, `assets/${entry.path}`))
|
|
215
|
-
}
|
|
216
|
-
routeRecords.push(createRouteBuildRecord({
|
|
211
|
+
const record = createRouteBuildRecord({
|
|
217
212
|
route: routePath,
|
|
218
213
|
output: route,
|
|
219
|
-
html,
|
|
214
|
+
html: inlineQueryFormCarry(result.html, plan),
|
|
220
215
|
plan,
|
|
221
216
|
handlerReferences: result.handlerReferences,
|
|
222
217
|
styles: styleUrls,
|
|
@@ -233,7 +228,10 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
233
228
|
},
|
|
234
229
|
entries,
|
|
235
230
|
runtimeSchema
|
|
236
|
-
})
|
|
231
|
+
})
|
|
232
|
+
routeRecords.push(record)
|
|
233
|
+
if (navigationGroup) navigationGroup.buildRecords.push(record)
|
|
234
|
+
routeDrafts.push({ record, result, runtimeSchema, navigationGroup, effectPath, nativePath, paramPath })
|
|
237
235
|
}
|
|
238
236
|
}
|
|
239
237
|
|
|
@@ -245,12 +243,65 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
245
243
|
group.records.sort((left, right) => (right.segments?.filter(segment => segment !== null).length ?? 0) - (left.segments?.filter(segment => segment !== null).length ?? 0) || left.id.localeCompare(right.id))
|
|
246
244
|
}
|
|
247
245
|
|
|
246
|
+
const runtimePlan = planRuntimeFamilies(routeRecords, navigationGroups)
|
|
247
|
+
for (const group of navigationGroups) {
|
|
248
|
+
group.runtimeFamily = runtimePlan.familyByRecord.get(group.buildRecords[0])
|
|
249
|
+
group.assetPath = assetPath(base, `assets/runtime/${group.runtimeFamily.id}/${group.assetName}`)
|
|
250
|
+
}
|
|
251
|
+
const runtimeFamilyByRecord = new Map()
|
|
252
|
+
routeRecords = routeDrafts.map(draft => {
|
|
253
|
+
const { record, result, runtimeSchema, navigationGroup, effectPath, nativePath, paramPath } = draft
|
|
254
|
+
const family = runtimePlan.familyByRecord.get(record)
|
|
255
|
+
if (record.capabilities.hasBehaviors && !family) throw new Error(`Interactive route has no runtime family: ${record.route}`)
|
|
256
|
+
const runtimeDirectory = family ? join(outputDirectory, "assets", "runtime", family.id) : undefined
|
|
257
|
+
const routeRuntimeName = record.capabilities.usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
|
|
258
|
+
const entries = {}
|
|
259
|
+
let html = record.html
|
|
260
|
+
if (record.capabilities.hasParams) {
|
|
261
|
+
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)
|
|
262
|
+
entries.param = entry.path
|
|
263
|
+
html = html.replaceAll(assetPath(base, `assets/${paramPath}`), assetPath(base, `assets/${entry.path}`))
|
|
264
|
+
}
|
|
265
|
+
if (record.capabilities.hasEffects) {
|
|
266
|
+
const entry = retainRouteEntry(effectPath, output => printEffectEntry(runtimeEffects(record.plan.effects, record.capabilities.navigable), output, handlerModules, join(outputDirectory, "assets"), base, entries.param, routeRuntimeName, record.capabilities.navigable, runtimeDirectory), routeEntrySources, routeEntryPaths, outputDirectory)
|
|
267
|
+
entries.effect = entry.path
|
|
268
|
+
html = html.replaceAll(assetPath(base, `assets/${effectPath}`), assetPath(base, `assets/${entry.path}`))
|
|
269
|
+
}
|
|
270
|
+
if (record.plan.events.some(event => event.native)) {
|
|
271
|
+
const modules = [...new Set(record.plan.events.filter(event => event.native).map(event => event.native.module))]
|
|
272
|
+
const nativeRuntime = assetPath(base, `assets/runtime/${family.id}/kudzu-native.js`)
|
|
273
|
+
const entry = retainRouteEntry(nativePath, () => printNativeEntrySource(modules, nativeRuntime), routeEntrySources, routeEntryPaths, outputDirectory)
|
|
274
|
+
entries.native = entry.path
|
|
275
|
+
html = html.replaceAll(assetPath(base, `assets/${nativePath}`), assetPath(base, `assets/${entry.path}`))
|
|
276
|
+
}
|
|
277
|
+
if (family) {
|
|
278
|
+
html = html.replaceAll(runtimePlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/${routeRuntimeName}`)))
|
|
279
|
+
html = html.replaceAll(bindingPlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/kudzu-binding.js`)))
|
|
280
|
+
html = html.replaceAll(listPlaceholder, escapeAttribute(assetPath(base, `assets/runtime/${family.id}/kudzu-list.js`)))
|
|
281
|
+
}
|
|
282
|
+
if (navigationGroup) html = html.replaceAll(escapeAttribute(navigationAssets.get(record.route)), escapeAttribute(navigationGroup.assetPath))
|
|
283
|
+
const finalRecord = createRouteBuildRecord({
|
|
284
|
+
route: record.route,
|
|
285
|
+
output: record.output,
|
|
286
|
+
html,
|
|
287
|
+
plan: record.plan,
|
|
288
|
+
handlerReferences: result.handlerReferences,
|
|
289
|
+
styles: record.artifacts.styles,
|
|
290
|
+
capabilities: record.capabilities,
|
|
291
|
+
entries,
|
|
292
|
+
runtimeSchema
|
|
293
|
+
})
|
|
294
|
+
if (family) runtimeFamilyByRecord.set(finalRecord, family)
|
|
295
|
+
if (navigationGroup) navigationAssets.set(finalRecord.route, navigationGroup.assetPath)
|
|
296
|
+
return finalRecord
|
|
297
|
+
})
|
|
298
|
+
|
|
248
299
|
const assetsDirectory = join(outputDirectory, "assets")
|
|
249
300
|
await mkdir(assetsDirectory, { recursive: true })
|
|
250
301
|
const { handlerModules: emittedHandlerModules, workerReferences: renderedWorkerReferences, styles: renderedStyles } = planRouteArtifacts(routeRecords, handlerModules, workerReferences, module => assetPath(base, `assets/${module.path}`))
|
|
251
302
|
const renderedStyleUrls = new Set(renderedStyles)
|
|
252
303
|
if (renderedWorkerReferences.length && await exists(join(publicDirectory, "assets", "workers"))) throw new Error("public/assets/workers collides with Kudzu's generated Worker asset namespace")
|
|
253
|
-
const workerAssets = await project.workerCompiler.emit(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
|
|
304
|
+
const { assets: workerAssets, outputs: workerOutputs } = await project.workerCompiler.emit(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
|
|
254
305
|
for (const module of emittedHandlerModules) {
|
|
255
306
|
for (const reference of workerReferences) {
|
|
256
307
|
if (reference.module !== assetPath(base, `assets/${module.path}`)) continue
|
|
@@ -260,63 +311,55 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
260
311
|
if (module.code.includes("/__kudzu_worker_")) throw new Error(`Worker URL placeholder survived in ${module.path}`)
|
|
261
312
|
}
|
|
262
313
|
const plans = routeRecords.map(record => record.plan)
|
|
263
|
-
const
|
|
264
|
-
const {
|
|
265
|
-
routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount, dependencyStateSeeds: dependencyStateSeedCount },
|
|
266
|
-
events: { command: commandEvents, hasNativeHandlers },
|
|
267
|
-
bindings: { count: bindingCount },
|
|
268
|
-
lists,
|
|
269
|
-
effects: { any: hasEffects, derivedDependencies: hasDerivedEffectDependencies, captures: hasEffectCaptures },
|
|
270
|
-
captures: { nestedState: hasNestedStateCaptures, setter: hasSetterCaptures },
|
|
271
|
-
runtime: { shared: hasSharedRuntime, dependency: hasDependencyRuntime }
|
|
272
|
-
} = capabilityIR
|
|
273
|
-
const runtimeName = usesDependencyRuntime => usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
|
|
314
|
+
const behaviorCount = routeRecords.filter(record => record.capabilities.hasBehaviors).length
|
|
274
315
|
for (let offset = 0; offset < routeRecords.length; offset += 64) {
|
|
275
316
|
await Promise.all(routeRecords.slice(offset, offset + 64).map(async record => {
|
|
276
317
|
const routeDirectory = join(outputDirectory, record.output)
|
|
277
318
|
await mkdir(routeDirectory, { recursive: true })
|
|
278
|
-
|
|
279
|
-
await writeFile(join(routeDirectory, "index.html"), html)
|
|
319
|
+
if ([runtimePlaceholder, bindingPlaceholder, listPlaceholder].some(placeholder => record.html.includes(placeholder))) throw new Error(`Runtime family placeholder survived in ${record.route}`)
|
|
320
|
+
await writeFile(join(routeDirectory, "index.html"), preloadModules(record.html))
|
|
280
321
|
}))
|
|
281
322
|
}
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
await
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
323
|
+
for (const family of runtimePlan.families) {
|
|
324
|
+
const capabilityIR = family.capability
|
|
325
|
+
const { bindings, captures, effects, events, lists, routes, runtime } = capabilityIR
|
|
326
|
+
const familyDirectory = join(assetsDirectory, "runtime", family.id)
|
|
327
|
+
await mkdir(familyDirectory, { recursive: true })
|
|
328
|
+
if (runtime.dependency) {
|
|
329
|
+
const source = await readFile(new URL("./dependency-runtime.js", import.meta.url), "utf8")
|
|
330
|
+
await writeJavaScript(join(familyDirectory, "kudzu-deps.js"), specializeRuntime(source, events.command, routes.dependencyStateSeeds > 0), minify)
|
|
331
|
+
} else {
|
|
332
|
+
const source = await readFile(new URL(runtime.shared ? "./shared-runtime.js" : "./runtime.js", import.meta.url), "utf8")
|
|
333
|
+
await writeJavaScript(join(familyDirectory, "kudzu.js"), generateCoreRuntime(source, capabilityIR), minify)
|
|
334
|
+
}
|
|
335
|
+
if (bindings.count || events.hasNativeHandlers || effects.captures) await writeJavaScript(join(familyDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify, {
|
|
336
|
+
"globalThis.__KUDZU_CAPTURE_STATE__": String(captures.nestedState),
|
|
337
|
+
"globalThis.__KUDZU_CAPTURE_SETTER__": String(captures.setter)
|
|
338
|
+
})
|
|
339
|
+
if (effects.any) {
|
|
340
|
+
const generated = generateEffectRuntime(await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8"), capabilityIR)
|
|
341
|
+
await writeBundledJavaScript(join(familyDirectory, "kudzu-effect.js"), generated.source, minify, generated.define)
|
|
342
|
+
}
|
|
343
|
+
if (bindings.count || lists.styleCount) await writeJavaScript(join(familyDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
|
|
344
|
+
if (bindings.count) {
|
|
345
|
+
const generated = generateBindingRuntime(await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"), capabilityIR, family.navigation)
|
|
346
|
+
await writeBundledJavaScript(join(familyDirectory, "kudzu-binding.js"), generated.source, minify, generated.define)
|
|
347
|
+
}
|
|
348
|
+
if (effects.derivedDependencies || lists.selectors) await writeJavaScript(join(familyDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
|
|
349
|
+
if (lists.count) {
|
|
350
|
+
const generated = generateListRuntime(await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"), capabilityIR)
|
|
351
|
+
await writeBundledJavaScript(join(familyDirectory, "kudzu-list.js"), generated.source, minify, generated.define)
|
|
352
|
+
}
|
|
353
|
+
if (events.hasNativeHandlers) {
|
|
354
|
+
const generated = generateNativeRuntime(await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"), capabilityIR)
|
|
355
|
+
await writeJavaScript(join(familyDirectory, "kudzu-native.js"), generated.source, minify, generated.define)
|
|
356
|
+
}
|
|
315
357
|
}
|
|
358
|
+
for (const [path, source] of routeEntrySources) if (path.startsWith("native/")) await writeRetainedRouteEntry(path, source, assetsDirectory, minify, routeEntryTransforms)
|
|
316
359
|
if (navigationGroups.length) {
|
|
317
360
|
const navigationSource = await readFile(new URL("./navigation-runtime.js", import.meta.url), "utf8")
|
|
318
361
|
for (const group of navigationGroups) {
|
|
319
|
-
await writeJavaScript(join(assetsDirectory, group.assetName), generateNavigationRuntime(navigationSource, group), minify)
|
|
362
|
+
await writeJavaScript(join(assetsDirectory, "runtime", group.runtimeFamily.id, group.assetName), generateNavigationRuntime(navigationSource, group), minify)
|
|
320
363
|
}
|
|
321
364
|
}
|
|
322
365
|
for (const handlerModule of emittedHandlerModules) {
|
|
@@ -333,8 +376,9 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
333
376
|
await mkdir(dirname(output), { recursive: true })
|
|
334
377
|
await writeJavaScript(output, module.code, minify)
|
|
335
378
|
}
|
|
379
|
+
let handlerMetafile
|
|
336
380
|
if (clientModules.length || emittedHandlerModules.some(module => module.hasPackageImports)) {
|
|
337
|
-
await bundle({
|
|
381
|
+
const result = await bundle({
|
|
338
382
|
entryPoints: emittedHandlerModules.map(module => join(assetsDirectory, module.path)),
|
|
339
383
|
outbase: join(assetsDirectory, "handlers"),
|
|
340
384
|
outdir: join(assetsDirectory, "handlers"),
|
|
@@ -347,12 +391,16 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
347
391
|
target: "es2022",
|
|
348
392
|
minify,
|
|
349
393
|
legalComments: "none",
|
|
394
|
+
metafile: true,
|
|
350
395
|
logLevel: "silent"
|
|
351
396
|
})
|
|
397
|
+
handlerMetafile = result.metafile
|
|
352
398
|
await rm(join(assetsDirectory, "modules"), { recursive: true, force: true })
|
|
353
399
|
}
|
|
354
400
|
const sortedRewrites = rewrites.sort((left, right) => runtimeSpecificity(right) - runtimeSpecificity(left) || left.pattern.localeCompare(right.pattern))
|
|
355
401
|
await writeFile(join(workDirectory, "kudzu-plan.json"), JSON.stringify({ routes: plans, rewrites: sortedRewrites }, null, 2))
|
|
402
|
+
const artifacts = createRouteArtifactReport(routeRecords, { base, handlerMetafile, outputDirectory, navigationAssets, runtimeFamilies: runtimePlan.families, runtimeFamilyByRecord, workerReferences: renderedWorkerReferences, workerOutputs })
|
|
403
|
+
await writeFile(join(workDirectory, "kudzu-artifacts.json"), JSON.stringify(artifacts, null, 2))
|
|
356
404
|
const emittedCssFiles = new Set()
|
|
357
405
|
for (const file of cssFiles.filter(file => renderedStyleUrls.has(assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)))) {
|
|
358
406
|
const output = join(assetsDirectory, relative(sourceDirectory, file))
|
|
@@ -384,7 +432,7 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
384
432
|
}
|
|
385
433
|
if (config.afterBuild !== undefined) {
|
|
386
434
|
if (typeof config.afterBuild !== "function") throw new Error("kudzu.config afterBuild must be a function")
|
|
387
|
-
await config.afterBuild({ root, outDir: outputDirectory, sourceDir: sourceDirectory, base, routes: plans.map(plan => plan.route), plans, rewrites: sortedRewrites })
|
|
435
|
+
await config.afterBuild({ root, outDir: outputDirectory, sourceDir: sourceDirectory, base, routes: plans.map(plan => plan.route), plans, rewrites: sortedRewrites, artifacts })
|
|
388
436
|
}
|
|
389
437
|
|
|
390
438
|
return { result: { sourceResults }, pageCount: plans.length, behaviorCount }
|
|
@@ -489,10 +537,9 @@ function inlineQueryFormCarry(html, plan) {
|
|
|
489
537
|
return html.replace("</body>", `${script}</body>`)
|
|
490
538
|
}
|
|
491
539
|
|
|
492
|
-
function printNativeEntrySource(modules,
|
|
540
|
+
function printNativeEntrySource(modules, runtime) {
|
|
493
541
|
const imports = modules.map((module, index) => `import * as __kNativeModule${index} from ${JSON.stringify(module)}`).join("\n")
|
|
494
542
|
const registrations = modules.map((module, index) => `[${JSON.stringify(module)}, __kNativeModule${index}]`).join(",")
|
|
495
|
-
const runtime = assetPath(base, "assets/kudzu-native.js")
|
|
496
543
|
return `import { registerNativeModules } from ${JSON.stringify(runtime)}\n${imports}\nregisterNativeModules([${registrations}])`
|
|
497
544
|
}
|
|
498
545
|
|
|
@@ -545,7 +592,7 @@ async function writeBundledJavaScript(file, source, minify, define) {
|
|
|
545
592
|
stdin: { contents: source, resolveDir: dirname(file), sourcefile: file },
|
|
546
593
|
bundle: true,
|
|
547
594
|
write: false,
|
|
548
|
-
external: ["./kudzu.js", "./kudzu-binding.js", "./kudzu-serialization.js", "./kudzu-style.js"],
|
|
595
|
+
external: ["./kudzu.js", "./kudzu-binding.js", "./kudzu-collection-selector.js", "./kudzu-serialization.js", "./kudzu-style.js"],
|
|
549
596
|
define,
|
|
550
597
|
format: "esm",
|
|
551
598
|
target: "es2022",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path"
|
|
2
2
|
|
|
3
3
|
export function createEffectCodegen({ assetPath, inlineJson, relativeModulePath }) {
|
|
4
|
-
function printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName) {
|
|
4
|
+
function printEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base, paramPath, runtimeName) {
|
|
5
5
|
const hasCleanup = effects.some(effect => effect.cleanup)
|
|
6
6
|
const hasDependencies = effects.some(effect => effect.dependencies?.length || effect.itemDependencies?.length)
|
|
7
7
|
const hasOwners = effects.some(effect => effect.owner)
|
|
@@ -14,10 +14,10 @@ function printEffectEntry(effects, output, handlerModules, assetsDirectory, base
|
|
|
14
14
|
})
|
|
15
15
|
const imports = [
|
|
16
16
|
hasCleanup || hasDependencies || hasOwners
|
|
17
|
-
? `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(
|
|
18
|
-
: `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(
|
|
19
|
-
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(
|
|
20
|
-
...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(
|
|
17
|
+
? `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, runtimeName)))}\nconst { browserState, commitDom } = __kRuntime`
|
|
18
|
+
: `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, runtimeName)))}`,
|
|
19
|
+
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-effect.js")))}`,
|
|
20
|
+
...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-collection-selector.js")))}`] : []),
|
|
21
21
|
...(paramPath ? [`import ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, paramPath)))}`] : []),
|
|
22
22
|
...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
|
|
23
23
|
]
|
|
@@ -178,7 +178,7 @@ addEventListener("pagehide", event => {
|
|
|
178
178
|
})`
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
function printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
|
|
181
|
+
function printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base) {
|
|
182
182
|
const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
|
|
183
183
|
const moduleUrls = [...new Set(effects.map(effect => effect.module))]
|
|
184
184
|
const modules = moduleUrls.map(url => {
|
|
@@ -187,9 +187,9 @@ function printNavigableEffectEntry(effects, output, handlerModules, assetsDirect
|
|
|
187
187
|
return module
|
|
188
188
|
})
|
|
189
189
|
const imports = [
|
|
190
|
-
`import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(
|
|
191
|
-
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(
|
|
192
|
-
...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(
|
|
190
|
+
`import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu.js")))}`,
|
|
191
|
+
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-effect.js")))}`,
|
|
192
|
+
...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-collection-selector.js")))}`] : []),
|
|
193
193
|
...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
|
|
194
194
|
]
|
|
195
195
|
const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
|
|
@@ -300,7 +300,7 @@ ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState
|
|
|
300
300
|
}`
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
|
|
303
|
+
function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base) {
|
|
304
304
|
const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
|
|
305
305
|
const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
|
|
306
306
|
const moduleUrls = [...new Set(effects.map(effect => effect.module))]
|
|
@@ -310,9 +310,9 @@ function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsD
|
|
|
310
310
|
return module
|
|
311
311
|
})
|
|
312
312
|
const imports = [
|
|
313
|
-
`import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(
|
|
314
|
-
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(
|
|
315
|
-
...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(
|
|
313
|
+
`import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu.js")))}`,
|
|
314
|
+
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-effect.js")))}`,
|
|
315
|
+
...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, "kudzu-collection-selector.js")))}`] : []),
|
|
316
316
|
...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
|
|
317
317
|
]
|
|
318
318
|
const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
|
|
@@ -876,9 +876,9 @@ async function invokeCleanup() {
|
|
|
876
876
|
}${disposal}`
|
|
877
877
|
}
|
|
878
878
|
|
|
879
|
-
return (effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName, navigable) => navigable
|
|
879
|
+
return (effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName, navigable, runtimeDirectory = assetsDirectory) => navigable
|
|
880
880
|
? effects.some(effect => effect.owner)
|
|
881
|
-
? printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base)
|
|
882
|
-
: printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base)
|
|
883
|
-
: printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath, runtimeName)
|
|
881
|
+
? printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base)
|
|
882
|
+
: printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base)
|
|
883
|
+
: printEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base, paramPath, runtimeName)
|
|
884
884
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path"
|
|
2
2
|
|
|
3
3
|
export function createParamCodegen({ browserPath, inlineJson, relativeModulePath }) {
|
|
4
|
-
return function printParamEntry(schema, params, searchParams, searchParamsWritable, output,
|
|
4
|
+
return function printParamEntry(schema, params, searchParams, searchParamsWritable, output, runtimeDirectory, base, runtimeName, navigable) {
|
|
5
5
|
const hasSearch = searchParams.length || searchParamsWritable
|
|
6
6
|
const signature = hasSearch ? "pathname, search" : "pathname"
|
|
7
7
|
const prefix = navigable ? `export function initializeParams(${signature}) {\n${searchParamsWritable ? "globalThis.__kSetSearchParams = setSearchParams\n" : ""}` : `${schema ? "let pathname = location.pathname\n" : ""}${hasSearch ? "let search = location.search\n" : ""}`
|
|
@@ -66,7 +66,7 @@ function setSearchParams(update, replace) {
|
|
|
66
66
|
}
|
|
67
67
|
${navigable ? "" : `globalThis.__kSetSearchParams = setSearchParams
|
|
68
68
|
addEventListener("popstate", () => ${searchParams.length ? "initializeSearch(location.search)" : "undefined"})`}` : ""
|
|
69
|
-
return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(
|
|
69
|
+
return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(runtimeDirectory, runtimeName)))}
|
|
70
70
|
${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}`
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { dirname, relative, resolve, sep } from "node:path"
|
|
2
|
+
import { assetPath } from "./path-helpers.mjs"
|
|
3
|
+
import { assertRouteBuildRecord } from "./route-build-record.mjs"
|
|
4
|
+
import { planRouteCapabilities } from "./route-capability-planner.mjs"
|
|
5
|
+
import { capabilitySignature, planRuntimeFamilies } from "./runtime-family-planner.mjs"
|
|
6
|
+
|
|
7
|
+
export function createRouteArtifactReport(records, {
|
|
8
|
+
base = "",
|
|
9
|
+
handlerMetafile,
|
|
10
|
+
outputDirectory,
|
|
11
|
+
navigationAssets = new Map(),
|
|
12
|
+
runtimeFamilies,
|
|
13
|
+
runtimeFamilyByRecord,
|
|
14
|
+
workerReferences = [],
|
|
15
|
+
workerOutputs = new Map()
|
|
16
|
+
} = {}) {
|
|
17
|
+
for (const record of records) assertRouteBuildRecord(record)
|
|
18
|
+
if (!runtimeFamilies || !runtimeFamilyByRecord) ({ families: runtimeFamilies, familyByRecord: runtimeFamilyByRecord } = planRuntimeFamilies(records))
|
|
19
|
+
const handlerGraph = handlerMetafile ? outputGraph(handlerMetafile, outputDirectory, base) : new Map()
|
|
20
|
+
const routes = records.map(record => {
|
|
21
|
+
const capability = planRouteCapabilities([record], { navigationRouteCount: Number(record.capabilities.navigable) })
|
|
22
|
+
const handlerEntries = [...new Set(record.artifacts.handlers.map(reference => reference.module))].sort()
|
|
23
|
+
const handlerOutputs = closure(handlerEntries, handlerGraph, Boolean(handlerMetafile))
|
|
24
|
+
const workers = workerReferences
|
|
25
|
+
.filter(reference => record.artifacts.effects.some(effect => effect.module === reference.module && effect.handler === reference.handler))
|
|
26
|
+
.map(reference => {
|
|
27
|
+
const output = workerOutputs.get(reference.placeholder)
|
|
28
|
+
if (!output) throw new Error(`Worker output was not recorded: ${reference.root}`)
|
|
29
|
+
return { source: reference.root, entry: output.entry, chunks: [...output.chunks].sort() }
|
|
30
|
+
})
|
|
31
|
+
.sort((left, right) => left.source.localeCompare(right.source) || left.entry.localeCompare(right.entry))
|
|
32
|
+
return {
|
|
33
|
+
route: record.route,
|
|
34
|
+
capability: {
|
|
35
|
+
signature: capabilitySignature(capability),
|
|
36
|
+
manifest: capability
|
|
37
|
+
},
|
|
38
|
+
runtime: routeRuntimeEdges(record, capability, runtimeFamilyByRecord.get(record), base, navigationAssets.get(record.route)),
|
|
39
|
+
handlers: {
|
|
40
|
+
entries: handlerEntries,
|
|
41
|
+
chunks: handlerOutputs.filter(output => !handlerEntries.includes(output))
|
|
42
|
+
},
|
|
43
|
+
workers,
|
|
44
|
+
styles: [...record.artifacts.styles].sort()
|
|
45
|
+
}
|
|
46
|
+
}).sort((left, right) => left.route.localeCompare(right.route))
|
|
47
|
+
const owners = new Map()
|
|
48
|
+
for (const route of routes) {
|
|
49
|
+
for (const path of [...route.handlers.chunks, ...route.workers.flatMap(worker => worker.chunks)]) {
|
|
50
|
+
const paths = owners.get(path) ?? new Set()
|
|
51
|
+
paths.add(route.route)
|
|
52
|
+
owners.set(path, paths)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
version: 2,
|
|
57
|
+
runtimeFamilies: runtimeFamilies.map(family => ({
|
|
58
|
+
id: family.id,
|
|
59
|
+
signature: family.signature,
|
|
60
|
+
navigation: family.navigation,
|
|
61
|
+
routes: records.filter(record => runtimeFamilyByRecord.get(record)?.id === family.id).map(record => record.route).sort(),
|
|
62
|
+
manifest: family.capability,
|
|
63
|
+
requirements: familyRuntimeRequirements(family, base)
|
|
64
|
+
})),
|
|
65
|
+
routes,
|
|
66
|
+
sharedChunks: [...owners].filter(([, routes]) => routes.size > 1).map(([path, routes]) => ({ path, routes: [...routes].sort() })).sort((left, right) => left.path.localeCompare(right.path))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function routeRuntimeEdges(record, capability, family, base, navigationAsset) {
|
|
71
|
+
const entries = Object.values(record.entries).map(path => assetPath(base, `assets/${path}`))
|
|
72
|
+
if (navigationAsset) entries.push(navigationAsset)
|
|
73
|
+
if (!family) return { family: null, entries: [...new Set(entries)].sort(), requirements: [] }
|
|
74
|
+
const modules = []
|
|
75
|
+
const add = name => modules.push(runtimeAsset(base, family.id, name))
|
|
76
|
+
if (record.capabilities.hasBehaviors || record.capabilities.navigable) add(record.capabilities.usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js")
|
|
77
|
+
if (record.capabilities.hasBindings) add("kudzu-binding.js")
|
|
78
|
+
if (record.capabilities.hasLists) add("kudzu-list.js")
|
|
79
|
+
if (record.capabilities.hasBindings || record.capabilities.hasLists && family.capability.lists.styleCount) add("kudzu-style.js")
|
|
80
|
+
if (capability.effects.derivedDependencies || record.capabilities.hasLists && family.capability.lists.selectors) add("kudzu-collection-selector.js")
|
|
81
|
+
if (record.capabilities.hasBindings || capability.events.hasNativeHandlers || record.capabilities.hasEffects && family.capability.effects.captures) add("kudzu-serialization.js")
|
|
82
|
+
if (record.capabilities.hasEffects) add("kudzu-effect.js")
|
|
83
|
+
if (capability.events.hasNativeHandlers) add("kudzu-native.js")
|
|
84
|
+
return { family: family.id, entries: [...new Set(entries)].sort(), requirements: [...new Set(modules)].sort() }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function familyRuntimeRequirements(family, base) {
|
|
88
|
+
const { bindings, effects, events, lists, runtime } = family.capability
|
|
89
|
+
const names = [runtime.dependency ? "kudzu-deps.js" : "kudzu.js"]
|
|
90
|
+
if (bindings.count || events.hasNativeHandlers || effects.captures) names.push("kudzu-serialization.js")
|
|
91
|
+
if (effects.any) names.push("kudzu-effect.js")
|
|
92
|
+
if (bindings.count || lists.styleCount) names.push("kudzu-style.js")
|
|
93
|
+
if (bindings.count) names.push("kudzu-binding.js")
|
|
94
|
+
if (effects.derivedDependencies || lists.selectors) names.push("kudzu-collection-selector.js")
|
|
95
|
+
if (lists.count) names.push("kudzu-list.js")
|
|
96
|
+
if (events.hasNativeHandlers) names.push("kudzu-native.js")
|
|
97
|
+
return names.map(name => runtimeAsset(base, family.id, name)).sort()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const runtimeAsset = (base, id, name) => assetPath(base, `assets/runtime/${id}/${name}`)
|
|
101
|
+
|
|
102
|
+
function outputGraph(metafile, outputDirectory, base) {
|
|
103
|
+
if (!metafile || typeof metafile !== "object" || !metafile.outputs || typeof metafile.outputs !== "object") throw new Error("Invalid esbuild metafile")
|
|
104
|
+
if (typeof outputDirectory !== "string") throw new Error("Route artifact reporting requires an output directory")
|
|
105
|
+
const paths = new Map()
|
|
106
|
+
for (const output of Object.keys(metafile.outputs)) paths.set(resolve(output), outputUrl(resolve(output), outputDirectory, base))
|
|
107
|
+
const graph = new Map()
|
|
108
|
+
for (const [output, metadata] of Object.entries(metafile.outputs)) {
|
|
109
|
+
const absolute = resolve(output)
|
|
110
|
+
const url = paths.get(absolute)
|
|
111
|
+
const imports = (metadata.imports ?? []).filter(entry => !entry.external).map(entry => paths.get(resolve(entry.path)) ?? paths.get(resolve(dirname(absolute), entry.path))).filter(Boolean)
|
|
112
|
+
graph.set(url, [...new Set(imports)].sort())
|
|
113
|
+
}
|
|
114
|
+
return graph
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function outputUrl(output, outputDirectory, base) {
|
|
118
|
+
const path = relative(outputDirectory, output)
|
|
119
|
+
if (!path || path === ".." || path.startsWith(`..${sep}`)) throw new Error(`Bundled output is outside the build directory: ${output}`)
|
|
120
|
+
return assetPath(base, path.replaceAll(sep, "/"))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function closure(entries, graph, requireEntries) {
|
|
124
|
+
const visited = new Set()
|
|
125
|
+
const visit = output => {
|
|
126
|
+
if (visited.has(output)) return
|
|
127
|
+
if (requireEntries && !graph.has(output)) throw new Error(`Bundled handler entry was not emitted: ${output}`)
|
|
128
|
+
visited.add(output)
|
|
129
|
+
for (const imported of graph.get(output) ?? []) visit(imported)
|
|
130
|
+
}
|
|
131
|
+
for (const entry of entries) visit(entry)
|
|
132
|
+
return [...visited].sort()
|
|
133
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { assertRouteBuildRecord } from "./route-build-record.mjs"
|
|
3
|
+
import { planRouteCapabilities } from "./route-capability-planner.mjs"
|
|
4
|
+
|
|
5
|
+
export function capabilitySignature(capability) {
|
|
6
|
+
return createHash("sha256").update(JSON.stringify(capability)).digest("hex")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function planRuntimeFamilies(records, navigationGroups = []) {
|
|
10
|
+
for (const record of records) assertRouteBuildRecord(record)
|
|
11
|
+
const grouped = new Set()
|
|
12
|
+
const scopes = []
|
|
13
|
+
for (const group of navigationGroups) {
|
|
14
|
+
const groupRecords = group.buildRecords ?? group.records ?? []
|
|
15
|
+
if (!groupRecords.length) continue
|
|
16
|
+
for (const record of groupRecords) {
|
|
17
|
+
if (!records.includes(record)) throw new Error(`Navigation runtime family contains an unknown route: ${record.route}`)
|
|
18
|
+
if (grouped.has(record)) throw new Error(`Route belongs to multiple runtime families: ${record.route}`)
|
|
19
|
+
grouped.add(record)
|
|
20
|
+
}
|
|
21
|
+
scopes.push({ navigation: true, records: groupRecords })
|
|
22
|
+
}
|
|
23
|
+
for (const record of records) if (!grouped.has(record) && record.capabilities.hasBehaviors) scopes.push({ navigation: false, records: [record] })
|
|
24
|
+
|
|
25
|
+
const familiesBySignature = new Map()
|
|
26
|
+
const signaturesById = new Map()
|
|
27
|
+
const familyByRecord = new Map()
|
|
28
|
+
for (const scope of scopes) {
|
|
29
|
+
const capability = planRouteCapabilities(scope.records, { navigationRouteCount: scope.navigation ? scope.records.length : 0 })
|
|
30
|
+
const descriptor = { version: 1, navigation: scope.navigation, capability }
|
|
31
|
+
const signature = capabilitySignature(descriptor)
|
|
32
|
+
const id = signature.slice(0, 16)
|
|
33
|
+
const existingSignature = signaturesById.get(id)
|
|
34
|
+
if (existingSignature && existingSignature !== signature) throw new Error(`Runtime family ID collision: ${id}`)
|
|
35
|
+
signaturesById.set(id, signature)
|
|
36
|
+
let family = familiesBySignature.get(signature)
|
|
37
|
+
if (!family) {
|
|
38
|
+
family = { id, signature, navigation: scope.navigation, capability, records: [] }
|
|
39
|
+
familiesBySignature.set(signature, family)
|
|
40
|
+
}
|
|
41
|
+
for (const record of scope.records) {
|
|
42
|
+
family.records.push(record)
|
|
43
|
+
familyByRecord.set(record, family)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const families = [...familiesBySignature.values()].map(family => ({ ...family, records: [...family.records].sort((left, right) => left.route.localeCompare(right.route)) })).sort((left, right) => left.id.localeCompare(right.id))
|
|
47
|
+
return { families, familyByRecord }
|
|
48
|
+
}
|
|
@@ -130,7 +130,7 @@ export function createWorkerCompiler({
|
|
|
130
130
|
|
|
131
131
|
const emit = async (references, sourceFiles, assetsDirectory, base, minify) => {
|
|
132
132
|
const roots = [...new Set(references.map(reference => resolve(sourceDirectory, reference.root)))].sort()
|
|
133
|
-
if (!roots.length) return new Map()
|
|
133
|
+
if (!roots.length) return { assets: new Map(), outputs: new Map() }
|
|
134
134
|
await validateGraphs(roots, sourceFiles)
|
|
135
135
|
const workerDirectory = resolve(assetsDirectory, "workers")
|
|
136
136
|
await mkdir(workerDirectory, { recursive: true })
|
|
@@ -152,16 +152,35 @@ export function createWorkerCompiler({
|
|
|
152
152
|
logLevel: "silent"
|
|
153
153
|
})
|
|
154
154
|
const emitted = new Map()
|
|
155
|
+
const outputUrls = new Map(Object.keys(result.metafile.outputs).map(output => {
|
|
156
|
+
const outputFile = resolve(root, output)
|
|
157
|
+
return [outputFile, assetPath(base, relative(resolve(assetsDirectory, ".."), outputFile).replaceAll(sep, "/"))]
|
|
158
|
+
}))
|
|
159
|
+
const outputImports = new Map(Object.entries(result.metafile.outputs).map(([output, metadata]) => {
|
|
160
|
+
const outputFile = resolve(root, output)
|
|
161
|
+
return [outputUrls.get(outputFile), (metadata.imports ?? []).filter(entry => !entry.external).map(entry => outputUrls.get(resolve(root, entry.path)) ?? outputUrls.get(resolve(outputFile, "..", entry.path))).filter(Boolean)]
|
|
162
|
+
}))
|
|
163
|
+
const outputs = new Map()
|
|
155
164
|
for (const [output, metadata] of Object.entries(result.metafile.outputs)) {
|
|
156
165
|
if (!metadata.entryPoint) continue
|
|
157
166
|
const entry = resolve(root, metadata.entryPoint)
|
|
158
167
|
const rootReferences = references.filter(reference => resolve(sourceDirectory, reference.root) === entry)
|
|
159
168
|
const outputFile = resolve(root, output)
|
|
160
|
-
const url =
|
|
161
|
-
|
|
169
|
+
const url = outputUrls.get(outputFile)
|
|
170
|
+
const closure = new Set()
|
|
171
|
+
const visit = path => {
|
|
172
|
+
if (closure.has(path)) return
|
|
173
|
+
closure.add(path)
|
|
174
|
+
for (const imported of outputImports.get(path) ?? []) visit(imported)
|
|
175
|
+
}
|
|
176
|
+
visit(url)
|
|
177
|
+
for (const reference of rootReferences) {
|
|
178
|
+
emitted.set(reference.placeholder, url)
|
|
179
|
+
outputs.set(reference.placeholder, { entry: url, chunks: [...closure].filter(path => path !== url).sort() })
|
|
180
|
+
}
|
|
162
181
|
}
|
|
163
182
|
for (const reference of references) if (!emitted.has(reference.placeholder)) throw new Error(`Worker entry was not emitted: ${reference.root}`)
|
|
164
|
-
return emitted
|
|
183
|
+
return { assets: emitted, outputs }
|
|
165
184
|
}
|
|
166
185
|
|
|
167
186
|
return { candidate, emit, rejectConstructions, rejectOrdinaryImports, rewriteEffect }
|
package/framework/core.d.ts
CHANGED
package/framework/core.mjs
CHANGED
|
@@ -501,10 +501,10 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
501
501
|
? `<script type="module"${capability} src="${escapeAttribute(metadata.paramAsset)}"></script>`
|
|
502
502
|
: ""
|
|
503
503
|
const bindingRuntime = renderContext.hasBindings
|
|
504
|
-
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-binding.js")}"></script>`
|
|
504
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.bindingAsset ?? assetPath(metadata.base, "assets/kudzu-binding.js"))}"></script>`
|
|
505
505
|
: ""
|
|
506
506
|
const listRuntime = renderContext.hasLists
|
|
507
|
-
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-list.js")}"></script>`
|
|
507
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.listAsset ?? assetPath(metadata.base, "assets/kudzu-list.js"))}"></script>`
|
|
508
508
|
: ""
|
|
509
509
|
const effectRuntime = renderContext.hasEffects
|
|
510
510
|
? `<script type="module"${capability} src="${escapeAttribute(metadata.effectAsset)}"></script>`
|
package/framework/dev-server.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { extname, join, resolve, sep } from "node:path"
|
|
|
5
5
|
import { browserPath, withBase } from "./compiler/path-helpers.mjs"
|
|
6
6
|
import { stateSchema } from "./dev-state.js"
|
|
7
7
|
|
|
8
|
-
const devClient = (session, revision, schema) => `<script>(()=>{const show=event=>{let box=document.getElementById("__kudzu_error");if(!box){box=document.createElement("div");box.id="__kudzu_error";box.setAttribute("role","alert");box.setAttribute("aria-live","assertive");box.style.cssText="position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace";const title=document.createElement("strong"),text=document.createElement("pre");title.textContent="Kudzu build error";text.style.whiteSpace="pre-wrap";box.append(title,text);document.body.append(box)}box.querySelector("pre").textContent=event.data};const schema=${inlineJson(schema)},route=location.pathname+location.search+location.hash,urls=[...document.querySelectorAll('script[type="module"][src]')].map(node=>node.src).filter(url=>/\/assets\/kudzu(?:-
|
|
8
|
+
const devClient = (session, revision, schema) => `<script>(()=>{const show=event=>{let box=document.getElementById("__kudzu_error");if(!box){box=document.createElement("div");box.id="__kudzu_error";box.setAttribute("role","alert");box.setAttribute("aria-live","assertive");box.style.cssText="position:fixed;inset:0;z-index:2147483647;overflow:auto;padding:2rem;background:#200;color:#fff;font:16px/1.5 ui-monospace,monospace";const title=document.createElement("strong"),text=document.createElement("pre");title.textContent="Kudzu build error";text.style.whiteSpace="pre-wrap";box.append(title,text);document.body.append(box)}box.querySelector("pre").textContent=event.data};const schema=${inlineJson(schema)},route=location.pathname+location.search+location.hash,urls=[...document.querySelectorAll('script[type="module"][src]')].map(node=>node.src).filter(url=>/\/assets\/runtime\/[a-f0-9]+\/kudzu(?:-deps|-binding|-list|-native)?\.js$/.test(new URL(url).pathname));const devImport=import("/__kudzu_dev.js"),runtimeImports=Promise.allSettled(urls.map(url=>import(url)));const ready=(async()=>{const dev=await devImport,modules=await runtimeImports,runtime=modules.find(result=>result.status==="fulfilled"&&result.value.browserState instanceof Map&&typeof result.value.commitDom==="function")?.value;try{dev.restoreState(sessionStorage,route,runtime?.browserState,schema,runtime?.commitDom)}catch{}return{dev,runtime}})().catch(()=>({}));const events=new EventSource("/__kudzu_reload?session=${session}&revision=${revision}");let reloading=false;events.addEventListener("reload",async()=>{if(reloading)return;reloading=true;try{const{dev,runtime}=await ready;dev?.snapshotState(sessionStorage,route,runtime?.browserState,schema)}catch{}location.reload()});events.addEventListener("build-error",show)})()</script>`
|
|
9
9
|
|
|
10
10
|
export function parseDevPort(value) {
|
|
11
11
|
if (value === undefined || value.trim() === "") return 3000
|