@kudzujs/core 0.8.32 → 0.8.34

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.
@@ -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.32` 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.
5
+ The executable post-`0.8.34` 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
 
@@ -302,6 +302,18 @@ This queue orders the next investigations by general migration value. Start only
302
302
  - Same-root builds use an exclusive PID lock, stale locks fail closed, interrupted promotion backups recover on the next admitted build after lock removal, and successful replacement removes stale output.
303
303
  - Route HTML writes use bounded batches, keyed reverse/remove paths avoid repeated map reconstruction, and binding/condition commits share one dispatch without adding a runtime or public API.
304
304
 
305
+ ### Completed In 0.8.33
306
+
307
+ - Build root, source paths, source graph resolution, source records, and Worker compilation now belong to one explicit ProjectSession created for each build.
308
+ - Programmatic build and development entry points accept an explicit root while omitted roots preserve call-time CWD and existing CLI behavior.
309
+ - One process compiles two independent roots with identical module names and verifies isolated config, HTML, `.kudzu`, source results, and Worker output; no browser runtime or migration syntax changes.
310
+
311
+ ### Completed In 0.8.34
312
+
313
+ - Each unchanged source module is parsed and export-summarized once per ProjectSession, with source-text invalidation and no cross-project cache sharing.
314
+ - Read-only graph consumers share a canonical AST while importer normalization deep-clones every node and repairs independent parent links before transformation.
315
+ - A 100-importer fixture proves bounded parse/summary work and identical source results without changing accepted migration syntax or browser output.
316
+
305
317
  ## Cross-Cutting Performance Gates
306
318
 
307
319
  Every migration feature must preserve:
package/PERFORMANCE.md CHANGED
@@ -1,6 +1,57 @@
1
1
  # Performance Records
2
2
 
3
- Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, and `npm run benchmark:native` are maintained in this repository; `npm run benchmark:commerce` is a maintained paired runner over the public external storefront; older excluded-workspace sections are historical provenance only and are not current framework rankings.
3
+ Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, and `npm run benchmark:module-cache` are maintained in this repository; `npm run benchmark:commerce` is a maintained paired runner over the public external storefront; older excluded-workspace sections are historical provenance only and are not current framework rankings.
4
+
5
+ ## P0.7 Parsed Module And Export Summary Cache
6
+
7
+ Measured UTC 2026-08-11 on an Intel Core i5-9500 with 6 physical/logical cores, 31.2 GiB RAM, Linux 6.17.0-19-generic x64, Node 24.14.0, and npm 11.9.0. The baseline was clean tag `v0.8.33` at `65c96b13802c73e9c9a109cebbaac88bae7704a7`; both targets used the same installed dependencies. The implementation and maintained benchmark patch had SHA-256 `bca61b37f14c77488c4a35b62856faa6b471dd01081abcb69c47027b3b4615d9`, produced by:
8
+
9
+ ```bash
10
+ git diff --binary v0.8.33..v0.8.34 -- framework/compiler/project-session.mjs framework/compiler/source-compiler.mjs framework/compiler/worker-compiler.mjs test/compiler-passes.test.mjs test/module-cache-performance.mjs package.json | sha256sum
11
+ ```
12
+
13
+ The maintained in-memory fixture has 100 page importers sharing one barrel component and helper, for 103 unique modules. Each sample starts a fresh Node process; fixture generation and result hashing occur outside timing. Three warm-ups followed by 21 alternating samples compiled every reachable module. The candidate parsed and summarized each unique module once and created 200 importer-local normalization clones. Baseline and candidate produced the same 382,603-byte serialized source-result graph with SHA-256 `8c35b3f6d2c571306bd97c4d51d4af76ca244badd36c57363bf579ef961f41aa`.
14
+
15
+ | Target | Compiler median | Range | Peak RSS median | Parse / summary / clone misses |
16
+ |---|---:|---:|---:|---:|
17
+ | `v0.8.33` | 2,386.735 ms | 1,183.385–3,249.719 ms | 261.3 MiB | not instrumented |
18
+ | P0.7 candidate | 2,188.745 ms | 1,321.352–2,957.883 ms | 257.1 MiB | 103 / 103 / 200 |
19
+
20
+ Both timing arrays drifted upward during the run, making their raw 8.29% median difference unsuitable as an improvement claim. Round-paired candidate-minus-baseline differences had a +33.324 ms median, with the candidate faster in 9/21 pairs and the baseline faster in 12/21. Peak RSS ranges also overlap. This establishes bounded parse/summary work and identical compiler output with no material performance conclusion on this machine; it does not establish a speedup.
21
+
22
+ ```text
23
+ v0.8.33: [1512.096,1244.231,1183.385,1655.352,1669.380,2033.448,1820.868,1774.702,1847.652,1889.290,2386.735,2926.474,2853.159,3249.719,2716.963,2569.254,2531.427,2727.951,2483.411,2492.172,2597.774]
24
+ candidate: [1943.630,1416.969,1321.352,1603.967,1340.557,1440.993,1913.509,2060.240,1796.133,2011.796,2188.745,2888.145,2957.883,2947.417,2750.287,2745.883,2799.166,2620.866,2499.916,2541.054,2530.391]
25
+ paired candidate-baseline: [431.534,172.738,137.967,-51.385,-328.823,-592.455,92.641,285.538,-51.519,122.506,-197.990,-38.329,104.724,-302.302,33.324,176.629,267.739,-107.085,16.505,48.882,-67.383]
26
+ ```
27
+
28
+ ```bash
29
+ git worktree add --detach /tmp/opencode/kudzu-v0.8.33 v0.8.33
30
+ ln -s "$PWD/node_modules" /tmp/opencode/kudzu-v0.8.33/node_modules
31
+ BASELINE_ROOT=/tmp/opencode/kudzu-v0.8.33 CANDIDATE_ROOT="$PWD" WARMUPS=3 RUNS=21 npm run benchmark:module-cache
32
+ ```
33
+
34
+ ### Current Cross-Framework Whole-Build Context
35
+
36
+ This separate local check is not a parsed-module-cache comparison or maintained ranking. The external `/tmp/opencode/kudzu-dependency-benchmark` workspace is not Git-provenanced. It used Kudzu's current candidate, React 19.2.7, Vue 3.5.40, Svelte 5.56.6, Astro 7.1.1, and Vite 7.3.6. Every target emitted matched initial device content, and headless Chrome passed detail fetch, list filters, empty state, superseded async command, stale-result rejection, and HTTP error behavior. React, Vue, and Svelte perform separate client, SSR, and prerender builds; Astro uses its native build; Kudzu runs one static compiler build, so build stages and artifact architectures differ.
37
+
38
+ | Target | Build median | HTML raw / gzip | JavaScript raw / gzip | Total output |
39
+ |---|---:|---:|---:|---:|
40
+ | Kudzu | 856.2 ms | 5,647 B / 1,324 B | 30,712 B / 12,233 B | 36,359 B |
41
+ | React SSR | 2,802.0 ms | 1,338 B / 562 B | 195,864 B / 61,488 B | 197,202 B |
42
+ | Vue SSR | 2,295.3 ms | 1,348 B / 567 B | 69,643 B / 27,756 B | 70,991 B |
43
+ | Svelte SSR | 2,972.9 ms | 1,399 B / 582 B | 40,781 B / 15,898 B | 42,180 B |
44
+ | Astro native | 1,482.9 ms | 3,307 B / 1,229 B | 2,201 B / 883 B | 3,307 B |
45
+
46
+ ```text
47
+ Kudzu: [672.2,697.5,796.0,856.2,884.2,937.9,985.9]
48
+ React SSR: [2238.5,2345.7,2598.5,2802.0,2843.9,2848.5,3253.6]
49
+ Vue SSR: [1968.1,2068.9,2150.7,2295.3,2494.6,2617.1,3380.0]
50
+ Svelte SSR: [2615.6,2922.0,2926.6,2972.9,3020.5,3168.3,3739.9]
51
+ Astro native: [1444.9,1445.7,1451.2,1482.9,1732.5,1744.0,2236.9]
52
+ ```
53
+
54
+ The cross-framework result provides current whole-build context only. It does not show that Kudzu's module cache is faster than another framework's cache: this fixture has one application module per target and exposes no equivalent parser/cache counters. A cache-specific cross-framework claim would require matched 100-importer module graphs, equivalent output, equal cold/warm policy, and framework-specific cache instrumentation.
4
55
 
5
56
  ## 2026-08-11 React, Vue, And Svelte Check
6
57
 
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.32 - Staged and collision-safe output.** Builds complete in an isolated staging tree, reject public/generated collisions, preserve the previous `dist` on ordinary build and hook failures, recover interrupted promotion backups after a stale lock is cleared, and reject overlapping same-root builds. Read the [release notes](./RELEASES.md#0832---staged-and-collision-safe-output), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.32), or follow the [architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.34 - Session-local module cache.** Each unchanged source module is parsed and export-summarized once per ProjectSession, while every transformer receives an independent deep clone so mutable TypeScript AST state cannot leak across importers or projects. Read the [release notes](./RELEASES.md#0834---session-local-module-cache), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.34), 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,65 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.34 - Session-local module cache
4
+
5
+ Kudzu 0.8.34 completes P0.7 by making source parsing and supported-export summarization proportional to unique modules within one ProjectSession instead of repeated importer edges.
6
+
7
+ ### Changed in 0.8.34
8
+
9
+ - ProjectSession caches one canonical read-only TypeScript AST and one narrow export summary for each file/source-text pair.
10
+ - Reachability, static collection, Zustand, client-helper, style, layout-diagnostic, and Worker graph consumers reuse canonical module records.
11
+ - Importer normalization receives a deep clone of every node with independent parent links; transformed mutable AST is never shared between transformer contexts.
12
+ - Source-text changes invalidate the parsed tree and export summary together, and independent ProjectSessions never share records.
13
+ - Export summaries preserve the existing supported direct, aliased, default, and named re-export forms. Stable ModuleSymbol/SiteId identity and `export *` expansion remain P0.8 work.
14
+ - Optional injected counters expose parse, summary, and clone misses to tests and the maintained benchmark without production logging.
15
+
16
+ ### Performance
17
+
18
+ - A 100-importer fixture contains 103 unique page/barrel/component/helper modules. The candidate records exactly 103 parses, 103 export summaries, and 200 importer-local clones.
19
+ - Baseline and candidate produce the same 382,603-byte source-result graph and SHA-256 digest.
20
+ - The 21-pair timing run drifted materially on the measurement host. Paired candidate-minus-baseline differences had a +33.324 ms median, with the candidate faster in 9/21 pairs; no material speedup or regression is claimed.
21
+ - A separate matched SSR/native whole-build check measured Kudzu at 856.2 ms, Astro at 1,482.9 ms, Vue at 2,295.3 ms, React at 2,802.0 ms, and Svelte at 2,972.9 ms. Every target passed the same Chrome behavior validation; this is whole-build context, not a module-cache comparison.
22
+
23
+ ### Validation
24
+
25
+ - `npm run check`, `npm test`, `npm run test:package`, and all 193 tests pass.
26
+ - Unit coverage verifies cache hits, source-text invalidation, export summaries, cross-session isolation, independent cloned node identity, and clone-local parent links.
27
+ - Existing imported components, re-export cycles, static collections, Zustand, client helpers, Workers, source diagnostics, and generated artifacts remain covered.
28
+ - The repository build emits 149 pages; unaffected static routes remain complete zero-JavaScript documents.
29
+ - P0.8 stable ModuleSymbol and SiteId is next.
30
+
31
+ ### Upgrade
32
+
33
+ ```bash
34
+ npm install @kudzujs/core@^0.8.34
35
+ ```
36
+
37
+ ## 0.8.33 - Project-scoped compilation
38
+
39
+ Kudzu 0.8.33 completes P0.6 by replacing import-time project globals with an explicit build-scoped ProjectSession. Independent roots can now compile in one Node process without sharing paths, source records, graph resolution, or Worker compiler ownership.
40
+
41
+ ### Changed in 0.8.33
42
+
43
+ - Each `build()` creates a ProjectSession containing the absolute project root, `src`, `src/pages`, `.kudzu`, and `dist` paths.
44
+ - Source graph resolution, source indexes, reachable source sets, source compiler helpers, and Worker compilation are bound to that session instead of the directory where compiler modules were first imported.
45
+ - The internal programmatic build and development entry points accept an explicit `root`; omitted roots still use call-time `process.cwd()`, preserving existing `kudzu build` and `kudzu dev` behavior.
46
+ - Config loading, styles, static assets, route diagnostics, generated modules, output locking, staging, promotion, and development serving all resolve against the selected project.
47
+ - Parsed-module and export-summary caching remains deferred to P0.7; this release establishes ownership without adding speculative shared caches or changing browser output.
48
+
49
+ ### Validation
50
+
51
+ - `npm run check`, `npm test`, `npm run test:package`, and all 191 tests pass.
52
+ - One imported `build()` function compiles two roots with identical source filenames in sequence and verifies isolated config metadata, HTML, source results, `.kudzu` modules, and content-distinct Worker bundles.
53
+ - The standard CLI output-safety fixture still verifies staging, collision rejection, lock behavior, recovery, and replacement through call-time CWD.
54
+ - The repository build emits 148 pages, and existing static zero-JavaScript, interactive capability, Worker, navigation, ownership, and migration behavior remains covered.
55
+ - P0.7 parsed module and export summary caching is next.
56
+
57
+ ### Upgrade
58
+
59
+ ```bash
60
+ npm install @kudzujs/core@^0.8.33
61
+ ```
62
+
3
63
  ## 0.8.32 - Staged and collision-safe output
4
64
 
5
65
  Kudzu 0.8.32 completes P0.5 by building production artifacts away from the active deploy tree, rejecting public/generated collisions, and replacing `dist` only after generation and trusted `afterBuild()` work succeed.
@@ -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.32` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.5 staged and collision-safe output is complete; P0.6 ProjectSession and explicit root is next. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
14
+ The active post-`0.8.34` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.7 parsed module and export summary caching is complete; P0.8 stable ModuleSymbol and SiteId is next. 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,12 +1,14 @@
1
1
  # Current Compiler Architecture
2
2
 
3
- This maps the current `0.8.32` 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.
3
+ This maps the current `0.8.34` 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
 
7
7
  | Responsibility | Current owner | Current contract |
8
8
  |---|---|---|
9
9
  | CLI entry | [`bin/kudzu.mjs`](../../bin/kudzu.mjs) | Dispatches build and development commands. |
10
+ | Project session | [`framework/compiler/project-session.mjs`](../../framework/compiler/project-session.mjs), `createProjectSession()` | Owns one absolute root, standard project paths, source records, bound graph operations, and Worker compiler for a build. Omitted roots resolve from call-time CWD. |
11
+ | Parsed module cache | [`framework/compiler/project-session.mjs`](../../framework/compiler/project-session.mjs) | Parses each unchanged source module and summarizes its supported exports once per ProjectSession. Read-only graph/export/style/Worker consumers share the canonical tree; normalization receives a fresh deep clone with independent parent links. |
10
12
  | Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()` | Coordinates config, discovery, source compilation, RouteIR rendering, CapabilityIR projection, generator invocation, artifact emission, and `afterBuild`. |
11
13
  | Reachability/import resolution | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `reachableSourceFiles()`; [`framework/compiler/source-graph.mjs`](../../framework/compiler/source-graph.mjs), `ordinaryRuntimeDependencies()`, `resolveSourceImport()` | Starts from page entries, follows relative runtime imports/re-exports and validated Worker references, excludes unreachable migration source, and fails unresolved ordinary edges or dynamic imports at the importer source location before code generation. |
12
14
  | Ordered normalization | [`framework/compiler/normalization-pipeline.mjs`](../../framework/compiler/normalization-pipeline.mjs), `applyNormalizationPasses()`; [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `normalizeCompilerSource()` | Applies migration/resource passes in order and repairs TypeScript parent pointers after every structural change. Imported source uses the same pipeline. |
@@ -37,6 +39,7 @@ This maps the current `0.8.32` architecture built on the completed `0.8.23` Goal
37
39
 
38
40
  ```text
39
41
  src/pages entries + config
42
+ -> ProjectSession(root) with project paths, source records, parsed/export caches, graph, and Worker compiler
40
43
  -> project discovery and reachable relative graph
41
44
  -> compileSource()
42
45
  -> ordered normalization and parent repair
@@ -68,7 +71,7 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
68
71
  - Transient component rewrite indexes remain source-local AST indexes; handler, binding, derived, keyed, effect, and component ownership now have explicit JSON-safe source results.
69
72
  - `build()` still owns explicit artifact selection and filesystem writes after generator results are produced.
70
73
  - Runtime generators intentionally specialize readable authored sources through exact anchors; every required anchor fails closed, but a future generator format may remove this transitional dependency.
71
- - Source reachability and source compilation share one module because both consume the same normalization and import graph contracts.
74
+ - Source reachability and source compilation remain in one session-bound compiler factory because both consume the same normalization and import graph contracts. Export summaries deliberately preserve the existing narrow forms; stable ModuleSymbol and SiteId identity remains P0.8 work.
72
75
  - Imported, specialized, and compiler-synthesized trees still use conservative name/scope fallback where the source-local binding index does not own the complete AST; cross-module semantics remain deferred to stable ModuleSymbol and SiteId work.
73
76
 
74
77
  These are future simplification opportunities, not incomplete Goal A contracts. Goal A changed no source support, browser output semantics, or browser architecture.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Active execution plan after `0.8.32`. This document turns the current compiler audit into an ordered implementation program. It does not mark any 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.
5
+ Active execution plan after `0.8.34`. This document turns the current compiler audit into an ordered implementation program. It does not mark any 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
7
  [`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains authoritative for product invariants and fixture-driven feature selection. This plan is authoritative for the order and completion evidence of compiler generalization, large-application foundations, compatibility boundaries, AI tooling, and scale validation. If implementation evidence changes a boundary, update this document before broadening a patch.
8
8
 
@@ -34,7 +34,7 @@ The current limiting architecture is observable in these files:
34
34
 
35
35
  | Concern | Current owner | Limitation to remove |
36
36
  |---|---|---|
37
- | Project graph | `framework/compiler/source-graph.mjs`, `framework/build.mjs` | Global root, repeated parsing, narrow export resolution, no incremental project session |
37
+ | Project graph | `framework/compiler/project-session.mjs`, `source-graph.mjs`, `build.mjs` | Explicit build-scoped root, parsed-module/export-summary cache, and source ownership; narrow export resolution and no incremental invalidation remain |
38
38
  | Normalization | `framework/compiler/source-compiler.mjs`, focused passes | Pass order and package ownership are implicit; semantically equal source often follows different shape-specific paths |
39
39
  | Component/state analysis | `framework/compiler/source-compiler.mjs`, `analysis/component-analysis.mjs` | AST identity, identifier text, source offsets, and caller-side AST specialization remain central |
40
40
  | Handler/binding analysis | `framework/compiler/descriptor-session.mjs`, `handler-lowering.mjs` | Capture/import/state discovery is name-based and arbitrary handlers become code before IR finalization |
@@ -112,7 +112,7 @@ This is an incremental evolution of the current repository:
112
112
  - Resource-specific ownership passes into a package-neutral ResourceIR when evidence permits.
113
113
  - Serialized handler URL searches into explicit artifact references.
114
114
  - Site-wide capability unions into route capability signatures.
115
- - Full destructive development builds into project-session invalidation and rollback-safe output.
115
+ - Full development rebuilds into project-session invalidation; rollback-safe production output and explicit session ownership are complete.
116
116
  - String-only diagnostics into structured diagnostics suitable for machines and AI agents.
117
117
 
118
118
  ### Avoid
@@ -134,7 +134,9 @@ This is an incremental evolution of the current repository:
134
134
  - [x] P0.3 Graph diagnostics is complete in `0.8.30`. Reachable ordinary modules validate relative runtime imports/re-exports and reject every dynamic `import()` at the importer source location before compilation or generated module loading. Ordinary and Worker traversal retain separate ownership, type-only and unreachable edges remain excluded, focused page/helper/re-export/dynamic fixtures pass with all 189 tests and packed-package smoke, and no export-symbol graph, ProjectSession, runtime, or public API is added.
135
135
  - [x] P0.4 Async native handler ownership is complete in `0.8.31`. Mounted native registrations invalidate direct/captured setters, queued commits, and captured refs before listener removal. Chrome route, keyed-row, document-disposal, and synchronous event-dispatch checks, all 189 tests, and packed-package smoke pass without cancelling application promises or adding a scheduler.
136
136
  - [x] P0.5 Staged and collision-safe output is complete in `0.8.32`. Production output completes in one project-local staging tree, public files are compared against generated route/runtime/handler/chunk/Worker/CSS paths while copying, `afterBuild` runs before rollback-safe promotion, and ordinary failures preserve the prior `dist`. Same-root overlap and stale locks fail closed; after stale-lock removal, an interrupted promotion backup recovers on the next admitted build. Focused collision/replacement/dev checks pass with equivalent `v0.8.31` commerce deploy output; `.kudzu` remains compiler scratch for P0.6.
137
- - [ ] P0.6 Explicit ProjectSession is next. Root and caches must become build-scoped so independent projects can compile safely in one process.
137
+ - [x] P0.6 Explicit ProjectSession is complete in `0.8.33`. Each build owns an absolute root, project paths, source records, bound graph operations, and Worker compiler. Explicit-root build/dev entry points preserve omitted-root CLI CWD behavior. One imported build function compiles two same-shaped roots with isolated config, HTML, `.kudzu`, source results, and Worker bundles; all 191 tests and package checks pass without browser or source-syntax changes.
138
+ - [x] P0.7 Parsed module and export summary caching is complete in `0.8.34`. Canonical read-only source trees and narrow export summaries are invalidated together by source text and remain ProjectSession-local; every normalization context receives a deep clone with independent parent links. A 100-importer fixture parses and summarizes 103 unique page/barrel/component/helper modules exactly once and creates 200 importer-local clones. The maintained paired benchmark preserves the complete source-result digest and establishes no material timing or peak-RSS conclusion; all 193 tests and package checks pass without broadening exports, source syntax, or browser output.
139
+ - [ ] P0.8 Stable ModuleSymbol and SiteId is next. Cross-module declarations, aliases, re-exports, owners, and call sites must stop depending on transient AST identity outside one pass.
138
140
 
139
141
  ### P0: Semantic Correctness And Compiler Foundation
140
142
 
@@ -296,6 +298,8 @@ Every result carries a stable source-local binding slot, debug name, declaration
296
298
 
297
299
  **Done condition:** two independent roots compile in one process, current CLI behavior and artifacts remain unchanged, and source caches cannot leak between projects.
298
300
 
301
+ **Completed in `0.8.33`:** `createProjectSession()` resolves one explicit or call-time-CWD root and owns standard paths, source records, bound graph resolution, and a root-bound Worker compiler. Build, development, config, styles, routes, generated modules, locking, staging, and promotion consume that ownership. One process compiles two roots with identical module names and verifies distinct config, HTML, `.kudzu`, source results, and Worker bundles. Existing CLI output-safety coverage remains unchanged; no parsed-module cache, browser runtime, source syntax, or public migration API is added.
302
+
299
303
  ### PR 7: Parsed Module And Export Summary Cache
300
304
 
301
305
  **Objective:** make work proportional to unique modules instead of importer edges.
@@ -306,6 +310,8 @@ Every result carries a stable source-local binding slot, debug name, declaration
306
310
 
307
311
  **Done condition:** each unchanged module is parsed and summarized once per project session; transformed mutable AST is not shared across transformer contexts.
308
312
 
313
+ **Completed in `0.8.34`:** ProjectSession caches one canonical parsed tree and one supported-export summary per file/source-text pair. Reachability, static collection, Zustand, client-helper, style, layout-diagnostic, and Worker graph reads reuse canonical trees; importer normalization deep-clones every node and repairs clone-local parents. Optional injected counters add no production logging. Unit coverage proves source invalidation and cross-session isolation; 100 pages sharing one barrel component and helper produce 103 parse misses, 103 summary misses, and 200 independent normalization clones.
314
+
309
315
  ### PR 8: Stable ModuleSymbol And SiteId
310
316
 
311
317
  **Objective:** represent declarations, imports, re-exports, component calls, hooks, and ownership sites independently of transformed AST identity.
@@ -460,4 +466,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
460
466
 
461
467
  ## Immediate Decision
462
468
 
463
- PR 1 through PR 5 are complete. The next PR is **PR 6: ProjectSession And Explicit Root**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
469
+ PR 1 through PR 7 are complete. The next PR is **PR 8: Stable ModuleSymbol And SiteId**. Do not skip directly to a store, resource, router, virtualization, or 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.32` are completed scopes represented by package/release records.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.34` 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
 
@@ -23,6 +23,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
23
23
  | `0.8.30` | Validate ordinary runtime graph edges before code generation and reject dynamic imports at the importer source. | Page/helper/re-export/dynamic fixtures report original file/range/specifier without `.kudzu` paths; Worker, type-only, unreachable, output, and runtime behavior remain unchanged. |
24
24
  | `0.8.31` | Invalidate pending native-handler contexts when their route, keyed, conditional, or document DOM owner is released. | Late setters, queued commits, and captured refs cannot mutate replacement ownership; synchronous dispatch remains within the performance gate and runtime bytes are recorded. |
25
25
  | `0.8.32` | Stage and validate production output before rollback-safe promotion, reject same-root overlap, and tighten keyed browser paths. | Build/hook failures preserve the prior output, stale locks fail closed, interrupted backups recover on the next admitted build after lock removal, public/generated collisions fail, and deploy output remains equivalent. |
26
+ | `0.8.33` | Move root, graph, source records, compiler paths, and Worker ownership into an explicit build-scoped ProjectSession. | Two independent roots compile through one imported build entry without config, source, `.kudzu`, Worker, or output leakage; CLI CWD behavior remains unchanged. |
27
+ | `0.8.34` | Cache canonical parsed modules and narrow export summaries within one ProjectSession while cloning transformer input. | A 100-importer fixture parses and summarizes 103 unique modules once, creates independent normalization trees, preserves the complete source-result digest, and leaks no cache state across projects. |
26
28
 
27
29
  ## Sequence Rules
28
30
 
@@ -6,24 +6,18 @@ import { build as bundle, transform } from "esbuild"
6
6
  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
- import { collectClientModules, compileClientModule, compiledPath, compileSource, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles } from "./compiler/source-compiler.mjs"
9
+ import { createProjectSession } from "./compiler/project-session.mjs"
10
+ import { createSourceCompiler } from "./compiler/source-compiler.mjs"
10
11
  import { createParamCodegen } from "./compiler/param-codegen.mjs"
11
12
  import { planRouteCapabilities, usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
12
13
  import { generateBindingRuntime, generateCoreRuntime, generateEffectRuntime, generateNativeRuntime, generateNavigationRuntime, specializeRuntime } from "./compiler/runtime-codegen.mjs"
13
- import { emitWorkers } from "./compiler/worker-compiler.mjs"
14
14
  import { renderPage } from "./core.mjs"
15
15
  import { parseDevHost, parseDevPort, startDevServer } from "./dev-server.mjs"
16
16
 
17
17
  export { parseDevHost, parseDevPort }
18
18
  export { specializeRuntime }
19
19
 
20
- const root = process.cwd()
21
- const sourceDirectory = join(root, "src")
22
- const pagesDirectory = join(sourceDirectory, "pages")
23
- const workDirectory = join(root, ".kudzu")
24
- const outputDirectory = join(root, "dist")
25
-
26
- async function loadConfig() {
20
+ async function loadConfig(root) {
27
21
  for (const name of ["kudzu.config.mjs", "kudzu.config.js"]) {
28
22
  const file = join(root, name)
29
23
  if (!(await exists(file))) continue
@@ -34,7 +28,9 @@ async function loadConfig() {
34
28
  return {}
35
29
  }
36
30
 
37
- export async function build({ quiet = false, minify = true } = {}) {
31
+ export async function build({ quiet = false, minify = true, root: projectRoot = process.cwd() } = {}) {
32
+ const project = createProjectSession(projectRoot)
33
+ const { root, outputDirectory } = project
38
34
  const stagedOutput = join(root, ".kudzu-dist-staging")
39
35
  const backupOutput = join(root, ".kudzu-dist-backup")
40
36
  const lockPath = join(root, ".kudzu-build.lock")
@@ -42,7 +38,7 @@ export async function build({ quiet = false, minify = true } = {}) {
42
38
  try {
43
39
  await recoverOutput(outputDirectory, backupOutput)
44
40
  await rm(stagedOutput, { recursive: true, force: true })
45
- const { result, pageCount, behaviorCount } = await buildInto(stagedOutput, { minify })
41
+ const { result, pageCount, behaviorCount } = await buildInto(project, stagedOutput, { minify })
46
42
  await promoteOutput(stagedOutput, outputDirectory, backupOutput)
47
43
  if (!quiet) console.log(`Built ${pageCount} page(s), ${behaviorCount} interactive page(s) into dist/`)
48
44
  return result
@@ -59,11 +55,13 @@ export async function build({ quiet = false, minify = true } = {}) {
59
55
  }
60
56
  }
61
57
 
62
- async function buildInto(outputDirectory, { minify }) {
63
- const config = await loadConfig()
58
+ async function buildInto(project, outputDirectory, { minify }) {
59
+ const { root, sourceDirectory, pagesDirectory, workDirectory } = project
60
+ const { collectClientModules, compileClientModule, compiledPath, compileSource, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles } = createSourceCompiler(project)
61
+ const config = await loadConfig(root)
64
62
  const base = normalizeBase(config.base)
65
- const configuredStyles = normalizeStyles(config.styles, base)
66
- const publicDirectory = normalizePublicDirectory(config.publicDir)
63
+ const configuredStyles = normalizeStyles(config.styles, base, project)
64
+ const publicDirectory = normalizePublicDirectory(config.publicDir, project)
67
65
  const navigationGroups = normalizeNavigation(config.navigation)
68
66
  const navigationRoutes = navigationGroups.flatMap(group => group.routes)
69
67
  const navigationByRoute = new Map(navigationGroups.flatMap(group => group.routes.map(route => [route, group])))
@@ -86,15 +84,17 @@ async function buildInto(outputDirectory, { minify }) {
86
84
  const discoveredCssFiles = projectFiles.filter(file => file.toLowerCase().endsWith(".css") && !configuredStyleSources.has(file)).sort()
87
85
  if (!allSourceFiles.length) throw new Error("No TypeScript files found in src/")
88
86
  const allSourceFileSet = new Set(allSourceFiles)
89
- const sourceIndex = new Map(await Promise.all(allSourceFiles.map(async file => [file, await readFile(file, "utf8")])))
87
+ const sourceIndex = project.sourceIndex
88
+ for (const [file, source] of await Promise.all(allSourceFiles.map(async file => [file, await readFile(file, "utf8")]))) sourceIndex.set(file, source)
90
89
  const pageFiles = allSourceFiles.filter(file => file.startsWith(`${pagesDirectory}${sep}`) && file.endsWith(".tsx"))
91
90
  if (!pageFiles.length) throw new Error("No pages found in src/pages/")
92
91
  const sourceFiles = reachableSourceFiles(pageFiles, allSourceFileSet, sourceIndex)
93
- const sourceFileSet = new Set(sourceFiles)
92
+ const sourceFileSet = project.sourceFiles
93
+ for (const file of sourceFiles) sourceFileSet.add(file)
94
94
  const staticFiles = await safeStaticFiles(projectFiles)
95
95
  const cssFiles = orderSourceStyles(discoveredCssFiles, sourceFiles, sourceIndex, staticFiles)
96
96
  const importedAssets = new Set()
97
- const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base)
97
+ const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base, project)
98
98
 
99
99
  const sourceResults = []
100
100
  for (const file of sourceFiles) {
@@ -137,7 +137,7 @@ async function buildInto(outputDirectory, { minify }) {
137
137
  if (typeof module.default !== "function") throw new Error(`${relative(root, pageFile)} must export a default component`)
138
138
  if (Object.hasOwn(module, "layout") && typeof module.layout !== "function") throw layoutExportError(pageFile, sourceIndex.get(pageFile))
139
139
 
140
- const runtimeSchema = runtimeRouteSchema(module, pageFile)
140
+ const runtimeSchema = runtimeRouteSchema(module, pageFile, project)
141
141
  if (runtimeSchema) {
142
142
  const conflicting = rewrites.find(rewrite => sameRuntimePrecedence(rewrite, runtimeSchema))
143
143
  if (conflicting) throw new Error(`Ambiguous runtime routes: ${conflicting.route} and ${runtimeSchema.route}`)
@@ -149,9 +149,9 @@ async function buildInto(outputDirectory, { minify }) {
149
149
  segments: runtimeSchema.segments
150
150
  })
151
151
  }
152
- const entries = runtimeSchema ? [{ params: {}, props: {} }] : await staticPathEntries(module, pageFile)
152
+ const entries = runtimeSchema ? [{ params: {}, props: {} }] : await staticPathEntries(module, pageFile, root)
153
153
  for (const { params, props } of entries) {
154
- const route = runtimeSchema?.route ?? routeFromPage(pageFile, params)
154
+ const route = runtimeSchema?.route ?? routeFromPage(pageFile, params, pagesDirectory)
155
155
  const applicationRoute = `/${route}`
156
156
  const routePath = withBase(base, `/${route}`)
157
157
  const metadataContext = { route: routePath, params, props }
@@ -231,7 +231,7 @@ async function buildInto(outputDirectory, { minify }) {
231
231
  const renderedEffects = new Set(plans.flatMap(plan => plan.effects.map(effect => `${effect.module}:${effect.handler}`)))
232
232
  const renderedWorkerReferences = workerReferences.filter(reference => renderedEffects.has(`${reference.module}:${reference.handler}`))
233
233
  if (renderedWorkerReferences.length && await exists(join(publicDirectory, "assets", "workers"))) throw new Error("public/assets/workers collides with Kudzu's generated Worker asset namespace")
234
- const workerAssets = await emitWorkers(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
234
+ const workerAssets = await project.workerCompiler.emit(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
235
235
  for (const module of emittedHandlerModules) {
236
236
  for (const reference of workerReferences) {
237
237
  if (reference.module !== assetPath(base, `assets/${module.path}`)) continue
@@ -366,7 +366,7 @@ async function buildInto(outputDirectory, { minify }) {
366
366
  await writeFile(output, css)
367
367
  }
368
368
  if (await exists(publicDirectory)) {
369
- await copyPublic(publicDirectory, outputDirectory)
369
+ await copyPublic(publicDirectory, outputDirectory, outputDirectory, root)
370
370
  }
371
371
  if (config.afterBuild !== undefined) {
372
372
  if (typeof config.afterBuild !== "function") throw new Error("kudzu.config afterBuild must be a function")
@@ -376,7 +376,7 @@ async function buildInto(outputDirectory, { minify }) {
376
376
  return { result: { sourceResults }, pageCount: plans.length, behaviorCount }
377
377
  }
378
378
 
379
- async function acquireBuildLock(lockPath) {
379
+ async function acquireBuildLock(lockPath, root = dirname(lockPath)) {
380
380
  let lock
381
381
  try {
382
382
  lock = await open(lockPath, "wx")
@@ -432,7 +432,7 @@ async function promoteOutput(stagedOutput, finalOutput, backup) {
432
432
  if (previous) await rm(backup, { recursive: true, force: true }).catch(error => console.warn(`Built output was promoted, but ${backup} could not be removed and will be retried on the next build: ${error.message}`))
433
433
  }
434
434
 
435
- async function copyPublic(sourceDirectory, destinationDirectory, destinationRoot = destinationDirectory) {
435
+ async function copyPublic(sourceDirectory, destinationDirectory, destinationRoot, root) {
436
436
  for (const entry of await readdir(sourceDirectory, { withFileTypes: true })) {
437
437
  const source = join(sourceDirectory, entry.name)
438
438
  const destination = join(destinationDirectory, entry.name)
@@ -445,7 +445,7 @@ async function copyPublic(sourceDirectory, destinationDirectory, destinationRoot
445
445
  if (!generated) {
446
446
  await cp(source, destination, { recursive: entry.isDirectory(), force: false, errorOnExist: true })
447
447
  } else if (entry.isDirectory() && generated.isDirectory()) {
448
- await copyPublic(source, destination, destinationRoot)
448
+ await copyPublic(source, destination, destinationRoot, root)
449
449
  } else {
450
450
  throw new Error(`${relative(root, source)} collides with generated output ${relative(destinationRoot, destination).replaceAll(sep, "/")}`)
451
451
  }
@@ -514,11 +514,13 @@ async function writeBundledJavaScript(file, source, minify, define) {
514
514
  await writeFile(file, result.outputFiles[0].contents)
515
515
  }
516
516
 
517
- export async function dev({ port = parseDevPort(process.env.PORT), host = parseDevHost(process.env.HOST) } = {}) {
517
+ export async function dev({ port = parseDevPort(process.env.PORT), host = parseDevHost(process.env.HOST), root: projectRoot = process.cwd() } = {}) {
518
518
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid dev server port: ${port}`)
519
519
  if (typeof host !== "string" || !host.trim()) throw new Error(`Invalid dev server host: ${host}`)
520
- const base = normalizeBase((await loadConfig()).base)
521
- return startDevServer({ build, port, host, base, sourceDirectory, workDirectory, outputDirectory })
520
+ const project = createProjectSession(projectRoot)
521
+ const { root, sourceDirectory, workDirectory, outputDirectory } = project
522
+ const base = normalizeBase((await loadConfig(root)).base)
523
+ return startDevServer({ build: options => build({ ...options, root }), port, host, base, sourceDirectory, workDirectory, outputDirectory })
522
524
  }
523
525
 
524
526
  function inlineJson(value) {
@@ -533,11 +535,11 @@ function escapeAttribute(value) {
533
535
  return escapeHtml(value).replaceAll('"', "&quot;").replaceAll("'", "&#39;")
534
536
  }
535
537
 
536
- async function prepareSourceStyles(cssFiles, staticFiles, importedAssets, base) {
538
+ async function prepareSourceStyles(cssFiles, staticFiles, importedAssets, base, { root, sourceDirectory }) {
537
539
  const cssModules = new Map()
538
540
  const cssOutputs = new Map()
539
541
  for (const file of cssFiles) {
540
- let css = rewriteCssUrls(await readFile(file, "utf8"), file, staticFiles, importedAssets, base)
542
+ let css = rewriteCssUrls(await readFile(file, "utf8"), file, staticFiles, importedAssets, base, root, sourceDirectory)
541
543
  if (file.toLowerCase().endsWith(".module.css")) {
542
544
  if (/\bcomposes\s*:/i.test(maskCssCommentsAndStrings(css))) throw new Error(`${relative(root, file)} CSS Modules composes is not supported`)
543
545
  const prefix = `k${createHash("sha256").update(relative(sourceDirectory, file).replaceAll(sep, "/")).digest("hex").slice(0, 8)}`
@@ -551,7 +553,7 @@ async function prepareSourceStyles(cssFiles, staticFiles, importedAssets, base)
551
553
  return { cssModules, cssOutputs }
552
554
  }
553
555
 
554
- function rewriteCssUrls(css, file, staticFiles, importedAssets, base) {
556
+ function rewriteCssUrls(css, file, staticFiles, importedAssets, base, root, sourceDirectory) {
555
557
  let output = ""
556
558
  let cursor = 0
557
559
  let index = 0
@@ -596,7 +598,7 @@ function rewriteCssUrls(css, file, staticFiles, importedAssets, base) {
596
598
  continue
597
599
  }
598
600
  const value = css.slice(valueStart, end).trim()
599
- const replacement = rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base)
601
+ const replacement = rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base, root, sourceDirectory)
600
602
  output += css.slice(cursor, index) + (replacement ?? css.slice(index, close + 1))
601
603
  cursor = close + 1
602
604
  index = close + 1
@@ -604,7 +606,7 @@ function rewriteCssUrls(css, file, staticFiles, importedAssets, base) {
604
606
  return output + css.slice(cursor)
605
607
  }
606
608
 
607
- function rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base) {
609
+ function rewriteCssUrl(value, quote, file, staticFiles, importedAssets, base, root, sourceDirectory) {
608
610
  if (!value || value.startsWith("/") || value.startsWith("#") || value.startsWith("//") || /^[a-z][a-z\d+.-]*:/i.test(value)) return undefined
609
611
  const split = value.search(/[?#]/)
610
612
  const pathname = split === -1 ? value : value.slice(0, split)
@@ -646,7 +648,7 @@ function maskCssCommentsAndStrings(css) {
646
648
  return masked.join("")
647
649
  }
648
650
 
649
- function normalizeStyles(value, base) {
651
+ function normalizeStyles(value, base, { root }) {
650
652
  if (value === undefined) return { urls: [], sources: [] }
651
653
  if (!Array.isArray(value)) throw new Error("kudzu.config styles must be an array")
652
654
  const urls = []
@@ -680,7 +682,7 @@ function normalizeStyles(value, base) {
680
682
  return { urls, sources }
681
683
  }
682
684
 
683
- function normalizePublicDirectory(value) {
685
+ function normalizePublicDirectory(value, { root, outputDirectory, workDirectory }) {
684
686
  if (value === undefined) return join(root, "public")
685
687
  if (typeof value !== "string" || !value) throw new Error("kudzu.config publicDir must be a non-empty directory path")
686
688
  const directory = resolve(root, value)
@@ -771,7 +773,7 @@ function normalizeBase(value) {
771
773
  const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
772
774
  const printParamEntry = createParamCodegen({ browserPath, inlineJson, relativeModulePath })
773
775
 
774
- async function staticPathEntries(module, file) {
776
+ async function staticPathEntries(module, file, root) {
775
777
  if (typeof module.getStaticPaths !== "function") return [{ params: {}, props: {} }]
776
778
  const entries = await module.getStaticPaths()
777
779
  if (!Array.isArray(entries)) throw new Error(`${relative(root, file)} getStaticPaths() must return an array`)
@@ -785,11 +787,11 @@ async function staticPathEntries(module, file) {
785
787
  })
786
788
  }
787
789
 
788
- function runtimeRouteSchema(module, file) {
790
+ function runtimeRouteSchema(module, file, { root, pagesDirectory }) {
789
791
  if (!Object.hasOwn(module, "runtimeParams")) return undefined
790
792
  if (module.runtimeParams !== true) throw new Error(`${relative(root, file)} runtimeParams must be exactly true`)
791
793
  if (typeof module.getStaticPaths === "function") throw new Error(`${relative(root, file)} runtimeParams cannot be combined with getStaticPaths()`)
792
- const route = pageRoutePattern(file)
794
+ const route = pageRoutePattern(file, pagesDirectory)
793
795
  if (route.includes("[...")) throw new Error(`Catch-all routes are not supported: ${route}`)
794
796
  const names = new Set()
795
797
  const segments = route.split("/").map(segment => {
@@ -808,7 +810,7 @@ function runtimeRouteSchema(module, file) {
808
810
  return { route, segments, params: [...names] }
809
811
  }
810
812
 
811
- function pageRoutePattern(file) {
813
+ function pageRoutePattern(file, pagesDirectory) {
812
814
  const page = relative(pagesDirectory, file).replace(/\\/g, "/").replace(/\.tsx$/, "")
813
815
  return page === "index" ? "" : page.replace(/\/index$/, "")
814
816
  }
@@ -822,7 +824,7 @@ function sameRuntimePrecedence(left, right) {
822
824
  return left.segments.every((segment, index) => segment.literal === undefined || right.segments[index].literal === undefined || segment.literal === right.segments[index].literal)
823
825
  }
824
826
 
825
- function routeFromPage(file, params = {}) {
827
+ function routeFromPage(file, params = {}, pagesDirectory) {
826
828
  const page = relative(pagesDirectory, file).replace(/\\/g, "/").replace(/\.tsx$/, "")
827
829
  if (page.includes("[...")) throw new Error(`Catch-all routes are not supported: ${page}`)
828
830
  const filled = page.replace(/\[([^\]]+)\]/g, (_, name) => {
@@ -0,0 +1,90 @@
1
+ import { join, resolve } from "node:path"
2
+ import ts from "typescript"
3
+ import { assetPath } from "./path-helpers.mjs"
4
+ import { createSourceGraph } from "./source-graph.mjs"
5
+ import { createWorkerCompiler } from "./worker-compiler.mjs"
6
+
7
+ export function createProjectSession(projectRoot = process.cwd(), { counters, sourceIndex = new Map() } = {}) {
8
+ const root = resolve(projectRoot)
9
+ const sourceDirectory = join(root, "src")
10
+ const graph = createSourceGraph(root)
11
+ const modules = createModuleCache(sourceIndex, graph.parseSourceFile, counters)
12
+ return {
13
+ root,
14
+ sourceDirectory,
15
+ pagesDirectory: join(sourceDirectory, "pages"),
16
+ workDirectory: join(root, ".kudzu"),
17
+ outputDirectory: join(root, "dist"),
18
+ sourceIndex,
19
+ sourceFiles: new Set(),
20
+ graph,
21
+ modules,
22
+ workerCompiler: createWorkerCompiler({ root, sourceDirectory, assetPath, readSourceModule: modules.read, ...graph })
23
+ }
24
+ }
25
+
26
+ function createModuleCache(sourceIndex, parseSourceFile, counters) {
27
+ const records = new Map()
28
+ const increment = name => {
29
+ if (counters) counters[name] = (counters[name] ?? 0) + 1
30
+ }
31
+ const read = (file, source = sourceIndex.get(file)) => {
32
+ if (typeof source !== "string") throw new Error(`Source text is unavailable for ${file}`)
33
+ const cached = records.get(file)
34
+ if (cached?.source === source) return cached
35
+ const sourceFile = parseSourceFile(file, source)
36
+ increment("parsedModules")
37
+ const exports = summarizeExports(sourceFile)
38
+ increment("exportSummaries")
39
+ const record = { source, sourceFile, exports }
40
+ records.set(file, record)
41
+ return record
42
+ }
43
+ const clone = (file, factory, context) => {
44
+ const visit = node => {
45
+ const copy = factory.cloneNode(node)
46
+ ts.setTextRange(copy, node)
47
+ ts.setOriginalNode(copy, node)
48
+ return ts.visitEachChild(copy, visit, context)
49
+ }
50
+ const sourceFile = visit(read(file).sourceFile)
51
+ ts.setParentRecursive(sourceFile, false)
52
+ increment("clonedModules")
53
+ return sourceFile
54
+ }
55
+ return { clone, read }
56
+ }
57
+
58
+ function summarizeExports(sourceFile) {
59
+ const exports = new Map()
60
+ const add = (name, value) => {
61
+ if (!exports.has(name)) exports.set(name, value)
62
+ }
63
+ for (const statement of sourceFile.statements) {
64
+ if (ts.isFunctionDeclaration(statement)) {
65
+ const exported = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
66
+ const isDefault = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)
67
+ if (isDefault) add("default", { kind: "function", local: statement.name?.text })
68
+ if (exported && statement.name) add(statement.name.text, { kind: "function", local: statement.name.text })
69
+ continue
70
+ }
71
+ if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
72
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name)) add(declaration.name.text, { kind: "local", local: declaration.name.text })
73
+ continue
74
+ }
75
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
76
+ add("default", { kind: "local", local: statement.expression.text })
77
+ continue
78
+ }
79
+ if (!ts.isExportDeclaration(statement) || !statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue
80
+ for (const element of statement.exportClause.elements) {
81
+ if (element.isTypeOnly) continue
82
+ const name = element.name.text
83
+ const imported = (element.propertyName ?? element.name).text
84
+ add(name, statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
85
+ ? { kind: "reexport", imported, specifier: statement.moduleSpecifier.text, node: statement }
86
+ : { kind: "local", local: imported })
87
+ }
88
+ }
89
+ return exports
90
+ }
@@ -18,17 +18,16 @@ import { assetPath, relativeModulePath, withBase } from "./path-helpers.mjs"
18
18
  import { createReactMigrationPass, reactMemoExpression } from "./react-migration-pass.mjs"
19
19
  import { normalizeRenderControlFlow } from "./render-control-pass.mjs"
20
20
  import { createRouterPass } from "./router-pass.mjs"
21
- import { ordinaryRuntimeDependencies, parseSourceFile, resolveSourceImport, runtimeModuleReference } from "./source-graph.mjs"
22
- import { createWorkerCompiler } from "./worker-compiler.mjs"
21
+ import { createProjectSession } from "./project-session.mjs"
23
22
  import { createZustandPass } from "./zustand-pass.mjs"
24
23
 
25
- const root = process.cwd()
26
- const sourceDirectory = join(root, "src")
27
- const pagesDirectory = join(sourceDirectory, "pages")
28
- const workDirectory = join(root, ".kudzu")
24
+ export function createSourceCompiler(project) {
25
+ const { root, sourceDirectory, pagesDirectory, workDirectory, workerCompiler, modules } = project
26
+ const { ordinaryRuntimeDependencies, resolveSourceImport, runtimeModuleReference } = project.graph
27
+ const parseSourceFile = (file, source) => modules.read(file, source).sourceFile
29
28
  const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
30
29
 
31
- export function compileSource(file, sourceFiles, sourceIndex, staticFiles, cssModules, base) {
30
+ function compileSource(file, sourceFiles, sourceIndex, staticFiles, cssModules, base) {
32
31
  const importedAssets = new Set()
33
32
  const source = sourceIndex.get(file)
34
33
  const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
@@ -90,7 +89,7 @@ function emittedPackageReference(source, file, packages) {
90
89
  return found
91
90
  }
92
91
 
93
- export function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
92
+ function reachableSourceFiles(entries, sourceFiles, sourceIndex) {
94
93
  const reachable = new Set()
95
94
  const ordinary = new Set()
96
95
  const workers = new Set()
@@ -284,7 +283,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
284
283
  const importedSource = target => {
285
284
  let result = importedSourceCache.get(target)
286
285
  if (!result) {
287
- result = normalizeCompilerSource(parseSourceFile(target, sourceIndex.get(target)), { base, context, file: target, sourceFiles, sourceIndex })
286
+ result = normalizeCompilerSource(modules.clone(target, context.factory, context), { base, context, file: target, sourceFiles, sourceIndex })
288
287
  importedSourceCache.set(target, result)
289
288
  }
290
289
  return result.sourceFile
@@ -2725,33 +2724,19 @@ function resolveComponentExport(file, exportName, getSource, sourceFiles, trail
2725
2724
  if (trail.includes(key)) throw new Error(`Imported keyed list component re-export cycle: ${[...trail, key].map(entry => relative(root, entry.slice(0, entry.lastIndexOf(":")))).join(" -> ")}`)
2726
2725
  const sourceFile = getSource(file)
2727
2726
  const nextTrail = [...trail, key]
2728
-
2729
- for (const statement of sourceFile.statements) {
2730
- if (ts.isFunctionDeclaration(statement)) {
2731
- const isDefault = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)
2732
- const isExported = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
2733
- if (exportName === "default" && isDefault || exportName !== "default" && isExported && statement.name?.text === exportName) return statement
2734
- }
2735
- if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) && exportName !== "default") {
2736
- const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === exportName)
2737
- if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
2738
- }
2739
- if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
2740
- const component = localComponentDeclaration(sourceFile, statement.expression.text)
2741
- if (component) return component
2742
- }
2743
- if (ts.isExportDeclaration(statement) && ts.isNamedExports(statement.exportClause)) {
2744
- const entry = statement.exportClause.elements.find(element => !element.isTypeOnly && element.name.text === exportName)
2745
- if (!entry) continue
2746
- const imported = (entry.propertyName ?? entry.name).text
2747
- if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
2748
- if (!statement.moduleSpecifier.text.startsWith(".")) throw sourceNodeError(statement, sourceFile, "Imported keyed list components must use relative TypeScript re-exports")
2749
- const target = resolveSourceImport(file, statement.moduleSpecifier.text, sourceFiles)
2750
- return resolveComponentExport(target, imported, getSource, sourceFiles, nextTrail)
2751
- }
2752
- const component = localComponentDeclaration(sourceFile, imported)
2753
- if (component) return component
2754
- }
2727
+ const entry = modules.read(file).exports.get(exportName)
2728
+ if (entry?.kind === "reexport") {
2729
+ if (!entry.specifier.startsWith(".")) throw sourceNodeError(entry.node, modules.read(file).sourceFile, "Imported keyed list components must use relative TypeScript re-exports")
2730
+ const target = resolveSourceImport(file, entry.specifier, sourceFiles)
2731
+ return resolveComponentExport(target, entry.imported, getSource, sourceFiles, nextTrail)
2732
+ }
2733
+ if (entry?.kind === "function" && !entry.local) {
2734
+ const component = sourceFile.statements.find(statement => ts.isFunctionDeclaration(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword))
2735
+ if (component) return component
2736
+ }
2737
+ if (entry?.local) {
2738
+ const component = localComponentDeclaration(sourceFile, entry.local)
2739
+ if (component) return component
2755
2740
  }
2756
2741
  throw new Error(`${relative(root, file)} does not export a statically analyzable keyed list component named ${JSON.stringify(exportName)}`)
2757
2742
  }
@@ -2767,14 +2752,14 @@ function localComponentDeclaration(sourceFile, name) {
2767
2752
  return undefined
2768
2753
  }
2769
2754
 
2770
- export async function collectClientModules(entries, sourceFiles) {
2755
+ async function collectClientModules(entries, sourceFiles) {
2771
2756
  const modules = new Set()
2772
2757
  const queue = [...new Set(entries)]
2773
2758
  while (queue.length) {
2774
2759
  const file = queue.shift()
2775
2760
  if (modules.has(file)) continue
2776
- const source = await readFile(file, "utf8")
2777
- const sourceFile = parseSourceFile(file, source)
2761
+ const source = project.sourceIndex.get(file) ?? await readFile(file, "utf8")
2762
+ const sourceFile = project.modules.read(file, source).sourceFile
2778
2763
  workerCompiler.rejectConstructions(sourceFile, sourceFile, "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback, not imported client helpers")
2779
2764
  if (containsJsx(sourceFile)) throw new Error(`${relative(root, file)} Imported client helpers must not contain JSX`)
2780
2765
  rejectUnsupportedClientImports(sourceFile, file)
@@ -2795,7 +2780,7 @@ export async function collectClientModules(entries, sourceFiles) {
2795
2780
  return [...modules].sort()
2796
2781
  }
2797
2782
 
2798
- export async function compileClientModule(file, sourceFiles, staticFiles, cssModules, base) {
2783
+ async function compileClientModule(file, sourceFiles, staticFiles, cssModules, base) {
2799
2784
  const importedAssets = new Set()
2800
2785
  const source = await readFile(file, "utf8")
2801
2786
  const transformer = context => sourceFile => {
@@ -2840,7 +2825,7 @@ function resolveStaticImport(importer, specifier, staticFiles) {
2840
2825
  return target
2841
2826
  }
2842
2827
 
2843
- export async function safeStaticFiles(files) {
2828
+ async function safeStaticFiles(files) {
2844
2829
  const sourceRoot = await realpath(sourceDirectory)
2845
2830
  const entries = await Promise.all(files.map(async file => {
2846
2831
  try {
@@ -2855,7 +2840,7 @@ export async function safeStaticFiles(files) {
2855
2840
  return new Set(entries.filter(Boolean))
2856
2841
  }
2857
2842
 
2858
- export function orderSourceStyles(cssFiles, sourceFiles, sourceIndex, staticFiles) {
2843
+ function orderSourceStyles(cssFiles, sourceFiles, sourceIndex, staticFiles) {
2859
2844
  const ordered = []
2860
2845
  const seenStyles = new Set()
2861
2846
  const seenSources = new Set()
@@ -2940,7 +2925,7 @@ function rejectUnsupportedClientImports(sourceFile, file) {
2940
2925
  visit(sourceFile)
2941
2926
  }
2942
2927
 
2943
- export function layoutExportError(file, source) {
2928
+ function layoutExportError(file, source) {
2944
2929
  const sourceFile = parseSourceFile(file, source)
2945
2930
  for (const statement of sourceFile.statements) {
2946
2931
  if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
@@ -2956,20 +2941,32 @@ export function layoutExportError(file, source) {
2956
2941
  return new Error(`${relative(root, file)} layout export must be a function`)
2957
2942
  }
2958
2943
 
2959
- export function clientModulePath(file) {
2944
+ function clientModulePath(file) {
2960
2945
  return `modules/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
2961
2946
  }
2962
2947
 
2963
- export function compiledPath(file) {
2948
+ function compiledPath(file) {
2964
2949
  return join(workDirectory, relative(sourceDirectory, file)).replace(/\.(?:ts|tsx)$/, ".mjs")
2965
2950
  }
2966
2951
 
2967
2952
  const compileEventCommand = createCommandSpecializer({ isPrimitiveLiteral: isPrimitiveDefaultLiteral })
2968
2953
  const { analyzeZustandStores, normalizeZustandMigrationSyntax } = createZustandPass({ isSerializableStateLiteral, nativeCaptureNames, sourceDirectory })
2969
- const workerCompiler = createWorkerCompiler({ root, sourceDirectory, assetPath, parseSourceFile, resolveSourceImport, runtimeModuleReference })
2970
2954
  const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
2971
2955
  const printHandlerModule = createHandlerCodegen({
2972
2956
  resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
2973
2957
  })
2974
2958
  const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
2975
2959
  const normalizeReactRouterSyntax = createRouterPass({ withBase })
2960
+
2961
+ return { collectClientModules, compileClientModule, compiledPath, compileSource, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles }
2962
+ }
2963
+
2964
+ const currentCompiler = sourceIndex => createSourceCompiler(createProjectSession(process.cwd(), { sourceIndex }))
2965
+ export const collectClientModules = (...arguments_) => currentCompiler().collectClientModules(...arguments_)
2966
+ export const compileClientModule = (...arguments_) => currentCompiler().compileClientModule(...arguments_)
2967
+ export const compiledPath = (...arguments_) => currentCompiler().compiledPath(...arguments_)
2968
+ export const compileSource = (...arguments_) => currentCompiler(arguments_[2]).compileSource(...arguments_)
2969
+ export const layoutExportError = (...arguments_) => currentCompiler(new Map([[arguments_[0], arguments_[1]]])).layoutExportError(...arguments_)
2970
+ export const orderSourceStyles = (...arguments_) => currentCompiler(arguments_[2]).orderSourceStyles(...arguments_)
2971
+ export const reachableSourceFiles = (...arguments_) => currentCompiler(arguments_[2]).reachableSourceFiles(...arguments_)
2972
+ export const safeStaticFiles = (...arguments_) => currentCompiler().safeStaticFiles(...arguments_)
@@ -2,18 +2,46 @@ import { dirname, extname, join, relative, resolve } from "node:path"
2
2
  import ts from "typescript"
3
3
  import { sourceNodeError } from "./ast-helpers.mjs"
4
4
 
5
- const root = process.cwd()
5
+ export function createSourceGraph(root) {
6
+ const resolveSourceImport = (importer, specifier, sourceFiles) => {
7
+ const base = resolve(dirname(importer), specifier)
8
+ const extension = extname(base)
9
+ const stem = /\.(?:js|jsx|ts|tsx)$/.test(extension) ? base.slice(0, -extension.length) : base
10
+ const candidates = extension === ".ts" || extension === ".tsx"
11
+ ? [base]
12
+ : [`${stem}.ts`, `${stem}.tsx`, join(stem, "index.ts"), join(stem, "index.tsx")]
13
+ const matches = candidates.filter(candidate => sourceFiles.has(candidate))
14
+ if (matches.length !== 1) throw new Error(`${relative(root, importer)} Relative import ${JSON.stringify(specifier)} must resolve to one TypeScript file in src/`)
15
+ return matches[0]
16
+ }
17
+
18
+ const ordinaryRuntimeDependencies = (file, sourceFile, sourceFiles, isStaticImport) => {
19
+ const dependencies = []
20
+ const rejectDynamicImports = node => {
21
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
22
+ const argument = node.arguments[0]
23
+ const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(argument) ? JSON.stringify(argument.text) : argument?.getText(sourceFile) ?? "<missing>"
24
+ throw sourceNodeError(node, sourceFile, `Dynamic import ${specifier} is not supported in ordinary source modules`)
25
+ }
26
+ ts.forEachChild(node, rejectDynamicImports)
27
+ }
28
+ rejectDynamicImports(sourceFile)
29
+ for (const node of sourceFile.statements) {
30
+ if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !runtimeModuleReference(node) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier)) continue
31
+ const specifier = node.moduleSpecifier
32
+ if (!specifier.text.startsWith(".") || isStaticImport(specifier.text)) continue
33
+ try {
34
+ dependencies.push(resolveSourceImport(file, specifier.text, sourceFiles))
35
+ } catch (error) {
36
+ const detail = error.message.slice(error.message.indexOf("Relative import"))
37
+ const edge = ts.isExportDeclaration(node) ? "re-export" : "import"
38
+ throw sourceNodeError(specifier, sourceFile, detail.replace("Relative import", `Relative runtime ${edge}`))
39
+ }
40
+ }
41
+ return dependencies
42
+ }
6
43
 
7
- export function resolveSourceImport(importer, specifier, sourceFiles) {
8
- const base = resolve(dirname(importer), specifier)
9
- const extension = extname(base)
10
- const stem = /\.(?:js|jsx|ts|tsx)$/.test(extension) ? base.slice(0, -extension.length) : base
11
- const candidates = extension === ".ts" || extension === ".tsx"
12
- ? [base]
13
- : [`${stem}.ts`, `${stem}.tsx`, join(stem, "index.ts"), join(stem, "index.tsx")]
14
- const matches = candidates.filter(candidate => sourceFiles.has(candidate))
15
- if (matches.length !== 1) throw new Error(`${relative(root, importer)} Relative import ${JSON.stringify(specifier)} must resolve to one TypeScript file in src/`)
16
- return matches[0]
44
+ return { ordinaryRuntimeDependencies, parseSourceFile, resolveSourceImport, runtimeModuleReference }
17
45
  }
18
46
 
19
47
  export function runtimeModuleReference(node) {
@@ -25,32 +53,6 @@ export function runtimeModuleReference(node) {
25
53
  return clause.namedBindings?.elements.some(entry => !entry.isTypeOnly) ?? false
26
54
  }
27
55
 
28
- export function ordinaryRuntimeDependencies(file, sourceFile, sourceFiles, isStaticImport) {
29
- const dependencies = []
30
- const rejectDynamicImports = node => {
31
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
32
- const argument = node.arguments[0]
33
- const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(argument) ? JSON.stringify(argument.text) : argument?.getText(sourceFile) ?? "<missing>"
34
- throw sourceNodeError(node, sourceFile, `Dynamic import ${specifier} is not supported in ordinary source modules`)
35
- }
36
- ts.forEachChild(node, rejectDynamicImports)
37
- }
38
- rejectDynamicImports(sourceFile)
39
- for (const node of sourceFile.statements) {
40
- if ((!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) || !runtimeModuleReference(node) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier)) continue
41
- const specifier = node.moduleSpecifier
42
- if (!specifier.text.startsWith(".") || isStaticImport(specifier.text)) continue
43
- try {
44
- dependencies.push(resolveSourceImport(file, specifier.text, sourceFiles))
45
- } catch (error) {
46
- const detail = error.message.slice(error.message.indexOf("Relative import"))
47
- const edge = ts.isExportDeclaration(node) ? "re-export" : "import"
48
- throw sourceNodeError(specifier, sourceFile, detail.replace("Relative import", `Relative runtime ${edge}`))
49
- }
50
- }
51
- return dependencies
52
- }
53
-
54
56
  export function parseSourceFile(file, source) {
55
57
  return ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
56
58
  }
@@ -1,17 +1,15 @@
1
1
  import { createHash } from "node:crypto"
2
- import { mkdir, readFile } from "node:fs/promises"
2
+ import { mkdir } from "node:fs/promises"
3
3
  import { dirname, relative, resolve, sep } from "node:path"
4
4
  import { build as bundle } from "esbuild"
5
5
  import ts from "typescript"
6
6
  import { containsJsx, isUnshadowedGlobal, nearestFunction, sourceNodeError } from "./ast-helpers.mjs"
7
- import { assetPath } from "./path-helpers.mjs"
8
- import { parseSourceFile, resolveSourceImport, runtimeModuleReference } from "./source-graph.mjs"
9
7
 
10
8
  export function createWorkerCompiler({
11
9
  root,
12
10
  sourceDirectory,
13
11
  assetPath,
14
- parseSourceFile,
12
+ readSourceModule,
15
13
  resolveSourceImport,
16
14
  runtimeModuleReference
17
15
  }) {
@@ -108,7 +106,7 @@ export function createWorkerCompiler({
108
106
  const file = queue.shift()
109
107
  if (visited.has(file)) continue
110
108
  visited.add(file)
111
- const sourceFile = parseSourceFile(file, await readFile(file, "utf8"))
109
+ const sourceFile = readSourceModule(file).sourceFile
112
110
  if (containsJsx(sourceFile)) throw sourceNodeError(sourceFile, sourceFile, "Worker modules must not contain JSX")
113
111
  const visit = node => {
114
112
  if (ts.isImportEqualsDeclaration(node)) throw sourceNodeError(node, sourceFile, "TypeScript import-equals declarations are not supported in Worker modules; use a relative ESM import")
@@ -168,9 +166,3 @@ export function createWorkerCompiler({
168
166
 
169
167
  return { candidate, emit, rejectConstructions, rejectOrdinaryImports, rewriteEffect }
170
168
  }
171
-
172
- export function emitWorkers(references, sourceFiles, assetsDirectory, base, minify) {
173
- const root = process.cwd()
174
- const sourceDirectory = resolve(root, "src")
175
- return createWorkerCompiler({ root, sourceDirectory, assetPath, parseSourceFile, resolveSourceImport, runtimeModuleReference }).emit(references, sourceFiles, assetsDirectory, base, minify)
176
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.32",
3
+ "version": "0.8.34",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,6 +58,7 @@
58
58
  "benchmark": "node test/performance.mjs",
59
59
  "benchmark:keyed": "node test/keyed-performance.mjs",
60
60
  "benchmark:commerce": "node test/commerce-build-performance.mjs",
61
+ "benchmark:module-cache": "node test/module-cache-performance.mjs",
61
62
  "benchmark:native": "node test/native-performance.mjs",
62
63
  "prepublishOnly": "npm run check && npm test",
63
64
  "deploy": "wrangler deploy",