@kudzujs/core 0.16.5 → 0.16.6
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 +1 -1
- package/PERFORMANCE.md +15 -0
- package/README.md +1 -1
- package/RELEASES.md +40 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/application-capability-release-plan.md +74 -1
- package/docs/next-architecture/compiler-current-architecture.md +1 -1
- package/docs/next-architecture/large-application-ai-native-roadmap.md +3 -3
- package/docs/next-architecture/versioning.md +2 -2
- package/framework/compiler/descriptor-session.mjs +12 -0
- package/framework/compiler/ir/module-ir.mjs +13 -2
- package/framework/compiler/route-artifact-report.mjs +17 -8
- package/framework/compiler/source-compiler.mjs +21 -1
- package/framework/compiler/source-graph.mjs +13 -0
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -51,7 +51,7 @@ Syntax compatibility does not mean reproducing React wholesale. Kudzu accepts th
|
|
|
51
51
|
- stronger JavaScript failure resilience;
|
|
52
52
|
- no remaining material large-route build-scaling weakness.
|
|
53
53
|
|
|
54
|
-
The completed 0.9 plan defines the cross-framework comparison and release gate. The active application packet is `0.18.
|
|
54
|
+
The completed 0.9 plan defines the cross-framework comparison and release gate. The active application packet is `0.18.2` in the capability release plan below. Benchmark-only feature omission, unmatched accessibility, weighted scores that hide losses, and unrecorded environment differences do not count as proof.
|
|
55
55
|
|
|
56
56
|
### 0.10.0 Through 0.21.x: Application Capability Release Train
|
|
57
57
|
|
package/PERFORMANCE.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, `npm run benchmark:module-cache`, `npm run benchmark:project-navigation`, `npm run benchmark:project-state`, and `npm run benchmark:source-scale` 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
4
|
|
|
5
|
+
## 0.16.6 Owner-Triggered Capability Imports
|
|
6
|
+
|
|
7
|
+
Measured 2026-08-29 on macOS arm64 with Node 24.14.0 and Chrome. The existing
|
|
8
|
+
eager CodeMirror route owns 214,968 raw / 71,607 aggregate gzip B. Its guarded
|
|
9
|
+
lazy counterpart initially owns 12,982 raw / 6,153 gzip B and defers 250,086
|
|
10
|
+
raw / 80,890 gzip B, reducing initial gzip by 65,454 B (91.4%). Its static
|
|
11
|
+
sibling remains complete HTML with zero JavaScript.
|
|
12
|
+
|
|
13
|
+
Required Chrome proves zero initial deferred requests, one request on owner
|
|
14
|
+
activation, mount, exact cleanup, cached remount without another request, and
|
|
15
|
+
document disposal. The maintained Worker benchmark remains 907 raw / 477 gzip B
|
|
16
|
+
for the Worker graph and 14,456 raw / 6,160 gzip B for the window graph. Seven
|
|
17
|
+
clean builds record a 222.8 ms median. No browser runtime file or concept was
|
|
18
|
+
added.
|
|
19
|
+
|
|
5
20
|
## 0.16.5 Bounded Navigation Cache
|
|
6
21
|
|
|
7
22
|
Measured 2026-08-28 on Linux x64 with Node 24.14.0 and Chrome. Before the
|
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.16.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.16.
|
|
17
|
+
**Latest release: 0.16.6 - Owner-triggered capability imports.** One literal package dependency may stay out of the initial route graph until its guarded effect owner activates, while static siblings remain JavaScript-free. Read the [release notes](./RELEASES.md#0166---owner-triggered-capability-imports), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.16.6), 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,45 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.16.6 - Owner-Triggered Capability Imports
|
|
4
|
+
|
|
5
|
+
Kudzu 0.16.6 lets one guarded effect load a literal package dependency only
|
|
6
|
+
when its owner activates, using native ESM code splitting without a loader or
|
|
7
|
+
component runtime.
|
|
8
|
+
|
|
9
|
+
### Changed in 0.16.6
|
|
10
|
+
|
|
11
|
+
- Accepts one literal bare-package `import()` directly inside an inline
|
|
12
|
+
synchronous `useEffect` with a direct dependency-state early-return guard.
|
|
13
|
+
- Keeps relative, URL, render-time, event-handler, indirect, async-effect, and
|
|
14
|
+
multiple dynamic imports unsupported with source diagnostics.
|
|
15
|
+
- Removes the package edge from build-time component execution and preserves it
|
|
16
|
+
only in the route-owned effect bundle.
|
|
17
|
+
- Reports deferred handler closure separately as `handlers.lazyChunks` while
|
|
18
|
+
retaining exact route ownership and shared-chunk accounting.
|
|
19
|
+
- Uses native module-map deduplication across owner remounts and exact effect
|
|
20
|
+
cleanup on owner and document release.
|
|
21
|
+
- Adds no loader, retry cache, component runtime, React, VDOM, hydration, or
|
|
22
|
+
retained browser component tree.
|
|
23
|
+
- Updates `create-kudzu@0.1.131` to generate projects on
|
|
24
|
+
`@kudzujs/core@^0.16.6`.
|
|
25
|
+
|
|
26
|
+
### Output And Browser Evidence
|
|
27
|
+
|
|
28
|
+
- The eager CodeMirror route owns 214,968 raw / 71,607 gzip bytes. The lazy
|
|
29
|
+
route initially owns 12,982 raw / 6,153 gzip bytes and defers 250,086 raw /
|
|
30
|
+
80,890 gzip bytes, reducing initial gzip by 65,454 bytes (91.4%).
|
|
31
|
+
- Required Chrome proves zero initial chunk requests, one activation request,
|
|
32
|
+
package mount, exact owner cleanup, cached remount without another request,
|
|
33
|
+
and document cleanup.
|
|
34
|
+
- The static sibling emits zero JavaScript. The maintained Worker and window
|
|
35
|
+
graphs remain 907 raw / 477 gzip bytes and 14,456 raw / 6,160 gzip bytes.
|
|
36
|
+
|
|
37
|
+
### Upgrade
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install @kudzujs/core@^0.16.6
|
|
41
|
+
```
|
|
42
|
+
|
|
3
43
|
## 0.16.5 - Bounded Navigation Cache
|
|
4
44
|
|
|
5
45
|
Kudzu 0.16.5 prevents superseded enhanced navigations from resurrecting pruned
|
|
@@ -15,7 +15,7 @@ The completed `0.9.0` milestone is recorded in [`0.9-semantic-compression.md`](.
|
|
|
15
15
|
|
|
16
16
|
[`1.0-large-application-compatibility-audit.md`](./1.0-large-application-compatibility-audit.md) records the first post-0.9 probes against Memos, Apache Answer, and Actual Budget. The audit finds that reduced slices build but whole-application source retention and behavior parity do not yet pass.
|
|
17
17
|
|
|
18
|
-
[`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue, currently at `0.18.
|
|
18
|
+
[`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue, currently at `0.18.2`. It assigns one application-capability section to each minor release and one independently accepted evidence packet to each patch release from `0.10.0` through the `1.0.0` gate. It supersedes the provisional 0.10/0.11/0.12 tool-first ordering in the completed 0.9 handoff without rewriting that historical record.
|
|
19
19
|
|
|
20
20
|
## Required Invariants
|
|
21
21
|
|
|
@@ -1333,6 +1333,16 @@ runtime files, and public APIs change by zero; one test fixture and one browser
|
|
|
1333
1333
|
harness were added. This evidence changed no production source by itself and was
|
|
1334
1334
|
published with the later stale-prefetch fix in the actual `0.16.5` patch.
|
|
1335
1335
|
|
|
1336
|
+
Release transaction complete at commit
|
|
1337
|
+
`f23db7403060df7615b684f3a4a39781e600ed23` and immutable tag `v0.16.5`.
|
|
1338
|
+
CI run `33235364419` passed Node 22, required-Chrome Node 24, and the Cloudflare
|
|
1339
|
+
deployment. The GitHub release is
|
|
1340
|
+
[`v0.16.5`](https://github.com/kudzujs/kudzu/releases/tag/v0.16.5), and publish
|
|
1341
|
+
workflow run `33235705347` published and verified `@kudzujs/core@0.16.5` and
|
|
1342
|
+
`create-kudzu@0.1.130`. A fresh registry-only install imported core, confirmed
|
|
1343
|
+
both exact package versions, generated a two-route application, and passed its
|
|
1344
|
+
TypeScript and production build check.
|
|
1345
|
+
|
|
1336
1346
|
## `0.17.x`: Long-Lived Applications
|
|
1337
1347
|
|
|
1338
1348
|
### `0.17.0`: WebSocket Application Journey
|
|
@@ -1437,6 +1447,37 @@ retain the public `0.16.x` version line.
|
|
|
1437
1447
|
measured.
|
|
1438
1448
|
- **Done condition:** every eager dependency has a structural reason.
|
|
1439
1449
|
|
|
1450
|
+
**Result:** closed by the existing RouteBuildRecord and artifact report. A
|
|
1451
|
+
test-only collector builds the greenfield project, real CodeMirror editor, four
|
|
1452
|
+
separate Apache Answer reductions, and Memos outline reduction: seven fixtures
|
|
1453
|
+
and 25 routes total. It joins each route's source page to HTML, runtime entries
|
|
1454
|
+
and requirements, handler entries/chunks, Worker entries/chunks, and styles,
|
|
1455
|
+
then records exact raw/gzip bytes and SHA-256 values. Every generated JavaScript
|
|
1456
|
+
file has at least one route owner, every direct module script has one matching
|
|
1457
|
+
`modulepreload`, and every static control owns zero JavaScript and zero preload.
|
|
1458
|
+
No measured fixture emits a shared chunk.
|
|
1459
|
+
|
|
1460
|
+
The greenfield deploy is 45 files and 196,678 raw / 60,880 aggregate gzip
|
|
1461
|
+
bytes. Its project routes own 38,652-69,178 raw JavaScript bytes; `/login` owns
|
|
1462
|
+
12,093 raw bytes and `/help` owns zero. The editor route owns 214,968 raw /
|
|
1463
|
+
71,607 gzip bytes, including its 200,260 raw / 64,949 gzip CodeMirror handler;
|
|
1464
|
+
its static sibling owns zero. The Answer question reduction owns 32,022 raw /
|
|
1465
|
+
12,408 gzip bytes. Its route-shell static pages own zero while the two runtime
|
|
1466
|
+
parameter pages own 2,134 and 2,078 raw bytes. Answer auth routes own 25,684 and
|
|
1467
|
+
26,818 raw bytes with a zero-byte public sibling; authoring/admin routes own
|
|
1468
|
+
12,251 and 20,919 raw bytes with a zero-byte legal sibling. The Memos outline
|
|
1469
|
+
owns 14,701 raw / 7,044 gzip bytes and its static sibling owns zero.
|
|
1470
|
+
|
|
1471
|
+
Current navigation prefetch remains structurally bounded: eligible same-origin
|
|
1472
|
+
group links prefetch on hover, focus, viewport proximity, or idle fallback;
|
|
1473
|
+
promises deduplicate, failures leave the cache, current/eligible URLs define the
|
|
1474
|
+
retained set, and the `0.16.5` revision check rejects stale navigation results
|
|
1475
|
+
before cache insertion. The report intentionally records emitted closure rather
|
|
1476
|
+
than esbuild input-byte attribution. Answer and Memos remain disconnected
|
|
1477
|
+
reductions, not whole-application claims. Semantic primitives, IR kinds,
|
|
1478
|
+
compiler passes, production compiler/runtime LOC, runtime concepts/files, and
|
|
1479
|
+
public APIs change by zero. No release is consumed.
|
|
1480
|
+
|
|
1440
1481
|
### `0.18.1`: Bounded Lazy Capability Import
|
|
1441
1482
|
|
|
1442
1483
|
- **Purpose:** permit one statically analyzable feature dependency to load only
|
|
@@ -1450,6 +1491,36 @@ retain the public `0.16.x` version line.
|
|
|
1450
1491
|
- **Done condition:** the feature loader is capability-specific, not a component
|
|
1451
1492
|
runtime.
|
|
1452
1493
|
|
|
1494
|
+
**Completion evidence:** one direct literal bare-package `import()` may remain
|
|
1495
|
+
inside an inline synchronous effect when its first statement is the exact
|
|
1496
|
+
dependency-state guard `if (!state) return`. The source graph, descriptor
|
|
1497
|
+
session, ModuleIR validation, build-module erasure, esbuild split graph, and
|
|
1498
|
+
route artifact report preserve that edge without adding a loader. Relative,
|
|
1499
|
+
URL, render-time, event, indirect, async-effect, unguarded, and multiple dynamic
|
|
1500
|
+
imports remain rejected. Failed native ESM evaluation remains visible through
|
|
1501
|
+
application-owned error state; same-document retry of the failed URL is not
|
|
1502
|
+
claimed because the browser module map cannot be reset without adding the
|
|
1503
|
+
loader forbidden by this packet.
|
|
1504
|
+
|
|
1505
|
+
The CodeMirror-derived fixture initially requests no deferred chunk. Activation
|
|
1506
|
+
requests one chunk, mounts the editor, and owner release destroys it. Remount
|
|
1507
|
+
uses the browser module map without another request, and document disposal
|
|
1508
|
+
balances the second mount. The initial eager graph is 12,982 raw / 6,153 gzip
|
|
1509
|
+
bytes versus 214,968 raw / 71,607 gzip bytes for the existing eager editor;
|
|
1510
|
+
250,086 raw / 80,890 gzip bytes move to the deferred graph. Initial gzip falls
|
|
1511
|
+
by 65,454 bytes (91.4%), while the static sibling remains 0 B JavaScript.
|
|
1512
|
+
|
|
1513
|
+
The packet adds no semantic primitive, compiler pass, public API, browser
|
|
1514
|
+
runtime concept/file, React, VDOM, hydration, retained component tree, or retry
|
|
1515
|
+
cache. It adds one validated `ImportIR` kind and additive
|
|
1516
|
+
`handlers.lazyChunks` reporting. Production compiler sources change by 76 added
|
|
1517
|
+
and 11 removed lines. The artifact collector now covers eight fixtures and 27
|
|
1518
|
+
routes with no ownerless JavaScript or preload mismatch. `npm run check`, 284
|
|
1519
|
+
required-Chrome tests, package smoke, and the maintained benchmark pass. The
|
|
1520
|
+
Worker graph remains 907 raw / 477 gzip bytes; the window graph remains 14,456
|
|
1521
|
+
raw / 6,160 gzip bytes, with a seven-build median of 222.8 ms. This packet ships
|
|
1522
|
+
as the actual `0.16.6` patch to retain the public `0.16.x` version line.
|
|
1523
|
+
|
|
1453
1524
|
### `0.18.2`: Lazy Editor Journey
|
|
1454
1525
|
|
|
1455
1526
|
- **Purpose:** apply `0.18.1` to the real editor.
|
|
@@ -1764,4 +1835,6 @@ release transaction where possible or document and publish a forward-fix patch.
|
|
|
1764
1835
|
| `0.17.0` | Closed by existing layout effect ownership | Preserve project notification connect, keyed update, reconnect, stale rejection, offline recovery, retained navigation, and exact cleanup evidence. | No production change; no release consumed |
|
|
1765
1836
|
| `0.17.1` | Closed by stop condition | Preserve independent effect ownership until three applications prove the complete shared transport and bounded replay contract. | Zero qualifying applications; no release consumed |
|
|
1766
1837
|
| `0.17.2` | Released as `0.16.5` | Preserve stale-prefetch rejection and bounded document, state, resource, listener, DOM, and external-instance ownership. | Patch release retained the `0.16.x` public version line |
|
|
1767
|
-
| `0.18.0` |
|
|
1838
|
+
| `0.18.0` | Closed by existing artifact reporting | Preserve exact route/source ownership, bytes, hashes, preload equality, zero-JavaScript controls, and bounded prefetch policy. | No production change; no release consumed |
|
|
1839
|
+
| `0.18.1` | Released as `0.16.6` | Preserve one guarded literal package edge, native module-map deduplication, exact owner cleanup, deferred artifact ownership, and static exclusion. | Patch release retained the `0.16.x` public version line |
|
|
1840
|
+
| `0.18.2` | Active | Apply the bounded edge to the retained editor mount/update/dispose journey without broadening dynamic imports. | None |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the current `0.16.
|
|
3
|
+
This maps the current `0.16.6` architecture, built on the completed `0.9.0` semantic-compression release and `0.8.23` Goal A compiler foundation. The active application packet is `0.18.2`; file and function names are the stable references, while line numbers are intentionally omitted because later work may still move code.
|
|
4
4
|
|
|
5
5
|
## Responsibility Map
|
|
6
6
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
Completed compiler-foundation record and longer-term plan after `0.8.35`. The [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) execution queue is complete. The active queue for current work is [`application-capability-release-plan.md`](./application-capability-release-plan.md) at packet `0.18.
|
|
5
|
+
Completed compiler-foundation record and longer-term plan after `0.8.35`. The [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) execution queue is complete. The active queue for current work is [`application-capability-release-plan.md`](./application-capability-release-plan.md) at packet `0.18.2`. This document does not mark any remaining planned capability as supported and does not authorize a React runtime, VDOM, hydration, retained browser component tree, generic rerenderer, public store/query/resource API, SPA router, or islands.
|
|
6
6
|
|
|
7
|
-
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains the product authority for product invariants and fixture-driven feature selection. [`application-capability-release-plan.md`](./application-capability-release-plan.md) is authoritative for current work order and evidence at packet `0.18.
|
|
7
|
+
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains the product authority for product invariants and fixture-driven feature selection. [`application-capability-release-plan.md`](./application-capability-release-plan.md) is authoritative for current work order and evidence at packet `0.18.2`; this plan retains the completed foundation, deferred program, and long-term production gates. If implementation evidence changes either boundary, update the relevant document before broadening a patch.
|
|
8
8
|
|
|
9
9
|
## Product Outcome
|
|
10
10
|
|
|
@@ -483,4 +483,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
483
483
|
|
|
484
484
|
## Immediate Decision
|
|
485
485
|
|
|
486
|
-
All listed foundation and `0.9` slices are complete. Current work continues at `0.18.
|
|
486
|
+
All listed foundation and `0.9` slices are complete. Current work continues at `0.18.2` Lazy Editor Journey under the [`application-capability-release-plan.md`](./application-capability-release-plan.md); keep ResourceIR limited to qualifying independent fixtures, and do not add range ownership, virtualization, optimistic transactions, a public adapter/store API, or a router before evidence justifies them.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Planned Version Sequence
|
|
2
2
|
|
|
3
|
-
This is an execution sequence, not release history. `0.8.16` through `0.8.62` are completed scopes represented by package/release records. The `0.10.0` through `1.0.0` minor/patch sequence is maintained in [`application-capability-release-plan.md`](./application-capability-release-plan.md), currently at `0.18.
|
|
3
|
+
This is an execution sequence, not release history. `0.8.16` through `0.8.62` are completed scopes represented by package/release records. The `0.10.0` through `1.0.0` minor/patch sequence is maintained in [`application-capability-release-plan.md`](./application-capability-release-plan.md), currently at `0.18.2`; that plan supersedes the provisional tool-first 0.10/0.11/0.12 ordering in the completed 0.9 handoff.
|
|
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
|
|
|
@@ -66,7 +66,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
66
66
|
|
|
67
67
|
## Generator Versions
|
|
68
68
|
|
|
69
|
-
`create-kudzu@0.1.
|
|
69
|
+
`create-kudzu@0.1.131` retains the explicit install instructions and generates projects with `@kudzujs/core@^0.16.6`.
|
|
70
70
|
|
|
71
71
|
## Release Boundary
|
|
72
72
|
|
|
@@ -4,6 +4,7 @@ import { knownGlobalNames } from "./analysis/binding-index.mjs"
|
|
|
4
4
|
import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
|
|
5
5
|
import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
|
|
6
6
|
import { assertModuleIRReferences, createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler, registerSignal } from "./ir/module-ir.mjs"
|
|
7
|
+
import { ownedLazyPackageImport } from "./source-graph.mjs"
|
|
7
8
|
|
|
8
9
|
export function createSemanticArtifact(file) {
|
|
9
10
|
return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file) }
|
|
@@ -168,6 +169,17 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
168
169
|
const usedReducers = referencedReducerDispatches(expression.body, reducers, expression, indexedBindingIndex)
|
|
169
170
|
const imports = [...referencedImportedBindings(expression, importBindings, indexedBindingIndex)].map(name => importBindings.get(name))
|
|
170
171
|
imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
|
|
172
|
+
const dynamicImports = []
|
|
173
|
+
const visitDynamicImports = node => {
|
|
174
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
175
|
+
if (role !== "effect" || !ownedLazyPackageImport(node)) throw new Error("Dynamic package imports require one literal import() directly inside an inline owned effect")
|
|
176
|
+
dynamicImports.push({ kind: "dynamic", local: node.arguments[0].text, target: node.arguments[0].text, package: true })
|
|
177
|
+
}
|
|
178
|
+
ts.forEachChild(node, visitDynamicImports)
|
|
179
|
+
}
|
|
180
|
+
visitDynamicImports(expression)
|
|
181
|
+
if (dynamicImports.length > 1) throw new Error("An owned effect may contain only one dynamic package import")
|
|
182
|
+
imports.push(...dynamicImports)
|
|
171
183
|
const captures = new Set([...allCaptures].filter(name => !importBindings.has(name) && !usedReducers.has(name)))
|
|
172
184
|
registerClientImports(imports)
|
|
173
185
|
const usedStates = referencedStateNames(expression.body, setters, expression, indexedBindingIndex)
|
|
@@ -24,6 +24,11 @@ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
|
|
|
24
24
|
indexed("EffectIR", moduleIR.effects)
|
|
25
25
|
indexed("KeyedBlockIR", moduleIR.keyedBlocks)
|
|
26
26
|
indexed("ImportIR", moduleIR.imports)
|
|
27
|
+
const dynamicImports = new Set()
|
|
28
|
+
for (const imported of moduleIR.imports) if (imported.kind === "dynamic") {
|
|
29
|
+
if (!imported.package || typeof imported.target !== "string" || !imported.target || imported.target.startsWith(".") || imported.target.startsWith("/") || /^[a-z][a-z\d+.-]*:/i.test(imported.target) || imported.local !== imported.target) throw new Error(`ImportIR ${imported.slot} has an invalid dynamic package target`)
|
|
30
|
+
dynamicImports.add(imported.slot)
|
|
31
|
+
}
|
|
27
32
|
for (const action of moduleIR.sharedActions) slot(moduleIR.sharedStates, action.state, `SharedActionIR ${action.slot}`, "SharedStateIR")
|
|
28
33
|
if (componentAnalysis) {
|
|
29
34
|
indexed("Component owner", componentAnalysis.owners)
|
|
@@ -82,7 +87,10 @@ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
|
|
|
82
87
|
exported(handler, `HandlerIR ${handler.slot}`)
|
|
83
88
|
for (const [index, command] of (handler.commands ?? []).entries()) slot(moduleIR.signals, command.signal, `HandlerIR ${handler.slot} command ${index}`, "SignalIR")
|
|
84
89
|
for (const [index, signal] of (handler.signals ?? []).entries()) slot(moduleIR.signals, signal.signal, `HandlerIR ${handler.slot} signal ${index}`, "SignalIR")
|
|
85
|
-
for (const [index, imported] of (handler.imports ?? []).entries())
|
|
90
|
+
for (const [index, imported] of (handler.imports ?? []).entries()) {
|
|
91
|
+
slot(moduleIR.imports, imported, `HandlerIR ${handler.slot} import ${index}`, "ImportIR")
|
|
92
|
+
if (dynamicImports.has(imported) && handler.role !== "effect") throw new Error(`HandlerIR ${handler.slot} dynamic import ${index} requires role "effect"`)
|
|
93
|
+
}
|
|
86
94
|
for (const [index, capture] of (handler.captures ?? []).entries()) if (capture.symbol !== undefined) slot(moduleIR.symbols, capture.symbol, `HandlerIR ${handler.slot} capture ${index}`, "SymbolRef")
|
|
87
95
|
for (const [index, action] of (handler.actions ?? []).entries()) slot(moduleIR.sharedActions, action, `HandlerIR ${handler.slot} action ${index}`, "SharedActionIR")
|
|
88
96
|
if (handler.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, handler.keyedBlock, `HandlerIR ${handler.slot} keyed block`, "KeyedBlockIR")
|
|
@@ -90,7 +98,10 @@ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
|
|
|
90
98
|
for (const binding of moduleIR.bindings) {
|
|
91
99
|
exported(binding, `BindingIR ${binding.slot}`)
|
|
92
100
|
for (const [index, signal] of (binding.signals ?? []).entries()) slot(moduleIR.signals, signal, `BindingIR ${binding.slot} signal ${index}`, "SignalIR")
|
|
93
|
-
for (const [index, imported] of (binding.imports ?? []).entries())
|
|
101
|
+
for (const [index, imported] of (binding.imports ?? []).entries()) {
|
|
102
|
+
slot(moduleIR.imports, imported, `BindingIR ${binding.slot} import ${index}`, "ImportIR")
|
|
103
|
+
if (dynamicImports.has(imported)) throw new Error(`BindingIR ${binding.slot} cannot reference a dynamic import`)
|
|
104
|
+
}
|
|
94
105
|
for (const [index, capture] of (binding.captures ?? []).entries()) if (capture.symbol !== undefined) slot(moduleIR.symbols, capture.symbol, `BindingIR ${binding.slot} capture ${index}`, "SymbolRef")
|
|
95
106
|
if (binding.derived) {
|
|
96
107
|
const derived = slot(moduleIR.derived, binding.derived.derived, `BindingIR ${binding.slot} derived value`, "DerivedIR")
|
|
@@ -39,7 +39,8 @@ export function createRouteArtifactReport(records, {
|
|
|
39
39
|
runtime: routeRuntimeEdges(record, capability, family, base, navigationAssets.get(record.route)),
|
|
40
40
|
handlers: {
|
|
41
41
|
entries: handlerEntries,
|
|
42
|
-
chunks: handlerOutputs.filter(output => !handlerEntries.includes(output))
|
|
42
|
+
chunks: handlerOutputs.eager.filter(output => !handlerEntries.includes(output)),
|
|
43
|
+
lazyChunks: handlerOutputs.lazy
|
|
43
44
|
},
|
|
44
45
|
workers,
|
|
45
46
|
styles: [...record.artifacts.styles].sort()
|
|
@@ -47,7 +48,7 @@ export function createRouteArtifactReport(records, {
|
|
|
47
48
|
}).sort((left, right) => left.route.localeCompare(right.route))
|
|
48
49
|
const owners = new Map()
|
|
49
50
|
for (const route of routes) {
|
|
50
|
-
for (const path of [...route.handlers.chunks, ...route.workers.flatMap(worker => worker.chunks)]) {
|
|
51
|
+
for (const path of [...route.handlers.chunks, ...route.handlers.lazyChunks, ...route.workers.flatMap(worker => worker.chunks)]) {
|
|
51
52
|
const paths = owners.get(path) ?? new Set()
|
|
52
53
|
paths.add(route.route)
|
|
53
54
|
owners.set(path, paths)
|
|
@@ -109,7 +110,7 @@ function outputGraph(metafile, outputDirectory, base) {
|
|
|
109
110
|
for (const [output, metadata] of Object.entries(metafile.outputs)) {
|
|
110
111
|
const absolute = resolve(output)
|
|
111
112
|
const url = paths.get(absolute)
|
|
112
|
-
const imports = (metadata.imports ?? []).filter(entry => !entry.external).map(entry => paths.get(resolve(entry.path)) ?? paths.get(resolve(dirname(absolute), entry.path))).filter(
|
|
113
|
+
const imports = (metadata.imports ?? []).filter(entry => !entry.external).map(entry => ({ path: paths.get(resolve(entry.path)) ?? paths.get(resolve(dirname(absolute), entry.path)), kind: entry.kind })).filter(entry => entry.path)
|
|
113
114
|
graph.set(url, [...new Set(imports)].sort())
|
|
114
115
|
}
|
|
115
116
|
return graph
|
|
@@ -123,12 +124,20 @@ function outputUrl(output, outputDirectory, base) {
|
|
|
123
124
|
|
|
124
125
|
function closure(entries, graph, requireEntries) {
|
|
125
126
|
const visited = new Set()
|
|
126
|
-
const
|
|
127
|
-
|
|
127
|
+
const eager = new Set()
|
|
128
|
+
const lazy = new Set()
|
|
129
|
+
const visit = (output, deferred = false) => {
|
|
130
|
+
const key = `${Number(deferred)}:${output}`
|
|
131
|
+
if (visited.has(key)) return
|
|
132
|
+
visited.add(key)
|
|
128
133
|
if (requireEntries && !graph.has(output)) throw new Error(`Bundled handler entry was not emitted: ${output}`)
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
if (deferred && !eager.has(output)) lazy.add(output)
|
|
135
|
+
else {
|
|
136
|
+
eager.add(output)
|
|
137
|
+
lazy.delete(output)
|
|
138
|
+
}
|
|
139
|
+
for (const imported of graph.get(output) ?? []) visit(imported.path, deferred || imported.kind === "dynamic-import")
|
|
131
140
|
}
|
|
132
141
|
for (const entry of entries) visit(entry)
|
|
133
|
-
return [...
|
|
142
|
+
return { eager: [...eager].sort(), lazy: [...lazy].sort() }
|
|
134
143
|
}
|
|
@@ -22,6 +22,7 @@ import { createReactMigrationPass, reactMemoExpression } from "./react-migration
|
|
|
22
22
|
import { normalizeRenderControlFlow } from "./render-control-pass.mjs"
|
|
23
23
|
import { createRouterPass } from "./router-pass.mjs"
|
|
24
24
|
import { createProjectSession } from "./project-session.mjs"
|
|
25
|
+
import { ownedLazyPackageImport } from "./source-graph.mjs"
|
|
25
26
|
import { createZustandPass } from "./zustand-pass.mjs"
|
|
26
27
|
|
|
27
28
|
export function createSourceCompiler(project) {
|
|
@@ -2029,6 +2030,19 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2029
2030
|
if (calculationEntries.length && (calculationEntries.length !== 1 || dependencyEntries.length !== 1)) effectFail(dependencies, "useEffect() selected calculation fields must be the only dependency")
|
|
2030
2031
|
if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
|
|
2031
2032
|
if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
|
|
2033
|
+
const lazyPackageImports = []
|
|
2034
|
+
const collectLazyPackageImports = current => {
|
|
2035
|
+
if (ownedLazyPackageImport(current)) lazyPackageImports.push(current)
|
|
2036
|
+
else ts.forEachChild(current, collectLazyPackageImports)
|
|
2037
|
+
}
|
|
2038
|
+
collectLazyPackageImports(callback)
|
|
2039
|
+
if (lazyPackageImports.length) {
|
|
2040
|
+
const guard = callback.body.statements[0]
|
|
2041
|
+
const condition = guard && ts.isIfStatement(guard) ? unwrapExpression(guard.expression) : undefined
|
|
2042
|
+
const guardedState = condition && ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken && ts.isIdentifier(unwrapExpression(condition.operand)) ? unwrapExpression(condition.operand).text : undefined
|
|
2043
|
+
const exits = guard && ts.isIfStatement(guard) && ts.isReturnStatement(guard.thenStatement) && !guard.thenStatement.expression
|
|
2044
|
+
if (lazyPackageImports.length !== 1 || !guardedState || !exits || !ordinaryDependencies.some(dependency => dependency.text === guardedState)) effectFail(lazyPackageImports[0], "Dynamic package import requires one direct dependency-state guard as the first effect statement")
|
|
2045
|
+
}
|
|
2032
2046
|
const cleanupSubstitutions = new Map()
|
|
2033
2047
|
const collectNamedCleanups = current => {
|
|
2034
2048
|
if (current !== callback && isFunctionLike(current)) return
|
|
@@ -2120,7 +2134,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2120
2134
|
factory.createSpreadAssignment(evaluator.descriptor),
|
|
2121
2135
|
factory.createPropertyAssignment("field", factory.createStringLiteral(dependencyEntries[index].field))
|
|
2122
2136
|
]) : factory.createNull()) : []
|
|
2123
|
-
|
|
2137
|
+
let hasLazyPackageImport = false
|
|
2138
|
+
const findLazyPackageImport = current => {
|
|
2139
|
+
if (ownedLazyPackageImport(current)) hasLazyPackageImport = true
|
|
2140
|
+
else ts.forEachChild(current, findLazyPackageImport)
|
|
2141
|
+
}
|
|
2142
|
+
findLazyPackageImport(callback)
|
|
2143
|
+
const buildCallback = hasLazyPackageImport || [...packageBindings].some(([name]) => referenceIdentifiers(callback, name).length)
|
|
2124
2144
|
? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([], false))
|
|
2125
2145
|
: callback
|
|
2126
2146
|
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
@@ -21,6 +21,7 @@ export function createSourceGraph(root) {
|
|
|
21
21
|
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
22
22
|
const argument = node.arguments[0]
|
|
23
23
|
const specifier = node.arguments.length === 1 && ts.isStringLiteralLike(argument) ? JSON.stringify(argument.text) : argument?.getText(sourceFile) ?? "<missing>"
|
|
24
|
+
if (ownedLazyPackageImport(node)) return
|
|
24
25
|
throw sourceNodeError(node, sourceFile, `Dynamic import ${specifier} is not supported in ordinary source modules`)
|
|
25
26
|
}
|
|
26
27
|
ts.forEachChild(node, rejectDynamicImports)
|
|
@@ -44,6 +45,18 @@ export function createSourceGraph(root) {
|
|
|
44
45
|
return { ordinaryRuntimeDependencies, parseSourceFile, resolveSourceImport, runtimeModuleReference }
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
export function ownedLazyPackageImport(node) {
|
|
49
|
+
if (!ts.isCallExpression(node) || node.expression.kind !== ts.SyntaxKind.ImportKeyword || node.arguments.length !== 1 || !ts.isStringLiteral(node.arguments[0])) return false
|
|
50
|
+
const target = node.arguments[0].text
|
|
51
|
+
if (!target || target.startsWith(".") || target.startsWith("/") || /^[a-z][a-z\d+.-]*:/i.test(target)) return false
|
|
52
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
53
|
+
if (!ts.isArrowFunction(current) && !ts.isFunctionExpression(current)) continue
|
|
54
|
+
const call = current.parent
|
|
55
|
+
return ts.isCallExpression(call) && call.arguments[0] === current && ts.isIdentifier(call.expression) && call.expression.text === "useEffect" && !current.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)
|
|
56
|
+
}
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
export function runtimeModuleReference(node) {
|
|
48
61
|
if (ts.isExportDeclaration(node)) return !node.isTypeOnly && (!node.exportClause || !ts.isNamedExports(node.exportClause) || node.exportClause.elements.some(entry => !entry.isTypeOnly))
|
|
49
62
|
const clause = node.importClause
|