@kudzujs/core 0.7.20 → 0.7.22
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/README.md +2 -1
- package/RELEASES.md +53 -0
- package/framework/README.md +5 -3
- package/framework/binding-runtime.js +12 -4
- package/framework/build.mjs +238 -33
- package/framework/collection-selector.js +3 -1
- package/framework/core.mjs +21 -10
- package/framework/list-runtime.js +14 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
10
10
|
|
|
11
11
|
> Experimental `0.7.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
|
-
**Latest release: 0.7.
|
|
13
|
+
**Latest release: 0.7.22 - SVG structures and native links.** Reactive SVG conditionals and flat keyed lists preserve their namespace, while supported React Router `Link` authoring erases to base-aware native anchors with zero router runtime. Read the [release notes](./RELEASES.md#0722---svg-structures-and-native-links) or open the [release page](https://kudzujs.cloud/releases/0.7.22).
|
|
14
14
|
|
|
15
15
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
16
16
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
|
@@ -82,6 +82,7 @@ ordinary React-shaped TSX
|
|
|
82
82
|
- Conditions, keyed collections, attributes, events, refs, effects, and supported component boundaries compile to route-specific capabilities. Inline or simple `const` setter callbacks and object refs may cross one ordinary component boundary into a direct intrinsic root.
|
|
83
83
|
- Build-known data and routes become complete HTML through async components and `getStaticPaths()`.
|
|
84
84
|
- Native document navigation is the default; static routes do not load a client runtime.
|
|
85
|
+
- A named or aliased React Router `Link` with a static root-relative `to` erases to a base-aware native anchor; no router package or runtime is emitted.
|
|
85
86
|
- Unsupported nearby patterns fail during the build with a source location and actionable boundary.
|
|
86
87
|
|
|
87
88
|
Migration input may retain supported imports from `react`; Kudzu erases those references and never emits or executes React. New Kudzu source should import framework APIs from `@kudzujs/core`.
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.22 - SVG structures and native links
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.22 extends ordinary React-shaped structural authoring into SVG and erases the common React Router `Link` form to native navigation without adding an SVG renderer or router runtime.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.22
|
|
8
|
+
|
|
9
|
+
- Reactive conditional branches inside SVG parse replacement markup in the actual SVG parent context and retain existing DOM ownership semantics.
|
|
10
|
+
- Flat intrinsic keyed lists inside SVG support add, update, reorder, and removal while preserving keyed identity and the SVG namespace.
|
|
11
|
+
- SVG fragment construction is compiled out of HTML-only condition and list builds; the measured matched fixture added 333 B raw / 79 B aggregate gzip JavaScript.
|
|
12
|
+
- A named or aliased `Link` import from `react-router-dom` with one static root-relative `to` lowers to a native `<a href>`, receives the configured `base`, and erases the package import.
|
|
13
|
+
- Dynamic or relative Link destinations, traversal, `NavLink`, router-only props, spreads, default/namespace imports, and non-JSX uses fail with source diagnostics.
|
|
14
|
+
- Effect dependency arrays are explicitly proven for multiple direct primitive states or supported props through existing commit batching and `Object.is` comparison.
|
|
15
|
+
- The complete suite passes 111/111 tests with Chrome coverage for SVG namespace, conditional replacement, keyed identity, Link erasure, and zero-JavaScript static output.
|
|
16
|
+
|
|
17
|
+
### Measured fixture
|
|
18
|
+
|
|
19
|
+
On the recorded Intel i5-9500 / Chrome 142 environment, 1,000-row SVG medians were 0.8 ms conditional, 1.9 ms update, 8.3 ms reverse, 2.4 ms remove, and 3.5 ms add. The matched HTML control measured 0.7, 1.8, 8.3, 2.5, and 3.6 ms. Link and native-anchor controls emitted byte-identical 248 B HTML and zero JavaScript. Full methodology and raw arrays are in `PERFORMANCE.md`.
|
|
20
|
+
|
|
21
|
+
### Boundary
|
|
22
|
+
|
|
23
|
+
Structural SVG currently supports reactive conditionals and flat intrinsic keyed lists. Keyed-item conditions, nested SVG lists, reactive MathML, and namespaced attributes remain unsupported. React Router lowering accepts only direct named or aliased `Link` JSX with one safe static root-relative destination and native anchor props. Native document navigation remains the default; no SPA router is included.
|
|
24
|
+
|
|
25
|
+
### Upgrade
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @kudzujs/core@^0.7.22
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 0.7.21 - Composable collections and effects
|
|
32
|
+
|
|
33
|
+
Kudzu 0.7.21 expands ordinary React-shaped collection pipelines and effect authoring while preserving keyed identity, derived dependency semantics, and capability-specific browser output.
|
|
34
|
+
|
|
35
|
+
### New in 0.7.21
|
|
36
|
+
|
|
37
|
+
- A relative-imported named or default synchronous one-parameter function may directly return a supported pure collection pipeline; Kudzu composes its selector without executing or shipping the function.
|
|
38
|
+
- Immutable `slice(start, end?)` accepts numeric literals and supported direct primitive state expressions for bounded pagination.
|
|
39
|
+
- Pure `filter` predicates may combine direct primitive state with supported string methods for reactive search.
|
|
40
|
+
- Expression-bodied `toSorted((left, right) => ...)` comparators compile into immutable selector sorting; mutating `sort()` fails with a source diagnostic.
|
|
41
|
+
- A top-level immutable primitive local derived from direct state may appear in an effect dependency array. Source commits schedule comparison, but unchanged derived results do not rerun the effect.
|
|
42
|
+
- Derived expressions are substituted into setup and cleanup handlers, so reruns and final disposal observe the latest value.
|
|
43
|
+
- Same-component top-level simple `const` setup and directly returned cleanup functions are statically substituted into effect handlers; indirect aliases remain unsupported.
|
|
44
|
+
- The complete suite passes 107/107 tests with browser coverage for pagination, search, sorted identity, derived equality, cleanup order, and named effect functions.
|
|
45
|
+
|
|
46
|
+
### Boundary
|
|
47
|
+
|
|
48
|
+
Collection transforms must be relative, synchronous, capture-free, one-parameter functions with one returned supported pipeline. Slice bounds and sort comparators reject arbitrary calls; `sort()` remains unsupported. Derived effect locals must be pure primitive expressions over direct state. Effect function references must be direct top-level `const` functions in the same component; dynamic selection, parameters, and cross-component references remain unsupported.
|
|
49
|
+
|
|
50
|
+
### Upgrade
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install @kudzujs/core@^0.7.21
|
|
54
|
+
```
|
|
55
|
+
|
|
3
56
|
## 0.7.20 - Computed child collections
|
|
4
57
|
|
|
5
58
|
Kudzu 0.7.20 accepts a common block-bodied keyed `map` callback that computes one direct child collection before returning JSX, without executing arbitrary callback code or adding a browser runtime path.
|
package/framework/README.md
CHANGED
|
@@ -4,9 +4,11 @@ Kudzu specializes ordinary common React-shaped TSX so migrations need minimal so
|
|
|
4
4
|
|
|
5
5
|
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, direct intrinsic `forwardRef`, top-level `const` identifiers initialized by `useId()`, and default, namespace, or named `Fragment`. `forwardRef()` accepts one inline synchronous `(props, ref)` function and requires the object ref exactly once on its direct intrinsic root; the compiler removes `ref` from props/rest and erases the wrapper. `useId()` becomes a deterministic build-time HTML ID and emits no browser capability; keyed rows reject it because cloned row templates cannot safely duplicate HTML IDs. Collection memos may start from local array state or a named relative import of an exported JSON-safe `const` array and may read direct local state declared in their dependency array. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined into existing bindings and keyed-list selectors because no browser component rerender or memo cache exists. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
6
6
|
|
|
7
|
+
A named or aliased `Link` import from `react-router-dom` may render directly with one static root-relative `to` plus native anchor props. The compiler prefixes the configured `base`, changes the element to `<a href>`, and erases the import, so native navigation remains the default and configured navigation groups see an ordinary eligible anchor. Dynamic or relative destinations, `NavLink`, router-only props, spreads, default/namespace imports, and non-JSX uses fail with source diagnostics. No React Router package code or router runtime is emitted.
|
|
8
|
+
|
|
7
9
|
Direct `clsx` calls over literal strings, numbers, arrays, object conditions, and conditional expressions are similarly lowered to ordinary concatenation and conditional expressions. The package import is erased, and dynamic classes continue through the existing binding compiler without serializing or shipping the `clsx` function.
|
|
8
10
|
|
|
9
|
-
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. A direct JSON-safe primitive parent state passed to a destructured child prop remains the same signal for child DOM bindings and effect dependencies; repeated calls own independent effect records, and conditional removal cleans up before remount recreates the effect. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
11
|
+
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. A direct JSON-safe primitive parent state passed to a destructured child prop remains the same signal for child DOM bindings and effect dependencies; repeated calls own independent effect records, and conditional removal cleans up before remount recreates the effect. Multiple direct primitive dependencies share the existing commit batching path: every value is compared with `Object.is`, and one or more same-turn changes cause one cleanup and rerun. A top-level immutable local derived through a supported pure primitive expression from direct state may also be an effect dependency: source state commits schedule evaluation, the derived result is compared with `Object.is`, and the expression is substituted into setup and cleanup handlers. Effect setup and directly returned cleanup callbacks may each resolve one top-level simple `const` function in the same component; those functions are substituted into the existing handler graph rather than retained in a browser registry. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
10
12
|
|
|
11
13
|
Reduced Zustand migration stores lower to one ordinary layout-lifetime state slot. The compiler accepts one exported `create(set => ({ data, ...actions }))` store with one serializable data property, direct property selectors, and synchronous capture-free actions using one-argument merge-form `set`; selected actions reuse the reducer-style functional update compiler, so same-turn calls observe current logical state and DOM writes still batch. The shared layout must initialize the store before route consumers, outside keyed rows. No Zustand import, store subscription runtime, React hook, or generic external-store capability is emitted.
|
|
12
14
|
|
|
@@ -31,11 +33,11 @@ Exact relative `.worker.ts` constructors in inline effects are validated and bun
|
|
|
31
33
|
|
|
32
34
|
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime. Source CSS and global `kudzu.config` styles are emitted in document heads before `afterBuild()` runs; static stylesheet links in component JSX fail compilation instead of loading from the body.
|
|
33
35
|
|
|
34
|
-
Inline SVG rendering normalizes an explicit set of common React presentation aliases before static serialization and binding descriptor creation. Reactive aliases
|
|
36
|
+
Inline SVG rendering normalizes an explicit set of common React presentation aliases before static serialization and binding descriptor creation. Reactive aliases use the existing generic `setAttribute` path. Reactive conditionals and flat intrinsic keyed lists store inert branch or row markup on SVG markers and parse it in the actual parent namespace only when replacement nodes are needed; existing condition/list ownership then handles insertion, identity, updates, and removal. Builds without structural SVG compile out that fragment path, and static SVG adds no JavaScript. Keyed-item conditions, nested SVG lists, MathML structures, and namespaced attributes remain unsupported.
|
|
35
37
|
|
|
36
38
|
Same-file, directly exported same-file, and relative-imported component chains receiving a direct local-state array or keyed item are recursively specialized to intrinsic JSX before keyed-list analysis, so their component functions are not retained in the browser. A directly exported row may be reused across static and keyed JSX sites; export-list/default aliases and non-JSX references remain rejected. Missing destructured props use directly serializable primitive, plain-object, or array literal defaults during specialization. One final identifier rest binding may be expanded exactly once at the direct intrinsic root. Rows may own multiple direct-property child maps recursively, nested conditions, latest-item handlers, multiple directly serializable state slots, effects, and `null`-initialized object refs. A block-bodied keyed `map` callback may declare one top-level `const` computed from a direct child collection through the supported pure selector pipeline and then return JSX; the alias must feed exactly one nested keyed list source. Structural list sites and ancestor key paths scope hooks across updates and reorder and release them on removal. Handler modules are emitted only when a rendered descriptor references them. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal and unrelated fields do nothing. Builds without item dependencies emit no item reader or list-state subscription code.
|
|
37
39
|
|
|
38
|
-
Rendered collection selectors compile immutable local aliases and inline `(item)` or `(item, index)` pipelines over local array state or supported static named imports. One alias may feed multiple keyed list sites when every reference is a statically analyzable collection source; mixed non-collection reads fail during compilation. Supported selectors are pure `filter` with direct local-state reads, direct-property `flatMap`,
|
|
40
|
+
Rendered collection selectors compile immutable local aliases and inline `(item)` or `(item, index)` pipelines over local array state or supported static named imports. One alias may feed multiple keyed list sites when every reference is a statically analyzable collection source; mixed non-collection reads fail during compilation. A relative-imported named or default synchronous function may accept one supported collection and directly return one pure selector pipeline; the compiler composes its selector without executing or shipping the function. Supported selectors are pure `filter` with direct local-state reads, direct-property `flatMap`, `Array.from`, immutable `slice(start, end?)`, and `toSorted((left, right) => expression)` before a final keyed `map`. Filter predicates may combine supported pure string methods such as `toLowerCase()` and `includes()` with direct primitive state for reactive search. Slice bounds may be direct numeric literals or supported expressions over direct primitive state. The two-parameter synchronous `toSorted` comparator may read item properties and direct primitive state; it sorts a copy in build and browser selectors, while mutating `sort()` is rejected. Dependency commits re-evaluate the selector against the immutable build-time collection while field keys retain item identity and `key={index}` retains positional identity. Compiler-owned static filters over structural keyed rows validate source references and keys once, retain removed row prototypes, clone fresh restoration nodes, and insert only new runs without moving retained DOM. Specialized collection wrappers and keyed rows inline direct object-literal or calling-component `const` object prop spreads in source order and forward JSX children into intrinsic output. Anonymous zero-argument lazy state initializers that return a directly serializable literal lower to the same ownership path. This route-specific path is compiled out elsewhere. Compiler-owned collection state is excluded from development snapshot restoration. Dynamic/computed prop spreads, arbitrary or captured imported callbacks, arbitrary slice-bound calls or sort comparators, package/namespace transforms, mutation, asynchronous selectors, prototype-sensitive reads, dynamic row state initializers, non-`null` or callback refs, and recursive component cycles fail during compilation.
|
|
39
41
|
|
|
40
42
|
The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. An optional inline, same-file, or relative-imported synchronous one-parameter initializer may derive a directly serializable literal only from its directly serializable initial argument; the compiler substitutes that argument and lowers the call to the ordinary two-argument ownership path. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Pure reducer-owned keyed lists reuse unchanged item identities for reorder, one removal, and append fast paths; ordinary `useState` lists retain full validation. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A reducer row reads the latest item through the existing list scope and uses the same multiple serializable state, effect, condition, and object-ref specialization as other keyed rows. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing directly serializable literal defaults and direct intrinsic rest props in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
41
43
|
|
|
@@ -122,10 +122,10 @@ function mountConditions(root) {
|
|
|
122
122
|
mountedConditions.add(start)
|
|
123
123
|
const descriptor = JSON.parse(start.dataset.kIf)
|
|
124
124
|
const end = findEnd(start, descriptor.id)
|
|
125
|
-
const truthy = start.content.querySelector("template[data-k-true]")
|
|
126
|
-
const falsy = start.content.querySelector("template[data-k-false]")
|
|
125
|
+
const truthy = globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg ? start.dataset.kSvgTrue : start.content.querySelector("template[data-k-true]")
|
|
126
|
+
const falsy = globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg ? start.dataset.kSvgFalse : start.content.querySelector("template[data-k-false]")
|
|
127
127
|
if (!end || !truthy || !falsy) continue
|
|
128
|
-
const condition = { start, end, truthy, falsy, kind: descriptor.kind, current: conditionKey(descriptor.kind, descriptor.initial), mount: descriptor.mount, owned: descriptor.owned }
|
|
128
|
+
const condition = { start, end, truthy, falsy, svg: globalThis.__KUDZU_SVG_CONDITIONS__ && descriptor.svg, kind: descriptor.kind, current: conditionKey(descriptor.kind, descriptor.initial), mount: descriptor.mount, owned: descriptor.owned }
|
|
129
129
|
mountConditionStates(condition, Boolean(descriptor.initial), false)
|
|
130
130
|
const mount = evaluator => {
|
|
131
131
|
if (!start.isConnected) return
|
|
@@ -154,7 +154,9 @@ function updateCondition(condition) {
|
|
|
154
154
|
const falseText = condition.kind === "and" && !truthy ? renderFalsy(value) : ""
|
|
155
155
|
const fragment = falseText
|
|
156
156
|
? textFragment(condition.end.ownerDocument, falseText)
|
|
157
|
-
:
|
|
157
|
+
: globalThis.__KUDZU_SVG_CONDITIONS__ && condition.svg
|
|
158
|
+
? svgFragment(condition.start, truthy ? condition.truthy : condition.falsy)
|
|
159
|
+
: (truthy ? condition.truthy : condition.falsy).content.cloneNode(true)
|
|
158
160
|
const nodes = condition.mount ? [...fragment.childNodes] : undefined
|
|
159
161
|
mountConditionStates(condition, truthy, true)
|
|
160
162
|
condition.end.parentNode.insertBefore(fragment, condition.end)
|
|
@@ -230,6 +232,12 @@ function textFragment(document, value) {
|
|
|
230
232
|
return fragment
|
|
231
233
|
}
|
|
232
234
|
|
|
235
|
+
function svgFragment(marker, markup) {
|
|
236
|
+
const range = marker.ownerDocument.createRange()
|
|
237
|
+
range.selectNode(marker)
|
|
238
|
+
return range.createContextualFragment(markup)
|
|
239
|
+
}
|
|
240
|
+
|
|
233
241
|
function findEnd(start, id) {
|
|
234
242
|
for (let node = start.nextSibling; node; node = node.nextSibling) {
|
|
235
243
|
if (node.nodeType === Node.ELEMENT_NODE && node.matches("template[data-k-if-end]") && node.dataset.kIfEnd === id) return node
|
package/framework/build.mjs
CHANGED
|
@@ -199,7 +199,9 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
199
199
|
const commandEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.commands).map(event => event.event)))].sort()
|
|
200
200
|
const nativeEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.native).map(event => event.event)))].sort()
|
|
201
201
|
const hasTextBindings = plans.some(plan => plan.bindings.some(binding => binding.target === "text"))
|
|
202
|
+
const hasSvgConditions = plans.some(plan => plan.conditions.some(condition => condition.svg))
|
|
202
203
|
const hasListConditions = plans.some(plan => plan.lists.some(list => list.conditions))
|
|
204
|
+
const hasSvgLists = plans.some(plan => plan.lists.some(list => list.svg))
|
|
203
205
|
const hasDeepListConditions = plans.some(plan => plan.lists.some(list => list.conditionHandlers))
|
|
204
206
|
const hasListTextRanges = plans.some(plan => plan.lists.some(list => list.textRanges))
|
|
205
207
|
const hasListAttributes = plans.some(plan => plan.lists.some(list => list.attributes))
|
|
@@ -213,6 +215,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
213
215
|
const hasComplexListRowState = plans.some(plan => plan.lists.some(list => list.rowStates?.some(state => state.initialValue !== null && typeof state.initialValue === "object")))
|
|
214
216
|
const hasNestedLists = plans.some(plan => plan.lists.some(list => list.ownerField))
|
|
215
217
|
const hasCollectionSelectors = plans.some(plan => plan.lists.some(list => list.selector))
|
|
218
|
+
const hasDerivedEffectDependencies = plans.some(plan => plan.effects.some(effect => effect.dependencyExpressions?.length))
|
|
216
219
|
const hasStaticCollections = plans.some(plan => plan.lists.some(list => list.static))
|
|
217
220
|
const hasListIndexes = plans.some(plan => plan.lists.some(list => list.indexed))
|
|
218
221
|
const hasListStableFastPaths = plans.some(plan => plan.lists.some(list => !list.children && !list.ownerField && list.key !== null && !list.indexed && !list.reducer && !list.selector))
|
|
@@ -271,11 +274,13 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
271
274
|
if (navigationRoutes.length) bindingRuntime = specializeNavigationTextDescriptors(bindingRuntime)
|
|
272
275
|
await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify, {
|
|
273
276
|
"globalThis.__KUDZU_TEXT_BINDINGS__": String(hasTextBindings),
|
|
277
|
+
"globalThis.__KUDZU_SVG_CONDITIONS__": String(hasSvgConditions),
|
|
274
278
|
"globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures)
|
|
275
279
|
})
|
|
276
280
|
}
|
|
281
|
+
if (hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
|
|
277
282
|
if (listCount) {
|
|
278
|
-
if (hasCollectionSelectors) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
|
|
283
|
+
if (hasCollectionSelectors && !hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
|
|
279
284
|
let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
|
|
280
285
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
281
286
|
listRuntime = hasCollectionSelectors
|
|
@@ -347,9 +352,10 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
347
352
|
__KUDZU_COLLECTION_SELECTORS__: String(hasCollectionSelectors),
|
|
348
353
|
__KUDZU_STATIC_COLLECTIONS__: String(hasStaticCollections),
|
|
349
354
|
__KUDZU_LIST_INDEXES__: String(hasListIndexes),
|
|
350
|
-
__KUDZU_LIST_STABLE_FAST_PATHS__: String(hasListStableFastPaths)
|
|
355
|
+
__KUDZU_LIST_STABLE_FAST_PATHS__: String(hasListStableFastPaths),
|
|
356
|
+
__KUDZU_SVG_LISTS__: String(hasSvgLists)
|
|
351
357
|
})
|
|
352
|
-
if (hasCollectionSelectors) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
|
|
358
|
+
if (hasCollectionSelectors && !hasDerivedEffectDependencies) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
|
|
353
359
|
}
|
|
354
360
|
if (hasNativeHandlers) {
|
|
355
361
|
const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
|
|
@@ -522,6 +528,7 @@ function printEffectEntry(effects, output, handlerModules, assetsDirectory, base
|
|
|
522
528
|
const hasCleanup = effects.some(effect => effect.cleanup)
|
|
523
529
|
const hasDependencies = effects.some(effect => effect.dependencies?.length || effect.itemDependencies?.length)
|
|
524
530
|
const hasOwners = effects.some(effect => effect.owner)
|
|
531
|
+
const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
|
|
525
532
|
const moduleUrls = [...new Set(effects.map(effect => effect.module))]
|
|
526
533
|
const modules = moduleUrls.map(url => {
|
|
527
534
|
const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
|
|
@@ -533,12 +540,13 @@ function printEffectEntry(effects, output, handlerModules, assetsDirectory, base
|
|
|
533
540
|
? `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}\nconst { browserState, commitDom } = __kRuntime`
|
|
534
541
|
: `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}`,
|
|
535
542
|
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
|
|
543
|
+
...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
|
|
536
544
|
...(paramPath ? [`import ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, paramPath)))}`] : []),
|
|
537
545
|
...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
|
|
538
546
|
]
|
|
539
547
|
const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
|
|
540
548
|
if (hasOwners) return printOwnedEffectEntry(imports, effects, entries)
|
|
541
|
-
if (effects.length === 1 && effects[0].dependencies?.length === 1) return printSingleDependencyEffect(imports, effects[0], hasCleanup)
|
|
549
|
+
if (effects.length === 1 && effects[0].dependencies?.length === 1 && !hasDependencyExpressions) return printSingleDependencyEffect(imports, effects[0], hasCleanup)
|
|
542
550
|
const disposal = hasCleanup ? `
|
|
543
551
|
let disposed = false
|
|
544
552
|
const dispose = root => {
|
|
@@ -616,6 +624,7 @@ async function flush() {
|
|
|
616
624
|
}
|
|
617
625
|
}
|
|
618
626
|
function readDependencies(record) {
|
|
627
|
+
${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
|
|
619
628
|
return (record.effect.dependencies ?? []).map(id => {
|
|
620
629
|
const value = browserState.get(id)
|
|
621
630
|
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
@@ -689,6 +698,7 @@ addEventListener("pagehide", event => {
|
|
|
689
698
|
}
|
|
690
699
|
|
|
691
700
|
function printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
|
|
701
|
+
const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
|
|
692
702
|
const moduleUrls = [...new Set(effects.map(effect => effect.module))]
|
|
693
703
|
const modules = moduleUrls.map(url => {
|
|
694
704
|
const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
|
|
@@ -698,6 +708,7 @@ function printNavigableEffectEntry(effects, output, handlerModules, assetsDirect
|
|
|
698
708
|
const imports = [
|
|
699
709
|
`import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
|
|
700
710
|
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
|
|
711
|
+
...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
|
|
701
712
|
...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
|
|
702
713
|
]
|
|
703
714
|
const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
|
|
@@ -765,6 +776,7 @@ function mount(lifetime) {
|
|
|
765
776
|
}
|
|
766
777
|
}
|
|
767
778
|
function readDependencies(record) {
|
|
779
|
+
${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
|
|
768
780
|
return (record.effect.dependencies ?? []).map(id => {
|
|
769
781
|
const value = __kRuntime.browserState.get(id)
|
|
770
782
|
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
@@ -809,6 +821,7 @@ function mount(lifetime) {
|
|
|
809
821
|
|
|
810
822
|
function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, base) {
|
|
811
823
|
const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
|
|
824
|
+
const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
|
|
812
825
|
const moduleUrls = [...new Set(effects.map(effect => effect.module))]
|
|
813
826
|
const modules = moduleUrls.map(url => {
|
|
814
827
|
const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
|
|
@@ -818,6 +831,7 @@ function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsD
|
|
|
818
831
|
const imports = [
|
|
819
832
|
`import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
|
|
820
833
|
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
|
|
834
|
+
...(hasDependencyExpressions ? [`import { evaluateCollectionExpression as __kEvaluateDependency } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-collection-selector.js")))}`] : []),
|
|
821
835
|
...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
|
|
822
836
|
]
|
|
823
837
|
const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
|
|
@@ -986,6 +1000,7 @@ function mount(lifetime) {
|
|
|
986
1000
|
}
|
|
987
1001
|
}
|
|
988
1002
|
function readDependencies(record) {
|
|
1003
|
+
${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
|
|
989
1004
|
const values = (record.effect.dependencies ?? []).map(id => {
|
|
990
1005
|
const value = __kRuntime.browserState.get(id)
|
|
991
1006
|
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
@@ -1063,6 +1078,7 @@ function runtimeEffects(effects, lifetimes = false) {
|
|
|
1063
1078
|
module: effect.module,
|
|
1064
1079
|
handler: effect.handler,
|
|
1065
1080
|
...(effect.dependencies ? { dependencies: effect.dependencies } : {}),
|
|
1081
|
+
...(effect.dependencyExpressions ? { dependencyExpressions: effect.dependencyExpressions, dependencyStates: effect.dependencyStates } : {}),
|
|
1066
1082
|
...(effect.itemDependencies ? { itemDependencies: effect.itemDependencies, listState: effect.listState } : {}),
|
|
1067
1083
|
...(effect.cleanup ? { cleanup: true } : {}),
|
|
1068
1084
|
...(effect.owner ? { owner: effect.owner } : {}),
|
|
@@ -1073,10 +1089,19 @@ function runtimeEffects(effects, lifetimes = false) {
|
|
|
1073
1089
|
}))
|
|
1074
1090
|
}
|
|
1075
1091
|
|
|
1092
|
+
function printDerivedDependencyRead(state) {
|
|
1093
|
+
return ` if (record.effect.dependencyExpressions) return record.effect.dependencyExpressions.map(expression => {
|
|
1094
|
+
const value = __kEvaluateDependency(expression, undefined, undefined, name => ${state}.get(record.effect.dependencyStates[name]))
|
|
1095
|
+
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() derived dependency must remain a JSON-safe primitive")
|
|
1096
|
+
return value
|
|
1097
|
+
})`
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1076
1100
|
function printOwnedEffectEntry(imports, effects, entries) {
|
|
1077
1101
|
const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
|
|
1078
1102
|
const hasOrdinaryDependencies = effects.some(effect => effect.dependencies?.length)
|
|
1079
|
-
const
|
|
1103
|
+
const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
|
|
1104
|
+
const hasRowState = effects.some(effect => JSON.stringify([effect.dependencies, effect.dependencyStates, effect.states, effect.scope]).includes("$k"))
|
|
1080
1105
|
return `${imports.join("\n")}
|
|
1081
1106
|
const effects = ${inlineJson(effects)}
|
|
1082
1107
|
const modules = new Map([${entries}])
|
|
@@ -1098,7 +1123,7 @@ ${hasRowState ? `function specializeRowEffect(effect, marker) {
|
|
|
1098
1123
|
const path = marker.dataset.kRowPath
|
|
1099
1124
|
const id = value => typeof value === "string" ? value.replace("$k", path) : value
|
|
1100
1125
|
const capture = value => value?.type === "state" || value?.type === "setter" || value?.type === "ref" ? { ...value, id: id(value.id) } : value?.type === "array" ? { ...value, value: value.value.map(capture) } : value?.type === "object" ? { ...value, value: value.value.map(([key, entry]) => [key, capture(entry)]) } : value
|
|
1101
|
-
return { ...effect, dependencies: effect.dependencies?.map(id), states: Object.fromEntries(Object.entries(effect.states).map(([name, value]) => [name, id(value)])), scope: Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, capture(value)])) }
|
|
1126
|
+
return { ...effect, dependencies: effect.dependencies?.map(id), dependencyStates: effect.dependencyStates && Object.fromEntries(Object.entries(effect.dependencyStates).map(([name, value]) => [name, id(value)])), states: Object.fromEntries(Object.entries(effect.states).map(([name, value]) => [name, id(value)])), scope: Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, capture(value)])) }
|
|
1102
1127
|
}
|
|
1103
1128
|
` : ""}
|
|
1104
1129
|
function registerDependencies(record) {
|
|
@@ -1240,6 +1265,7 @@ async function flush() {
|
|
|
1240
1265
|
}
|
|
1241
1266
|
}
|
|
1242
1267
|
function readDependencies(record) {
|
|
1268
|
+
${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
|
|
1243
1269
|
const values = (record.effect.dependencies ?? []).map(id => {
|
|
1244
1270
|
const value = browserState.get(id)
|
|
1245
1271
|
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
@@ -1712,7 +1738,8 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
|
|
|
1712
1738
|
if (errors.length) {
|
|
1713
1739
|
throw new Error(errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
|
|
1714
1740
|
}
|
|
1715
|
-
|
|
1741
|
+
const packageReference = emittedPackageReference(result.outputText, file, new Set(["react", "react-router-dom"]))
|
|
1742
|
+
if (packageReference) throw new Error(`${relative(root, file)} Runtime ${packageReference} module references are not supported`)
|
|
1716
1743
|
|
|
1717
1744
|
const output = compiledPath(file)
|
|
1718
1745
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
@@ -1735,18 +1762,88 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
|
|
|
1735
1762
|
return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, hasEffects: effectHandlers.length > 0, clientImports: [...clientImports] }
|
|
1736
1763
|
}
|
|
1737
1764
|
|
|
1738
|
-
function
|
|
1765
|
+
function emittedPackageReference(source, file, packages) {
|
|
1739
1766
|
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS)
|
|
1740
|
-
let found
|
|
1767
|
+
let found
|
|
1741
1768
|
const visit = node => {
|
|
1742
|
-
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text
|
|
1743
|
-
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && node.arguments[0].text
|
|
1769
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && packages.has(node.moduleSpecifier.text)) found = node.moduleSpecifier.text
|
|
1770
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require") && ts.isStringLiteral(node.arguments[0]) && packages.has(node.arguments[0].text)) found = node.arguments[0].text
|
|
1744
1771
|
if (!found) ts.forEachChild(node, visit)
|
|
1745
1772
|
}
|
|
1746
1773
|
visit(sourceFile)
|
|
1747
1774
|
return found
|
|
1748
1775
|
}
|
|
1749
1776
|
|
|
1777
|
+
function normalizeReactRouterSyntax(sourceFile, factory, context, base) {
|
|
1778
|
+
const links = new Set()
|
|
1779
|
+
for (const statement of sourceFile.statements) {
|
|
1780
|
+
if ((ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react-router-dom") {
|
|
1781
|
+
if (ts.isExportDeclaration(statement)) throw sourceNodeError(statement, sourceFile, "React Router exports are not supported; import Link directly where it renders")
|
|
1782
|
+
const clause = statement.importClause
|
|
1783
|
+
if (clause?.isTypeOnly) continue
|
|
1784
|
+
if (!clause) throw sourceNodeError(statement, sourceFile, "Side-effect React Router imports are not supported")
|
|
1785
|
+
if (clause.name) throw sourceNodeError(clause.name, sourceFile, "React Router default imports are not supported; use a named Link import")
|
|
1786
|
+
const bindings = clause.namedBindings
|
|
1787
|
+
if (!bindings || ts.isNamespaceImport(bindings)) throw sourceNodeError(bindings ?? statement, sourceFile, "React Router namespace imports are not supported; use a named Link import")
|
|
1788
|
+
for (const entry of bindings.elements) {
|
|
1789
|
+
if (entry.isTypeOnly) continue
|
|
1790
|
+
const imported = (entry.propertyName ?? entry.name).text
|
|
1791
|
+
if (imported === "NavLink") throw sourceNodeError(entry, sourceFile, "React Router NavLink active-route semantics cannot be erased to a native anchor")
|
|
1792
|
+
if (imported !== "Link") throw sourceNodeError(entry, sourceFile, `React Router ${imported} is not supported; only named Link imports can be erased to native anchors`)
|
|
1793
|
+
links.add(entry.name.text)
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (!links.size) return sourceFile
|
|
1798
|
+
|
|
1799
|
+
const routerProps = new Set(["discover", "end", "prefetch", "preventScrollReset", "relative", "reloadDocument", "replace", "state", "viewTransition"])
|
|
1800
|
+
const attributes = attributesNode => {
|
|
1801
|
+
const output = []
|
|
1802
|
+
let destination
|
|
1803
|
+
for (const property of attributesNode.properties) {
|
|
1804
|
+
if (ts.isJsxSpreadAttribute(property)) throw sourceNodeError(property, sourceFile, "React Router Link does not support spread attributes during native anchor lowering")
|
|
1805
|
+
const name = property.name.text
|
|
1806
|
+
if (name === "href") throw sourceNodeError(property, sourceFile, "React Router Link must not declare href; Kudzu derives it from to")
|
|
1807
|
+
if (routerProps.has(name)) throw sourceNodeError(property, sourceFile, `React Router Link prop ${JSON.stringify(name)} cannot be erased to a native anchor`)
|
|
1808
|
+
if (name !== "to") {
|
|
1809
|
+
output.push(ts.visitEachChild(property, visitor, context))
|
|
1810
|
+
continue
|
|
1811
|
+
}
|
|
1812
|
+
if (destination !== undefined) throw sourceNodeError(property, sourceFile, "React Router Link requires exactly one to attribute")
|
|
1813
|
+
if (!property.initializer || !ts.isStringLiteral(property.initializer)) throw sourceNodeError(property, sourceFile, 'React Router Link requires a static root-relative to="/path"')
|
|
1814
|
+
destination = property.initializer.text
|
|
1815
|
+
const pathname = destination.match(/^[^?#]*/)[0]
|
|
1816
|
+
let decoded
|
|
1817
|
+
try { decoded = decodeURIComponent(pathname) } catch { throw sourceNodeError(property.initializer, sourceFile, 'React Router Link requires a safe static root-relative to="/path"') }
|
|
1818
|
+
if (!destination.startsWith("/") || destination.startsWith("//") || /%(?:2f|5c)/i.test(pathname) || /[\\\0]/.test(decoded) || decoded.split("/").includes("..") || [...decoded].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159)) throw sourceNodeError(property.initializer, sourceFile, 'React Router Link requires a safe static root-relative to="/path"')
|
|
1819
|
+
output.push(factory.createJsxAttribute(factory.createIdentifier("href"), factory.createStringLiteral(withBase(base, destination))))
|
|
1820
|
+
}
|
|
1821
|
+
if (destination === undefined) throw sourceNodeError(attributesNode.parent, sourceFile, "React Router Link requires exactly one static root-relative to attribute")
|
|
1822
|
+
return factory.updateJsxAttributes(attributesNode, output)
|
|
1823
|
+
}
|
|
1824
|
+
const importedLink = tag => ts.isIdentifier(tag) && links.has(tag.text) && !isShadowedIdentifier(tag, sourceFile)
|
|
1825
|
+
const visitor = node => {
|
|
1826
|
+
if (ts.isJsxElement(node) && importedLink(node.openingElement.tagName)) {
|
|
1827
|
+
const opening = factory.updateJsxOpeningElement(node.openingElement, factory.createIdentifier("a"), node.openingElement.typeArguments, attributes(node.openingElement.attributes))
|
|
1828
|
+
const closing = factory.updateJsxClosingElement(node.closingElement, factory.createIdentifier("a"))
|
|
1829
|
+
return factory.updateJsxElement(node, opening, ts.visitNodes(node.children, visitor), closing)
|
|
1830
|
+
}
|
|
1831
|
+
if (ts.isJsxSelfClosingElement(node) && importedLink(node.tagName)) return factory.updateJsxSelfClosingElement(node, factory.createIdentifier("a"), node.typeArguments, attributes(node.attributes))
|
|
1832
|
+
if (ts.isIdentifier(node) && links.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile)) throw sourceNodeError(node, sourceFile, "React Router Link imports may only be used as direct JSX elements")
|
|
1833
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react-router-dom") {
|
|
1834
|
+
const clause = node.importClause
|
|
1835
|
+
if (!clause || clause.isTypeOnly) return node
|
|
1836
|
+
const bindings = clause.namedBindings
|
|
1837
|
+
if (!bindings || !ts.isNamedImports(bindings)) return node
|
|
1838
|
+
const elements = bindings.elements.filter(entry => entry.isTypeOnly || (entry.propertyName ?? entry.name).text !== "Link")
|
|
1839
|
+
if (!elements.length) return undefined
|
|
1840
|
+
return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, undefined, factory.updateNamedImports(bindings, elements)), node.moduleSpecifier, node.attributes)
|
|
1841
|
+
}
|
|
1842
|
+
return ts.visitEachChild(node, visitor, context)
|
|
1843
|
+
}
|
|
1844
|
+
return ts.visitNode(sourceFile, visitor)
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1750
1847
|
function normalizeClsxSyntax(sourceFile, factory, context) {
|
|
1751
1848
|
const names = new Set()
|
|
1752
1849
|
for (const statement of sourceFile.statements) {
|
|
@@ -2222,7 +2319,7 @@ function lowerReactMemoCollectionExpression(expression, factory) {
|
|
|
2222
2319
|
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && node.arguments.length === 1) {
|
|
2223
2320
|
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Array"), "from"), undefined, [visit(node.expression.expression), node.arguments[0]])
|
|
2224
2321
|
}
|
|
2225
|
-
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["filter", "flatMap"].includes(node.expression.name.text)) {
|
|
2322
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["filter", "flatMap", "slice", "toSorted"].includes(node.expression.name.text)) {
|
|
2226
2323
|
return factory.updateCallExpression(node, factory.updatePropertyAccessExpression(node.expression, visit(node.expression.expression), node.expression.name), node.typeArguments, node.arguments)
|
|
2227
2324
|
}
|
|
2228
2325
|
if (isArrayFromCall(node)) return factory.updateCallExpression(node, node.expression, node.typeArguments, [visit(node.arguments[0]), ...node.arguments.slice(1)])
|
|
@@ -2276,6 +2373,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2276
2373
|
const factory = context.factory
|
|
2277
2374
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
2278
2375
|
const importedCollections = importedSerializableCollectionNames(sourceFile, file, sourceFiles, sourceIndex)
|
|
2376
|
+
sourceFile = normalizeReactRouterSyntax(sourceFile, factory, context, base)
|
|
2377
|
+
ts.setParentRecursive(sourceFile, false)
|
|
2279
2378
|
sourceFile = normalizeClsxSyntax(sourceFile, factory, context)
|
|
2280
2379
|
ts.setParentRecursive(sourceFile, false)
|
|
2281
2380
|
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections)
|
|
@@ -2294,7 +2393,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2294
2393
|
const importedSource = target => {
|
|
2295
2394
|
let imported = importedSources.get(target)
|
|
2296
2395
|
if (!imported) {
|
|
2297
|
-
imported =
|
|
2396
|
+
imported = normalizeReactRouterSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context, base)
|
|
2397
|
+
ts.setParentRecursive(imported, false)
|
|
2398
|
+
imported = normalizeClsxSyntax(imported, factory, context)
|
|
2298
2399
|
ts.setParentRecursive(imported, false)
|
|
2299
2400
|
imported = normalizeReactMigrationSyntax(imported, factory, context, importedSerializableCollectionNames(imported, target, sourceFiles, sourceIndex))
|
|
2300
2401
|
ts.setParentRecursive(imported, false)
|
|
@@ -2309,6 +2410,13 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2309
2410
|
}
|
|
2310
2411
|
return imported
|
|
2311
2412
|
}
|
|
2413
|
+
const importedCollectionTransforms = new Map()
|
|
2414
|
+
for (const [name, binding] of importBindings) {
|
|
2415
|
+
if (binding.kind === "namespace") continue
|
|
2416
|
+
try {
|
|
2417
|
+
importedCollectionTransforms.set(name, resolveComponentExport(binding.target, binding.kind === "default" ? "default" : binding.imported, target => parseSourceFile(target, sourceIndex.get(target)), sourceFiles))
|
|
2418
|
+
} catch {}
|
|
2419
|
+
}
|
|
2312
2420
|
const settersByFunction = new Map()
|
|
2313
2421
|
const reducersByFunction = new Map()
|
|
2314
2422
|
const zustandStores = new Map()
|
|
@@ -2446,7 +2554,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2446
2554
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
2447
2555
|
for (const [name, entries] of declarations) {
|
|
2448
2556
|
for (const declaration of entries) {
|
|
2449
|
-
const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections, factory, context)
|
|
2557
|
+
const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections, factory, context, importedCollectionTransforms)
|
|
2450
2558
|
if (!parts) continue
|
|
2451
2559
|
const uses = []
|
|
2452
2560
|
const collectUses = node => {
|
|
@@ -2640,7 +2748,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2640
2748
|
return
|
|
2641
2749
|
}
|
|
2642
2750
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
2643
|
-
const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail, new Set(), importedCollections, factory, context)
|
|
2751
|
+
const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail, new Set(), importedCollections, factory, context, importedCollectionTransforms)
|
|
2644
2752
|
if (parts) {
|
|
2645
2753
|
for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
|
|
2646
2754
|
rawRenderedLists.push({ node, parts })
|
|
@@ -2855,14 +2963,24 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2855
2963
|
fail(target, message)
|
|
2856
2964
|
}
|
|
2857
2965
|
if (node.arguments.length !== 2) effectFail(node, "useEffect() requires exactly a callback and literal dependency array")
|
|
2858
|
-
const [
|
|
2859
|
-
|
|
2966
|
+
const [callbackArgument, dependencies] = node.arguments
|
|
2967
|
+
const effectOwner = nearestFunction(node)
|
|
2968
|
+
const resolveEffectFunction = expression => {
|
|
2969
|
+
if (!ts.isIdentifier(expression)) return undefined
|
|
2970
|
+
const entries = jsxLocalDeclarations.get(effectOwner)?.get(expression.text)
|
|
2971
|
+
if (entries?.length !== 1 || entries[0].node.parent?.parent?.parent !== effectOwner?.body) return undefined
|
|
2972
|
+
const initializer = entries[0].initializer
|
|
2973
|
+
return ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer) ? initializer : undefined
|
|
2974
|
+
}
|
|
2975
|
+
let callback = ts.isArrowFunction(callbackArgument) || ts.isFunctionExpression(callbackArgument) ? callbackArgument : resolveEffectFunction(callbackArgument)
|
|
2976
|
+
if (!callback) effectFail(callbackArgument, "useEffect() callback must be inline or one top-level const function")
|
|
2860
2977
|
if (ts.isFunctionExpression(callback) && callback.name) effectFail(callback, "useEffect() callback function must be anonymous")
|
|
2861
2978
|
if (callback.asteriskToken) effectFail(callback, "useEffect() callback cannot be a generator")
|
|
2862
2979
|
if (callback.parameters.length) effectFail(callback, "useEffect() callback cannot declare parameters")
|
|
2863
2980
|
if (!ts.isArrayLiteralExpression(dependencies)) effectFail(dependencies, "useEffect() dependencies must be a literal array")
|
|
2864
2981
|
const itemDependencies = []
|
|
2865
2982
|
const ordinaryDependencies = []
|
|
2983
|
+
const setters = settersForNode(node, settersByFunction)
|
|
2866
2984
|
let dependencyItem = listEffect?.item
|
|
2867
2985
|
for (const dependency of dependencies.elements) {
|
|
2868
2986
|
const value = unwrapExpression(dependency)
|
|
@@ -2879,24 +2997,74 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2879
2997
|
}
|
|
2880
2998
|
const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
|
|
2881
2999
|
if (invalidDependency) effectFail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
|
|
2882
|
-
|
|
3000
|
+
const dependencyExpressions = []
|
|
3001
|
+
const dependencyStates = new Map()
|
|
3002
|
+
const dependencySubstitutions = new Map()
|
|
3003
|
+
const subscriptionDependencies = []
|
|
3004
|
+
let hasDerivedDependency = false
|
|
3005
|
+
const stateNames = new Set(setters.values())
|
|
3006
|
+
const localDeclarations = jsxLocalDeclarations.get(nearestFunction(node))
|
|
3007
|
+
for (const dependency of ordinaryDependencies) {
|
|
3008
|
+
const entries = localDeclarations?.get(dependency.text)
|
|
3009
|
+
const initializer = entries?.length === 1 ? entries[0].initializer : undefined
|
|
3010
|
+
const directAlias = initializer && ts.isIdentifier(unwrapExpression(initializer)) && stateNames.has(unwrapExpression(initializer).text)
|
|
3011
|
+
const derivedStates = initializer && !directAlias ? referencedStateNames(initializer, setters) : new Set()
|
|
3012
|
+
if (derivedStates.size) {
|
|
3013
|
+
const usedStates = new Set()
|
|
3014
|
+
const expression = collectionExpression(initializer, {}, effectFail, stateNames, usedStates)
|
|
3015
|
+
if (!usedStates.size) effectFail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
|
|
3016
|
+
dependencyExpressions.push(expression)
|
|
3017
|
+
for (const name of usedStates) {
|
|
3018
|
+
subscriptionDependencies.push(factory.createIdentifier(name))
|
|
3019
|
+
dependencyStates.set(name, factory.createIdentifier(name))
|
|
3020
|
+
}
|
|
3021
|
+
dependencySubstitutions.set(dependency.text, initializer)
|
|
3022
|
+
hasDerivedDependency = true
|
|
3023
|
+
} else {
|
|
3024
|
+
subscriptionDependencies.push(dependency)
|
|
3025
|
+
dependencyExpressions.push(["state", dependency.text])
|
|
3026
|
+
dependencyStates.set(dependency.text, dependency)
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
if (!hasDerivedDependency) {
|
|
3030
|
+
dependencyExpressions.length = 0
|
|
3031
|
+
dependencyStates.clear()
|
|
3032
|
+
}
|
|
3033
|
+
if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
|
|
2883
3034
|
if (!ts.isBlock(callback.body)) effectFail(callback, "useEffect() callback must use a block body")
|
|
3035
|
+
const cleanupSubstitutions = new Map()
|
|
3036
|
+
const collectNamedCleanups = current => {
|
|
3037
|
+
if (current !== callback && isFunctionLike(current)) return
|
|
3038
|
+
if (ts.isReturnStatement(current) && current.expression && ts.isIdentifier(unwrapExpression(current.expression))) {
|
|
3039
|
+
const cleanup = resolveEffectFunction(unwrapExpression(current.expression))
|
|
3040
|
+
if (cleanup) cleanupSubstitutions.set(unwrapExpression(current.expression).text, cleanup)
|
|
3041
|
+
}
|
|
3042
|
+
ts.forEachChild(current, collectNamedCleanups)
|
|
3043
|
+
}
|
|
3044
|
+
collectNamedCleanups(callback.body)
|
|
3045
|
+
if (cleanupSubstitutions.size) {
|
|
3046
|
+
callback = substituteClone(callback, cleanupSubstitutions, factory, context)
|
|
3047
|
+
ts.setParentRecursive(callback, false)
|
|
3048
|
+
callback.parent = callbackArgument.parent
|
|
3049
|
+
}
|
|
2884
3050
|
const returns = effectReturns(callback)
|
|
2885
3051
|
if (returns.invalid) effectFail(returns.invalid, "useEffect() return values must be inline cleanup functions")
|
|
2886
3052
|
const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
|
|
2887
3053
|
if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
|
|
2888
3054
|
if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
|
|
2889
|
-
const setters = settersForNode(node, settersByFunction)
|
|
2890
3055
|
const callbackSource = listEffect?.sourceFile ?? sourceFile
|
|
2891
3056
|
const callbackFile = callbackSource.fileName
|
|
2892
3057
|
const workerStart = workerReferences.length
|
|
2893
|
-
let compiledCallback
|
|
3058
|
+
let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
|
|
3059
|
+
if (compiledCallback !== callback) {
|
|
3060
|
+
ts.setParentRecursive(compiledCallback, false)
|
|
3061
|
+
compiledCallback.parent = callback.parent
|
|
3062
|
+
}
|
|
2894
3063
|
if (listEffect && callbackFile !== file) {
|
|
2895
3064
|
const originalCallback = listEffect.source.arguments[0]
|
|
2896
3065
|
rejectWorkerConstructions(originalCallback, callbackSource, "Relative TypeScript Worker construction in imported keyed-row effects is not supported; construct the Worker in a directly compiled page or local component effect")
|
|
2897
|
-
compiledCallback = callback
|
|
2898
3066
|
} else {
|
|
2899
|
-
compiledCallback = rewriteEffectWorkers(
|
|
3067
|
+
compiledCallback = rewriteEffectWorkers(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
|
|
2900
3068
|
}
|
|
2901
3069
|
const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers, listEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup)
|
|
2902
3070
|
for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
|
|
@@ -2904,14 +3072,16 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2904
3072
|
usesBehavior = true
|
|
2905
3073
|
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
2906
3074
|
callback,
|
|
2907
|
-
factory.createArrayLiteralExpression(ordinaryDependencies),
|
|
3075
|
+
factory.createArrayLiteralExpression(hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies),
|
|
2908
3076
|
factory.createStringLiteral(handlerUrl),
|
|
2909
3077
|
factory.createStringLiteral(descriptor.exportName),
|
|
2910
3078
|
descriptor.states,
|
|
2911
3079
|
descriptor.scope,
|
|
2912
3080
|
factory.createStringLiteral(listEffect ? sourceLocation(listEffect.source, listEffect.sourceFile) : sourceLocation(node, sourceFile)),
|
|
2913
3081
|
returns.cleanup ? factory.createTrue() : factory.createFalse(),
|
|
2914
|
-
factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field)))
|
|
3082
|
+
factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
|
|
3083
|
+
hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
|
|
3084
|
+
factory.createArrayLiteralExpression([...dependencyStates].map(([name, state]) => factory.createArrayLiteralExpression([factory.createStringLiteral(name), state])))
|
|
2915
3085
|
])
|
|
2916
3086
|
}
|
|
2917
3087
|
|
|
@@ -3157,11 +3327,11 @@ function containsRenderControl(root, knownLocals) {
|
|
|
3157
3327
|
return found
|
|
3158
3328
|
}
|
|
3159
3329
|
|
|
3160
|
-
function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set(), factory = ts.factory, context) {
|
|
3330
|
+
function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set(), factory = ts.factory, context, importedCollectionTransforms = new Map()) {
|
|
3161
3331
|
const value = unwrapExpression(expression)
|
|
3162
3332
|
const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
|
|
3163
3333
|
if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
|
|
3164
|
-
const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()))
|
|
3334
|
+
const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context)
|
|
3165
3335
|
if (!collection?.state) return undefined
|
|
3166
3336
|
if (directFrom) collection.selector.push(["from", undefined])
|
|
3167
3337
|
let callback = directFrom ? value.arguments[1] : value.arguments[0]
|
|
@@ -3171,7 +3341,7 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
|
|
|
3171
3341
|
if (!context || root.statements.length !== 2 || !ts.isVariableStatement(root.statements[0]) || (root.statements[0].declarationList.flags & ts.NodeFlags.Const) === 0 || root.statements[0].declarationList.declarations.length !== 1 || !ts.isReturnStatement(root.statements[1]) || !root.statements[1].expression) fail(root, "Block-bodied keyed list map callbacks require one computed child collection const and a final JSX return")
|
|
3172
3342
|
const declaration = root.statements[0].declarationList.declarations[0]
|
|
3173
3343
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, "Computed child collections must initialize one const identifier")
|
|
3174
|
-
const computed = renderedCollectionSource(declaration.initializer, new Map(), undefined, fail, new Set())
|
|
3344
|
+
const computed = renderedCollectionSource(declaration.initializer, new Map(), undefined, fail, new Set(), new Set(), new Set(), importedCollectionTransforms, factory, context)
|
|
3175
3345
|
if (!computed?.ownerField || computed.parentItem !== parameters.item) fail(declaration.initializer, `Computed child collections must start from ${parameters.item}.<field>`)
|
|
3176
3346
|
const returned = root.statements[1].expression
|
|
3177
3347
|
if (identifierReferenceCount(returned, declaration.name.text) !== 1) fail(declaration.name, `Computed child collection alias "${declaration.name.text}" must be used exactly once`)
|
|
@@ -3208,7 +3378,7 @@ function nestedKeyedListParts(expression, parentItem, fail) {
|
|
|
3208
3378
|
return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
|
|
3209
3379
|
}
|
|
3210
3380
|
|
|
3211
|
-
function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set()) {
|
|
3381
|
+
function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set(), importedCollectionTransforms = new Map(), factory = ts.factory, context) {
|
|
3212
3382
|
const value = unwrapExpression(expression)
|
|
3213
3383
|
if (ts.isIdentifier(value)) {
|
|
3214
3384
|
if ([...setters.values()].includes(value.text)) return { state: value, selector: [], selectorStates: new Set() }
|
|
@@ -3217,16 +3387,30 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
3217
3387
|
if (!entries) return undefined
|
|
3218
3388
|
if (entries.length !== 1 || aliases.has(value.text) || entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value, `Rendered collection alias "${value.text}" must be one top-level immutable local`)
|
|
3219
3389
|
aliases.add(value.text)
|
|
3220
|
-
const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
3390
|
+
const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
3221
3391
|
aliases.delete(value.text)
|
|
3222
3392
|
return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node], aliasUses: [...(source.aliasUses ?? []), value] }
|
|
3223
3393
|
}
|
|
3224
3394
|
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) return { state: undefined, ownerField: value.name.text, selector: [], parentItem: value.expression.text }
|
|
3395
|
+
if (ts.isCallExpression(value) && ts.isIdentifier(value.expression) && importedCollectionTransforms.has(value.expression.text)) {
|
|
3396
|
+
const transform = importedCollectionTransforms.get(value.expression.text)
|
|
3397
|
+
const parameter = transform.parameters[0]
|
|
3398
|
+
if (value.arguments.length !== 1 || transform.parameters.length !== 1 || transform.asteriskToken || transform.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !parameter || !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken) fail(value, `Imported collection transform "${value.expression.text}" must be synchronous with exactly one identifier parameter and one argument`)
|
|
3399
|
+
const returned = ts.isBlock(transform.body)
|
|
3400
|
+
? transform.body.statements.length === 1 && ts.isReturnStatement(transform.body.statements[0]) ? transform.body.statements[0].expression : undefined
|
|
3401
|
+
: transform.body
|
|
3402
|
+
if (!returned) fail(value, `Imported collection transform "${value.expression.text}" must contain only one returned collection expression`)
|
|
3403
|
+
const transformSource = renderedCollectionSource(returned, new Map([[parameter.name.text, parameter.name.text]]), undefined, fail, new Set(), new Set(), new Set([parameter.name.text]))
|
|
3404
|
+
if (!transformSource?.state || transformSource.state.text !== parameter.name.text || transformSource.selectorStates.size) fail(value, `Imported collection transform "${value.expression.text}" must return a supported pure pipeline rooted only in its parameter`)
|
|
3405
|
+
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
3406
|
+
if (!source) fail(value.arguments[0], `Imported collection transform "${value.expression.text}" requires a supported collection argument`)
|
|
3407
|
+
return { ...source, selector: [...source.selector, ...transformSource.selector] }
|
|
3408
|
+
}
|
|
3225
3409
|
if (ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression)) {
|
|
3226
3410
|
const method = value.expression.name.text
|
|
3227
3411
|
if (method === "filter") {
|
|
3228
3412
|
if (value.arguments.length !== 1) fail(value, "Rendered collection filter() requires one inline predicate")
|
|
3229
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
3413
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
3230
3414
|
if (!source) return undefined
|
|
3231
3415
|
const parameters = collectionParameters(value.arguments[0], "Rendered collection filter()", fail)
|
|
3232
3416
|
const selectorStates = new Set(source.selectorStates)
|
|
@@ -3234,7 +3418,7 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
3234
3418
|
}
|
|
3235
3419
|
if (method === "flatMap") {
|
|
3236
3420
|
if (value.arguments.length !== 1) fail(value, "Rendered collection flatMap() requires one inline projector")
|
|
3237
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
3421
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
3238
3422
|
if (!source) return undefined
|
|
3239
3423
|
const parameters = collectionParameters(value.arguments[0], "Rendered collection flatMap()", fail)
|
|
3240
3424
|
const field = directProperty(value.arguments[0].body, parameters.item)
|
|
@@ -3242,10 +3426,31 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
3242
3426
|
if (["__proto__", "constructor", "prototype"].includes(field)) fail(value.arguments[0].body, `Rendered collection property "${field}" is not supported`)
|
|
3243
3427
|
return { ...source, selector: [...source.selector, ["flatMap", field]] }
|
|
3244
3428
|
}
|
|
3429
|
+
if (method === "slice") {
|
|
3430
|
+
if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered collection slice() requires a start and optional end")
|
|
3431
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
3432
|
+
if (!source) return undefined
|
|
3433
|
+
const selectorStates = new Set(source.selectorStates)
|
|
3434
|
+
const start = collectionExpression(value.arguments[0], {}, fail, stateNames, selectorStates)
|
|
3435
|
+
const end = value.arguments[1] && collectionExpression(value.arguments[1], {}, fail, stateNames, selectorStates)
|
|
3436
|
+
return { ...source, selector: [...source.selector, ["slice", start, end]], selectorStates }
|
|
3437
|
+
}
|
|
3438
|
+
if (method === "toSorted") {
|
|
3439
|
+
if (value.arguments.length !== 1) fail(value, "Rendered collection toSorted() requires one inline comparator")
|
|
3440
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
3441
|
+
if (!source) return undefined
|
|
3442
|
+
const comparator = value.arguments[0]
|
|
3443
|
+
const parameters = collectionParameters(comparator, "Rendered collection toSorted()", fail)
|
|
3444
|
+
if (comparator.parameters.length !== 2 || ts.isBlock(comparator.body)) fail(comparator, "Rendered collection toSorted() comparator must be a synchronous expression arrow with (left, right) identifier parameters")
|
|
3445
|
+
const selectorStates = new Set(source.selectorStates)
|
|
3446
|
+
const expression = collectionExpression(comparator.body, parameters, fail, stateNames, selectorStates)
|
|
3447
|
+
return { ...source, selector: [...source.selector, ["sort", expression]], selectorStates }
|
|
3448
|
+
}
|
|
3449
|
+
if (method === "sort") fail(value, "Rendered collections cannot use mutating sort(); use toSorted()")
|
|
3245
3450
|
}
|
|
3246
3451
|
if (isArrayFromCall(value)) {
|
|
3247
3452
|
if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered Array.from() requires an anchor and optional inline mapper")
|
|
3248
|
-
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
3453
|
+
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
3249
3454
|
if (!source) return undefined
|
|
3250
3455
|
let mapper
|
|
3251
3456
|
if (value.arguments[1]) {
|
|
@@ -3932,7 +4137,7 @@ function jsxTagUses(root, name) {
|
|
|
3932
4137
|
return uses
|
|
3933
4138
|
}
|
|
3934
4139
|
|
|
3935
|
-
const pureListMethods = new Set(["at", "charAt", "charCodeAt", "concat", "endsWith", "includes", "indexOf", "join", "lastIndexOf", "padEnd", "padStart", "repeat", "replace", "replaceAll", "slice", "startsWith", "substring", "toLowerCase", "toUpperCase", "trim", "trimEnd", "trimStart"])
|
|
4140
|
+
const pureListMethods = new Set(["at", "charAt", "charCodeAt", "concat", "endsWith", "includes", "indexOf", "join", "lastIndexOf", "localeCompare", "padEnd", "padStart", "repeat", "replace", "replaceAll", "slice", "startsWith", "substring", "toLowerCase", "toUpperCase", "trim", "trimEnd", "trimStart"])
|
|
3936
4141
|
const mutatingListMethods = new Set(["copyWithin", "fill", "pop", "push", "reverse", "shift", "sort", "splice", "unshift"])
|
|
3937
4142
|
const pureMathMethods = new Set(["abs", "ceil", "floor", "max", "min", "pow", "round", "sign", "sqrt", "trunc"])
|
|
3938
4143
|
const pureListGlobals = new Set(["Boolean", "Infinity", "Math", "NaN", "Number", "String", "undefined"])
|
|
@@ -6,13 +6,15 @@ export function selectCollection(anchor, selector = [], readState) {
|
|
|
6
6
|
if (!Array.isArray(values)) throw new Error("Rendered collection source must remain an array")
|
|
7
7
|
if (operation[0] === "filter") values = values.filter((item, index) => evaluateCollectionExpression(operation[1], item, index, readState))
|
|
8
8
|
else if (operation[0] === "flatMap") values = values.flatMap(item => item?.[operation[1]] ?? [])
|
|
9
|
+
else if (operation[0] === "slice") values = values.slice(evaluateCollectionExpression(operation[1], undefined, undefined, readState), operation[2] && evaluateCollectionExpression(operation[2], undefined, undefined, readState))
|
|
10
|
+
else if (operation[0] === "sort") values = [...values].sort((left, right) => evaluateCollectionExpression(operation[1], left, right, readState))
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
if (!Array.isArray(values)) throw new Error("Rendered collection source must remain an array")
|
|
12
14
|
return values
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
function evaluateCollectionExpression(expression, item, index, readState) {
|
|
17
|
+
export function evaluateCollectionExpression(expression, item, index, readState) {
|
|
16
18
|
const [kind, ...parts] = expression
|
|
17
19
|
if (kind === "value") return parts[0]
|
|
18
20
|
if (kind === "undefined") return undefined
|
package/framework/core.mjs
CHANGED
|
@@ -145,14 +145,18 @@ function createInternalState(initialValue) {
|
|
|
145
145
|
return signal
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup, itemDependencies = []) {
|
|
148
|
+
export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup, itemDependencies = [], dependencyExpressions = [], dependencyStates = []) {
|
|
149
149
|
if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
|
|
150
150
|
if (typeof callback !== "function" || !Array.isArray(dependencies) || !module || !handler) throw new Error("useEffect() must be compiled with a literal dependency array")
|
|
151
151
|
if (itemDependencies.length && !renderContext.listDepth) throw new Error(`${source} useEffect() item-property dependencies are only supported in direct keyed row components`)
|
|
152
|
-
const dependencyIds = dependencies.map(dependency => {
|
|
152
|
+
const dependencyIds = [...new Set(dependencies.map(dependency => {
|
|
153
153
|
if (!dependency?.[signalMarker] || !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive Kudzu state or runtime parameter identifiers`)
|
|
154
154
|
return dependency.id
|
|
155
|
-
})
|
|
155
|
+
}))]
|
|
156
|
+
const dependencyStateIds = Object.fromEntries(dependencyStates.map(([name, dependency]) => {
|
|
157
|
+
if (!dependency?.[signalMarker] || !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() derived dependency state ${JSON.stringify(name)} must be primitive Kudzu state`)
|
|
158
|
+
return [name, dependency.id]
|
|
159
|
+
}))
|
|
156
160
|
let owner
|
|
157
161
|
let list = false
|
|
158
162
|
if (renderContext.listDepth) {
|
|
@@ -178,7 +182,7 @@ export function useEffect(callback, dependencies, module, handler, states, scope
|
|
|
178
182
|
owner = nextRenderId("e")
|
|
179
183
|
owners.push(owner)
|
|
180
184
|
}
|
|
181
|
-
if (!renderContext.listDepth || list) renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(itemDependencies.length ? { itemDependencies, listState: renderContext.listRoot.state } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
185
|
+
if (!renderContext.listDepth || list) renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(dependencyExpressions.length ? { dependencyExpressions, dependencyStates: dependencyStateIds } : {}), ...(itemDependencies.length ? { itemDependencies, listState: renderContext.listRoot.state } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
182
186
|
renderContext.handlerModules.add(module)
|
|
183
187
|
renderContext.hasBehaviors = true
|
|
184
188
|
renderContext.hasEffects = true
|
|
@@ -424,6 +428,7 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
424
428
|
module: effect.module,
|
|
425
429
|
handler: effect.handler,
|
|
426
430
|
...(effect.dependencies ? { dependencies: effect.dependencies } : {}),
|
|
431
|
+
...(effect.dependencyExpressions ? { dependencyExpressions: effect.dependencyExpressions, dependencyStates: effect.dependencyStates } : {}),
|
|
427
432
|
...(effect.itemDependencies ? { itemDependencies: effect.itemDependencies, listState: effect.listState } : {}),
|
|
428
433
|
...(effect.cleanup ? { cleanup: true } : {}),
|
|
429
434
|
...(effect.owner ? { owner: effect.owner } : {}),
|
|
@@ -583,7 +588,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
583
588
|
const descriptor = bindingDescriptor(node)
|
|
584
589
|
const stateIds = reactiveStateIds(descriptor)
|
|
585
590
|
if (!stateIds.size) return renderNode(node.value ? node.truthy() : node.falsy(), namespace, selectValue)
|
|
586
|
-
if (namespace) throw new Error(
|
|
591
|
+
if (namespace === "math") throw new Error("Reactive conditional DOM is not supported inside math")
|
|
587
592
|
|
|
588
593
|
const id = renderContext.listRoot || renderContext.listRowRoot ? nextRowRenderId("c") : nextRenderId("c")
|
|
589
594
|
const renderBranch = async branch => {
|
|
@@ -604,14 +609,17 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
604
609
|
}
|
|
605
610
|
const owned = truthy.states.length || falsy.states.length ? { true: truthy.states, false: falsy.states } : undefined
|
|
606
611
|
const mount = Boolean(owned) || truthy.html.includes("data-k-") || falsy.html.includes("data-k-") || truthy.html.includes("<!--k-text:") || falsy.html.includes("<!--k-text:")
|
|
607
|
-
const metadata = { id, kind: node.kind, initial: node.value, ...descriptor, ...(owned ? { owned } : {}), ...(mount ? { mount: true } : {}) }
|
|
612
|
+
const metadata = { id, kind: node.kind, initial: node.value, ...descriptor, ...(namespace === "svg" ? { svg: true } : {}), ...(owned ? { owned } : {}), ...(mount ? { mount: true } : {}) }
|
|
608
613
|
for (const stateId of stateIds) renderContext.conditionStates.add(stateId)
|
|
609
614
|
renderContext.conditions.push(metadata)
|
|
610
615
|
renderContext.hasBehaviors = true
|
|
611
616
|
renderContext.hasBindings = true
|
|
612
617
|
const encoded = escapeJsonAttribute(metadata)
|
|
613
618
|
const current = node.value ? truthy.html : node.kind === "and" ? await renderNode(node.value, namespace, selectValue) : falsy.html
|
|
614
|
-
|
|
619
|
+
const branches = namespace === "svg"
|
|
620
|
+
? ` data-k-svg-true="${escapeAttribute(truthy.html)}" data-k-svg-false="${escapeAttribute(falsy.html)}"></template>`
|
|
621
|
+
: `><template data-k-true>${truthy.html}</template><template data-k-false>${falsy.html}</template></template>`
|
|
622
|
+
return `<template data-k-if='${encoded}'${branches}${current}<template data-k-if-end="${id}"></template>`
|
|
615
623
|
}
|
|
616
624
|
if (node?.[listMarker]) return renderList(node, namespace, selectValue)
|
|
617
625
|
if (node?.[listFieldMarker]) {
|
|
@@ -639,6 +647,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
639
647
|
return `<!--k-text:${id}-->${escapeHtml(node.value ?? "")}<!--k-text-end-->`
|
|
640
648
|
}
|
|
641
649
|
if (node?.[listConditionalMarker]) {
|
|
650
|
+
if (namespace === "svg") throw new Error("Keyed row conditions are not supported inside svg")
|
|
642
651
|
const descriptor = { kind: node.kind, module: node.module, handler: node.handler }
|
|
643
652
|
const owner = renderContext.listRoot ?? renderContext.listRowRoot
|
|
644
653
|
if (owner) owner.conditions = true
|
|
@@ -828,12 +837,13 @@ function sharedInitialListMarker() {
|
|
|
828
837
|
}
|
|
829
838
|
|
|
830
839
|
async function renderList(node, namespace, selectValue) {
|
|
831
|
-
if (namespace) throw new Error(
|
|
840
|
+
if (namespace === "math") throw new Error("Reactive keyed lists are not supported inside math")
|
|
841
|
+
if (namespace === "svg" && node.ownerField) throw new Error("Nested reactive keyed lists are not supported inside svg")
|
|
832
842
|
const ownerRoot = node.ownerField ? renderContext.listRoot ?? renderContext.listRowRoot : undefined
|
|
833
843
|
const ownerTemplate = Boolean(node.ownerField && renderContext.listTemplate)
|
|
834
844
|
const rowList = node.ownerField ? nextRowList() : undefined
|
|
835
845
|
const id = rowList?.id ?? nextRenderId("l")
|
|
836
|
-
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map((item, index) => node.keyField === null ? index : item[node.keyField]), ...(node.items[internalStateMarker] ? { static: true } : {}), ...(node.ownerField ? { ownerField: node.ownerField } : {}), ...(node.selector.length ? { selector: node.selector } : {}), ...(node.selectorStates.length ? { selectorStates: Object.fromEntries(node.selectorStates.map(([name, state]) => [name, state.id])) } : {}), ...(node.indexed ? { indexed: true } : {}), ...(!node.selector.length && node.keyField !== null && node.items[reducerStateMarker] ? { reducer: true } : {}) }
|
|
846
|
+
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map((item, index) => node.keyField === null ? index : item[node.keyField]), ...(namespace === "svg" ? { svg: true } : {}), ...(node.items[internalStateMarker] ? { static: true } : {}), ...(node.ownerField ? { ownerField: node.ownerField } : {}), ...(node.selector.length ? { selector: node.selector } : {}), ...(node.selectorStates.length ? { selectorStates: Object.fromEntries(node.selectorStates.map(([name, state]) => [name, state.id])) } : {}), ...(node.indexed ? { indexed: true } : {}), ...(!node.selector.length && node.keyField !== null && node.items[reducerStateMarker] ? { reducer: true } : {}) }
|
|
837
847
|
if (ownerTemplate) {
|
|
838
848
|
ownerRoot.descriptor.children ??= []
|
|
839
849
|
ownerRoot.descriptor.children.push({ id, field: node.ownerField, key: node.keyField, ...(node.selector.length ? { selector: node.selector } : {}) })
|
|
@@ -899,7 +909,8 @@ async function renderList(node, namespace, selectValue) {
|
|
|
899
909
|
renderContext.hasBehaviors = true
|
|
900
910
|
renderContext.hasLists = true
|
|
901
911
|
const prototype = node.ownerField && !ownerTemplate ? "" : template
|
|
902
|
-
|
|
912
|
+
const svgTemplate = namespace === "svg" ? ` data-k-svg-template="${escapeAttribute(prototype)}"` : ""
|
|
913
|
+
return `<template data-k-list='${escapeJsonAttribute(descriptor)}'${svgTemplate}>${namespace === "svg" ? "" : prototype}</template>${current}<template data-k-list-end="${id}"></template>`
|
|
903
914
|
} finally {
|
|
904
915
|
renderContext.listRoot = previousListRoot
|
|
905
916
|
renderContext.listRowRoot = previousListRowRoot
|
|
@@ -44,7 +44,7 @@ function mountLists(root) {
|
|
|
44
44
|
const end = findEnd(start, descriptor.id)
|
|
45
45
|
const roots = listRoots(start, end)
|
|
46
46
|
const nested = __KUDZU_NESTED_LISTS__ ? mountNestedPrototype(start, descriptor, roots) : undefined
|
|
47
|
-
const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : start
|
|
47
|
+
const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : listTemplateRoot(start, descriptor)
|
|
48
48
|
if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeGeneralRowHooks(descriptor, descriptor.keys[index], roots[index], nested?.owner)
|
|
49
49
|
const parts = listItemPartPlan(templateRoot, descriptor.nested)
|
|
50
50
|
const staticRows = __KUDZU_STATIC_COLLECTIONS__ && descriptor.static && parts.directFill ? new Map() : undefined
|
|
@@ -63,7 +63,8 @@ function mountLists(root) {
|
|
|
63
63
|
const list = {
|
|
64
64
|
start,
|
|
65
65
|
descriptor,
|
|
66
|
-
|
|
66
|
+
templateRoot,
|
|
67
|
+
...(__KUDZU_NESTED_LISTS__ && nested.childPrototypes?.size ? { childPrototypes: nested.childPrototypes } : {}),
|
|
67
68
|
parts,
|
|
68
69
|
...(__KUDZU_STATIC_COLLECTIONS__ && staticRows ? { staticRows } : {}),
|
|
69
70
|
...(__KUDZU_STATIC_COLLECTIONS__ && staticEntries ? { staticEntries, staticPositions: staticEntries.positions } : {}),
|
|
@@ -196,7 +197,7 @@ function updateList(list) {
|
|
|
196
197
|
let node = list.roots.get(token)
|
|
197
198
|
if (!node) {
|
|
198
199
|
const staticRoot = __KUDZU_STATIC_COLLECTIONS__ ? list.staticRows?.get(token)?.cloneNode(true) : undefined
|
|
199
|
-
node = staticRoot ??
|
|
200
|
+
node = staticRoot ?? list.templateRoot?.cloneNode(true)
|
|
200
201
|
if (!staticRoot && node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
201
202
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
202
203
|
node.removeAttribute("data-k-list-root")
|
|
@@ -360,7 +361,7 @@ function updateStableList(list, items) {
|
|
|
360
361
|
keys.add(token)
|
|
361
362
|
if (index < previous.length && (key !== previous[index][keyField] || appending && item !== previous[index])) stable = false
|
|
362
363
|
if (!stable || items.length === previous.length + 1 || index < previous.length) continue
|
|
363
|
-
let node =
|
|
364
|
+
let node = list.templateRoot?.cloneNode(true)
|
|
364
365
|
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
365
366
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
366
367
|
node.removeAttribute("data-k-list-root")
|
|
@@ -545,7 +546,7 @@ function updateReducerList(list, items) {
|
|
|
545
546
|
}
|
|
546
547
|
|
|
547
548
|
function addListRoot(list, { item, index = list.roots.size, key, token, value }) {
|
|
548
|
-
let node =
|
|
549
|
+
let node = list.templateRoot?.cloneNode(true)
|
|
549
550
|
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
550
551
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
551
552
|
node.removeAttribute("data-k-list-root")
|
|
@@ -862,7 +863,7 @@ function listOwner(start) {
|
|
|
862
863
|
function mountNestedPrototype(start, descriptor, roots) {
|
|
863
864
|
const owner = descriptor.ownerField ? listOwner(start) : undefined
|
|
864
865
|
const prototypeStart = owner ? childPrototypes.get(owner)?.get(descriptor.id) : undefined
|
|
865
|
-
const templateRoot = prototypeStart?.content.firstElementChild ?? start
|
|
866
|
+
const templateRoot = prototypeStart?.content.firstElementChild ?? listTemplateRoot(start, descriptor)
|
|
866
867
|
if (!templateRoot) throw new Error("Nested keyed list has no shared row prototype")
|
|
867
868
|
const prototypes = descriptor.children && new Map(descriptor.children.map(child => [child.id, findChildPrototype(templateRoot, child.id)]))
|
|
868
869
|
if (prototypes && [...prototypes.values()].some(prototype => !prototype)) throw new Error("Keyed list template has no nested row prototype")
|
|
@@ -871,6 +872,13 @@ function mountNestedPrototype(start, descriptor, roots) {
|
|
|
871
872
|
return { owner, childPrototypes: prototypes, templateRoot }
|
|
872
873
|
}
|
|
873
874
|
|
|
875
|
+
function listTemplateRoot(start, descriptor) {
|
|
876
|
+
if (!__KUDZU_SVG_LISTS__ || !descriptor.svg) return start.content.firstElementChild
|
|
877
|
+
const range = start.ownerDocument.createRange()
|
|
878
|
+
range.selectNode(start)
|
|
879
|
+
return range.createContextualFragment(start.dataset.kSvgTemplate).firstElementChild
|
|
880
|
+
}
|
|
881
|
+
|
|
874
882
|
function findChildPrototype(root, id) {
|
|
875
883
|
for (const element of root.children) {
|
|
876
884
|
if (element.matches("template[data-k-list]")) {
|