@kudzujs/core 0.5.1 → 0.5.4
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 +59 -18
- package/framework/binding-runtime.js +10 -1
- package/framework/build.mjs +392 -99
- package/framework/core.mjs +22 -8
- package/framework/native-runtime.js +16 -1
- package/framework/serialization.js +19 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -191,20 +191,22 @@ Kudzu resolves `current` when the handler reads it, so removed conditional eleme
|
|
|
191
191
|
Create a context to pass static or reactive values through component layers without prop drilling:
|
|
192
192
|
|
|
193
193
|
```tsx
|
|
194
|
-
|
|
194
|
+
type ThemeValue = { theme: string; setTheme: (theme: string) => void }
|
|
195
|
+
const ThemeContext = createContext<ThemeValue | null>(null)
|
|
195
196
|
|
|
196
197
|
function Toolbar() {
|
|
197
|
-
const
|
|
198
|
-
return
|
|
198
|
+
const value = useContext(ThemeContext)
|
|
199
|
+
if (!value) return null
|
|
200
|
+
return <button className={`theme-${value.theme}`} onClick={() => value.setTheme("light")}>{value.theme}</button>
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
function App() {
|
|
202
|
-
const [theme] = useState("dark")
|
|
203
|
-
return <ThemeContext.Provider value={theme}><Toolbar /></ThemeContext.Provider>
|
|
204
|
+
const [theme, setTheme] = useState("dark")
|
|
205
|
+
return <ThemeContext.Provider value={{ theme, setTheme }}><Toolbar /></ThemeContext.Provider>
|
|
204
206
|
}
|
|
205
207
|
```
|
|
206
208
|
|
|
207
|
-
|
|
209
|
+
Context values may contain state, setters, arrays, nested plain objects, and static serializable fields. Consumers can read reactive properties, destructure or rename them, and call setters from normal handlers. Kudzu serializes only state and setter IDs, then materializes live browser getters and batched setters; no function source, Provider tree, component tree, or hydration is shipped. The default applies outside a Provider and nested Providers resolve to independent concrete state IDs at build time. Arbitrary functions, accessors, cycles, symbols, and non-plain objects remain rejected at the browser capture boundary.
|
|
208
210
|
|
|
209
211
|
## Conditional DOM
|
|
210
212
|
|
|
@@ -227,7 +229,7 @@ Logical state persists across branch switches, while uncontrolled DOM state rese
|
|
|
227
229
|
|
|
228
230
|
Reactive conditional DOM currently targets the HTML namespace and is rejected inside SVG or MathML.
|
|
229
231
|
|
|
230
|
-
Top-level immutable JSX locals can hold static or state-dependent branches:
|
|
232
|
+
Top-level or block-scoped immutable JSX locals can hold static or state-dependent branches:
|
|
231
233
|
|
|
232
234
|
```tsx
|
|
233
235
|
const menu = open ? <MenuBar /> : <p>Menu dormant</p>
|
|
@@ -236,7 +238,22 @@ const content = open && menu
|
|
|
236
238
|
return <main>{content}</main>
|
|
237
239
|
```
|
|
238
240
|
|
|
239
|
-
Kudzu compiles the local initializer to the same bounded DOM ranges as an inline condition.
|
|
241
|
+
Kudzu compiles the local initializer to the same bounded DOM ranges as an inline condition. Terminal early returns and one adjacent exhaustive `let` assignment normalize to the same representation:
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
if (loading) return <Loading />
|
|
245
|
+
if (failed) return <ErrorView />
|
|
246
|
+
return <Content />
|
|
247
|
+
|
|
248
|
+
let view
|
|
249
|
+
if (open) view = <Menu />
|
|
250
|
+
else view = <p>Closed</p>
|
|
251
|
+
return view
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Branches may contain only the return or assignment being normalized. Effectful statements, non-exhaustive assignments, later reassignment, loops, `switch`, and `try` remain ordinary JavaScript and state-dependent render forms are rejected rather than evaluated against signal-object truthiness. Reactive branches are still both rendered into inert templates at build time.
|
|
255
|
+
|
|
256
|
+
A 1,000-component A/B build compared direct ternaries with an even mix of block locals, early returns, and exhaustive assignment. Both emitted 1,000 conditions and byte-identical runtime assets. The mixed source added 18 B gzip for three equivalent evaluator exports instead of one and built in 604 ms versus 590 ms (+2.24%).
|
|
240
257
|
|
|
241
258
|
## Keyed Lists
|
|
242
259
|
|
|
@@ -264,19 +281,32 @@ const rows = items.map(item =>
|
|
|
264
281
|
return <ul>{rows}</ul>
|
|
265
282
|
```
|
|
266
283
|
|
|
267
|
-
The root may also be a same-file row component
|
|
284
|
+
The root may also be a top-level same-file row component. Kudzu specializes each call at build time, so projected props, callback props, and simple local calculations compile to the same intrinsic list template:
|
|
268
285
|
|
|
269
286
|
```tsx
|
|
270
|
-
function ItemRow({
|
|
271
|
-
|
|
287
|
+
function ItemRow({ name, done, onRemove }: {
|
|
288
|
+
name: string
|
|
289
|
+
done: boolean
|
|
290
|
+
onRemove: () => void
|
|
291
|
+
}) {
|
|
292
|
+
const className = done ? "done" : "active"
|
|
293
|
+
return <li className={className}>
|
|
294
|
+
{name}
|
|
295
|
+
<button onClick={() => onRemove()}>Remove</button>
|
|
296
|
+
</li>
|
|
272
297
|
}
|
|
273
298
|
|
|
274
|
-
const rows = items.map(item => <ItemRow
|
|
299
|
+
const rows = items.map(item => <ItemRow
|
|
300
|
+
key={item.id}
|
|
301
|
+
name={item.name}
|
|
302
|
+
done={item.done}
|
|
303
|
+
onRemove={() => setItems(items.filter(entry => entry.id !== item.id))}
|
|
304
|
+
/>)
|
|
275
305
|
```
|
|
276
306
|
|
|
277
|
-
Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX or in one top-level immutable `const` rendered once as a JSX child. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers use direct DOM listeners and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
|
|
307
|
+
The original component remains reusable across multiple lists and ordinary JSX. No component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX or in one top-level immutable `const` rendered once as a JSX child. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers use direct DOM listeners and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
|
|
278
308
|
|
|
279
|
-
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or same-file row component, and `key={item.<field>}`.
|
|
309
|
+
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level same-file row component, and `key={item.<field>}`. Row components accept destructured projected props and top-level single-`const` calculations before one intrinsic return. A list alias may only be rendered once and cannot be read by other JavaScript. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Exported or imported row components, prop spreads/defaults/rest, children, nested item conditions, lists, or component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
280
310
|
|
|
281
311
|
## Normal JavaScript
|
|
282
312
|
|
|
@@ -357,7 +387,7 @@ Supported:
|
|
|
357
387
|
- Default, nested, and reactive context providers
|
|
358
388
|
- Controlled `value` and `checked` form properties
|
|
359
389
|
- Conditional child `&&` and ternary DOM patches
|
|
360
|
-
- Top-level
|
|
390
|
+
- Top-level and block-scoped JSX locals, terminal early returns, and exhaustive JSX assignment
|
|
361
391
|
- Direct keyed local-state lists
|
|
362
392
|
|
|
363
393
|
Not implemented yet:
|
|
@@ -398,13 +428,24 @@ The same native counter calculation was measured inline and through one relative
|
|
|
398
428
|
|
|
399
429
|
Bundling removes the helper file boundary, leaving 26 raw bytes and 18 gzip bytes for the function definition and calls. The measured call adds 0.69 µs per state update. The smaller 393 B command-only counter above uses a different optimized runtime path and is not the helper overhead baseline.
|
|
400
430
|
|
|
431
|
+
#### Context Object Cost
|
|
432
|
+
|
|
433
|
+
The same native counter was measured with local state access and through `value={{ count, setCount }}`. Context uses live object properties in both derived text and handlers, so this measures the complete recursive capture and generic binding capability.
|
|
434
|
+
|
|
435
|
+
| Kudzu variant | Files | Initial JS gzip | Total output | Clean build | Click |
|
|
436
|
+
|---|---:|---:|---:|---:|---:|
|
|
437
|
+
| Local native state | 5 | **1,890 B** | **4,064 B** | **421 ms** | **3.96 µs** |
|
|
438
|
+
| Context object | 7 | 4,991 B | 11,829 B | 441 ms | 7.54 µs |
|
|
439
|
+
|
|
440
|
+
Context adds 3,101 B gzip and 3.59 µs per update only on pages using nested reactive capture descriptors. It preserves immediate logical reads across repeated setter calls and batches DOM writes once per synchronous turn. Capability specialization removes the recursive state/setter branches from pages that do not use them.
|
|
441
|
+
|
|
401
442
|
#### Wrapper-Free Derived Text
|
|
402
443
|
|
|
403
|
-
The same object-state counter was built with the
|
|
444
|
+
The same object-state counter was built with the legacy span target and the current comment-bounded text range. Browser medians use five 20,000-update batches in each of seven fresh Chrome sessions.
|
|
404
445
|
|
|
405
446
|
| Text target | Files | JS gzip | Total output | Clean build | Update |
|
|
406
447
|
|---|---:|---:|---:|---:|---:|
|
|
407
|
-
|
|
|
448
|
+
| Legacy span target | 7 | **4,453 B** | **10,065 B** | **404 ms** | **4.83 µs** |
|
|
408
449
|
| Comment range | 7 | 4,763 B | 10,922 B | 426 ms | 5.03 µs |
|
|
409
450
|
|
|
410
451
|
The range costs 310 B gzip only on pages using derived reactive text. It removes wrapper elements and preserves authored structure across table cells, options, SVG text, selectors, and conditional remounts; ordinary attribute and condition pages tree-shake the range code entirely.
|
|
@@ -448,7 +489,7 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
|
|
|
448
489
|
| Svelte CSR | No | 12.9 KB | 33.1 KB | 828 ms | 5.8 ms | 38.9 ms | 4.0 ms | 5.9 ms | 54.6 ms |
|
|
449
490
|
| Qwik CSR | No | 22.2 KB | 64.1 KB | 594 ms | 9.1 ms | 22.2 ms | 30.8 ms | 19.0 ms | 81.1 ms |
|
|
450
491
|
|
|
451
|
-
An intrinsic-root versus row-component A/B build produced byte-for-byte identical `dist` output: 5,175 B JS gzip and 61,731 B total. Seven interleaved clean builds measured
|
|
492
|
+
An intrinsic-root versus projected-prop row-component A/B build produced byte-for-byte identical `dist` output: 5,175 B JS gzip and 61,731 B total. Seven interleaved clean builds measured 467 ms and 455 ms. Browser operation medians totaled 23.7 ms and 23.9 ms respectively; because the deployed HTML and JavaScript are identical, the 0.2 ms difference is measurement variance rather than component runtime overhead.
|
|
452
493
|
|
|
453
494
|
Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 23.2 ms, 10.7 ms behind the hand-authored Astro baseline and 7.1 ms ahead of React across all four operations.
|
|
454
495
|
|
|
@@ -229,7 +229,7 @@ async function loadEvaluator(descriptor) {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
async function createBindingContext(descriptor) {
|
|
232
|
-
const scope = Object.fromEntries(Object.entries(descriptor.scope).map(([name, value]) => [name, deserialize(value)]))
|
|
232
|
+
const scope = Object.fromEntries(Object.entries(descriptor.scope).map(([name, value]) => [name, deserialize(value, id => browserState.get(id))]))
|
|
233
233
|
const nested = {}
|
|
234
234
|
await Promise.all(Object.entries(descriptor.scopeBindings).map(async ([name, binding]) => {
|
|
235
235
|
const evaluator = await loadEvaluator(binding)
|
|
@@ -245,10 +245,19 @@ function bindingStateIds(descriptor) {
|
|
|
245
245
|
return new Set([
|
|
246
246
|
...Object.values(descriptor.states),
|
|
247
247
|
...Object.values(descriptor.scopeStates),
|
|
248
|
+
...(globalThis.__KUDZU_CAPTURE_STATE__ ? Object.values(descriptor.scope).flatMap(serializedStateIds) : []),
|
|
248
249
|
...Object.values(descriptor.scopeBindings).flatMap(binding => [...bindingStateIds(binding)])
|
|
249
250
|
])
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
function serializedStateIds(value) {
|
|
254
|
+
if (!value || typeof value !== "object") return []
|
|
255
|
+
if (value.type === "state") return [value.id]
|
|
256
|
+
if (value.type === "array") return value.value.flatMap(serializedStateIds)
|
|
257
|
+
if (value.type === "object") return value.value.flatMap(([, entry]) => serializedStateIds(entry))
|
|
258
|
+
return []
|
|
259
|
+
}
|
|
260
|
+
|
|
252
261
|
function matching(root, selector) {
|
|
253
262
|
return [...(root.matches?.(selector) ? [root] : []), ...(root.querySelectorAll?.(selector) ?? [])]
|
|
254
263
|
}
|
package/framework/build.mjs
CHANGED
|
@@ -82,6 +82,8 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
82
82
|
const nativeEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.native).map(event => event.event)))].sort()
|
|
83
83
|
const hasTextBindings = plans.some(plan => plan.bindings.some(binding => binding.target === "text"))
|
|
84
84
|
const hasListConditions = plans.some(plan => plan.lists.some(list => list.conditions))
|
|
85
|
+
const hasNestedStateCaptures = hasNestedCaptureState(plans)
|
|
86
|
+
const hasSetterCaptures = hasCaptureType(plans, "setter")
|
|
85
87
|
const nativeModules = handlerModules.filter(module => module.hasNativeHandlers).map(module => assetPath(base, `assets/${module.path}`))
|
|
86
88
|
const hasNativeHandlers = nativeModules.length > 0
|
|
87
89
|
if (behaviorCount) {
|
|
@@ -89,14 +91,20 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
89
91
|
const runtime = specializeRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), commandEvents, stateSeedCount > 0)
|
|
90
92
|
await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
|
|
91
93
|
}
|
|
92
|
-
if (bindingCount || hasNativeHandlers) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify
|
|
94
|
+
if (bindingCount || hasNativeHandlers) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify, {
|
|
95
|
+
"globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures),
|
|
96
|
+
"globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
|
|
97
|
+
})
|
|
93
98
|
if (bindingCount || listStyleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
|
|
94
99
|
if (bindingCount) {
|
|
95
100
|
const bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
|
|
96
101
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
97
102
|
.replace('"./serialization.js"', '"./kudzu-serialization.js"')
|
|
98
103
|
.replace('"./style.js"', '"./kudzu-style.js"')
|
|
99
|
-
await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify, {
|
|
104
|
+
await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify, {
|
|
105
|
+
"globalThis.__KUDZU_TEXT_BINDINGS__": String(hasTextBindings),
|
|
106
|
+
"globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures)
|
|
107
|
+
})
|
|
100
108
|
}
|
|
101
109
|
if (listCount) {
|
|
102
110
|
let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
|
|
@@ -115,7 +123,9 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
115
123
|
const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
|
|
116
124
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
117
125
|
.replace('"./serialization.js"', '"./kudzu-serialization.js"')
|
|
118
|
-
await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeNativeRuntime(nativeRuntime, nativeEvents, nativeModules), minify
|
|
126
|
+
await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeNativeRuntime(nativeRuntime, nativeEvents, nativeModules), minify, {
|
|
127
|
+
"globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
|
|
128
|
+
})
|
|
119
129
|
}
|
|
120
130
|
for (const handlerModule of handlerModules) {
|
|
121
131
|
const output = join(assetsDirectory, handlerModule.path)
|
|
@@ -171,6 +181,20 @@ function specializeNativeRuntime(source, events, modules) {
|
|
|
171
181
|
return `${imports}\n${specializeEvents(source, events).replace(/const modules = new Map\(\[[^\n]*\]\)/, `const modules = new Map([${entries}])`)}`
|
|
172
182
|
}
|
|
173
183
|
|
|
184
|
+
function hasCaptureType(value, type) {
|
|
185
|
+
if (!value || typeof value !== "object") return false
|
|
186
|
+
if (value.type === type) return true
|
|
187
|
+
return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasCaptureType(entry, type))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hasNestedCaptureState(value, insideCapture = false) {
|
|
191
|
+
if (!value || typeof value !== "object") return false
|
|
192
|
+
if (value.type === "state") return insideCapture
|
|
193
|
+
if (value.type === "array") return value.value.some(entry => hasNestedCaptureState(entry, true))
|
|
194
|
+
if (value.type === "object") return value.value.some(([, entry]) => hasNestedCaptureState(entry, true))
|
|
195
|
+
return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasNestedCaptureState(entry, false))
|
|
196
|
+
}
|
|
197
|
+
|
|
174
198
|
export function specializeRuntime(source, events, hasStateSeed) {
|
|
175
199
|
const specialized = specializeEvents(source, events)
|
|
176
200
|
if (hasStateSeed) return specialized
|
|
@@ -179,8 +203,8 @@ export function specializeRuntime(source, events, hasStateSeed) {
|
|
|
179
203
|
.replace(/^ if \(initialState\).*\n/m, "")
|
|
180
204
|
}
|
|
181
205
|
|
|
182
|
-
async function writeJavaScript(file, source, minify) {
|
|
183
|
-
const code = minify ? (await transform(source, { format: "esm", legalComments: "none", minify
|
|
206
|
+
async function writeJavaScript(file, source, minify, define) {
|
|
207
|
+
const code = minify || define ? (await transform(source, { define, format: "esm", legalComments: "none", minify, target: "es2022" })).code : source
|
|
184
208
|
await writeFile(file, code)
|
|
185
209
|
}
|
|
186
210
|
|
|
@@ -392,9 +416,13 @@ async function compile(file, sourceFiles, base) {
|
|
|
392
416
|
function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, clientImports) {
|
|
393
417
|
return context => sourceFile => {
|
|
394
418
|
const factory = context.factory
|
|
419
|
+
sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
|
|
420
|
+
ts.setParentRecursive(sourceFile, false)
|
|
395
421
|
const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
|
|
396
422
|
const settersByFunction = new Map()
|
|
397
423
|
const functions = new Map()
|
|
424
|
+
const components = new Map()
|
|
425
|
+
const contexts = new Set()
|
|
398
426
|
const jsxLocalDeclarations = new Map()
|
|
399
427
|
const jsxLocalsByFunction = new Map()
|
|
400
428
|
const listLocalDeclarations = new WeakSet()
|
|
@@ -420,14 +448,21 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
420
448
|
}
|
|
421
449
|
}
|
|
422
450
|
}
|
|
423
|
-
if (ts.isFunctionDeclaration(node) && node.name)
|
|
451
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
452
|
+
functions.set(node.name.text, node)
|
|
453
|
+
if (node.parent === sourceFile) components.set(node.name.text, { function: node, declaration: node })
|
|
454
|
+
}
|
|
424
455
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
|
|
425
456
|
functions.set(node.name.text, node.initializer)
|
|
457
|
+
if (node.parent?.parent?.parent === sourceFile) components.set(node.name.text, { function: node.initializer, declaration: node })
|
|
426
458
|
}
|
|
427
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer &&
|
|
459
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "createContext") contexts.add(node.name.text)
|
|
460
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
|
|
428
461
|
const owner = nearestFunction(node)
|
|
429
462
|
const declarations = jsxLocalDeclarations.get(owner) ?? new Map()
|
|
430
|
-
declarations.
|
|
463
|
+
const entries = declarations.get(node.name.text) ?? []
|
|
464
|
+
entries.push({ node, initializer: node.initializer })
|
|
465
|
+
declarations.set(node.name.text, entries)
|
|
431
466
|
jsxLocalDeclarations.set(owner, declarations)
|
|
432
467
|
}
|
|
433
468
|
ts.forEachChild(node, collect)
|
|
@@ -438,49 +473,135 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
438
473
|
let changed = true
|
|
439
474
|
while (changed) {
|
|
440
475
|
changed = false
|
|
441
|
-
for (const [name,
|
|
442
|
-
if (!names.has(name) && isJsxLocalValue(initializer, names)) {
|
|
476
|
+
for (const [name, entries] of declarations) {
|
|
477
|
+
if (!names.has(name) && entries.some(({ initializer }) => isJsxLocalValue(initializer, names))) {
|
|
443
478
|
names.add(name)
|
|
444
479
|
changed = true
|
|
445
480
|
}
|
|
446
481
|
}
|
|
447
482
|
}
|
|
483
|
+
for (const name of names) {
|
|
484
|
+
const entries = declarations.get(name)
|
|
485
|
+
if (entries.length > 1) {
|
|
486
|
+
const position = sourceFile.getLineAndCharacterOfPosition(entries[1].node.getStart(sourceFile))
|
|
487
|
+
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Block-scoped JSX local "${name}" must not shadow another local`)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
448
490
|
jsxLocalsByFunction.set(owner, names)
|
|
449
491
|
}
|
|
450
492
|
for (const [owner, declarations] of jsxLocalDeclarations) {
|
|
451
493
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
452
|
-
for (const [name,
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
494
|
+
for (const [name, entries] of declarations) {
|
|
495
|
+
for (const declaration of entries) {
|
|
496
|
+
const parts = keyedListParts(declaration.initializer, setters)
|
|
497
|
+
if (!parts) continue
|
|
498
|
+
const uses = []
|
|
499
|
+
const collectUses = node => {
|
|
500
|
+
if (ts.isJsxExpression(node) && node.initializer === undefined && ts.isIdentifier(node.expression) && node.expression.text === name && nearestFunction(node) === owner) uses.push(node)
|
|
501
|
+
ts.forEachChild(node, collectUses)
|
|
502
|
+
}
|
|
503
|
+
collectUses(owner.body)
|
|
504
|
+
const references = identifierReferenceCount(owner.body, name)
|
|
505
|
+
const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
|
|
506
|
+
if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
|
|
507
|
+
if (references !== uses.length) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" may only be used as a JSX child`)
|
|
508
|
+
listLocalDeclarations.add(declaration.node)
|
|
509
|
+
if (uses.length) listLocalUses.set(uses[0], parts)
|
|
459
510
|
}
|
|
460
|
-
collectUses(owner.body)
|
|
461
|
-
const references = identifierReferenceCount(owner.body, name)
|
|
462
|
-
const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
|
|
463
|
-
if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
|
|
464
|
-
if (references !== uses.length) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" may only be used as a JSX child`)
|
|
465
|
-
listLocalDeclarations.add(declaration.node)
|
|
466
|
-
if (uses.length) listLocalUses.set(uses[0], parts)
|
|
467
511
|
}
|
|
468
512
|
}
|
|
469
|
-
const
|
|
513
|
+
const rawRenderedLists = []
|
|
470
514
|
const collectRenderedLists = node => {
|
|
471
515
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
472
516
|
const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction))
|
|
473
|
-
if (parts) {
|
|
474
|
-
if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
|
|
475
|
-
validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, functions)
|
|
476
|
-
renderedLists.set(node, parts)
|
|
477
|
-
}
|
|
517
|
+
if (parts) rawRenderedLists.push({ node, parts })
|
|
478
518
|
}
|
|
479
519
|
ts.forEachChild(node, collectRenderedLists)
|
|
480
520
|
}
|
|
481
521
|
collectRenderedLists(sourceFile)
|
|
522
|
+
const fail = (node, message) => {
|
|
523
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
524
|
+
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
|
|
525
|
+
}
|
|
526
|
+
const rejectUnsupportedRenderControl = node => {
|
|
527
|
+
if (ts.isIfStatement(node) && containsRenderControl(node, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
|
|
528
|
+
const setters = settersForNode(node, settersByFunction)
|
|
529
|
+
if (referencedStateNames(node.expression, setters).size) {
|
|
530
|
+
fail(node, "Reactive render if statements must use terminal returns or exhaustive adjacent JSX assignment")
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
ts.forEachChild(node, rejectUnsupportedRenderControl)
|
|
534
|
+
}
|
|
535
|
+
rejectUnsupportedRenderControl(sourceFile)
|
|
536
|
+
const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
|
|
537
|
+
const tag = jsxTagName(parts.root)
|
|
538
|
+
return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
|
|
539
|
+
}))
|
|
540
|
+
const componentSpecializations = new WeakMap()
|
|
541
|
+
const specializedDeclarations = new WeakSet()
|
|
542
|
+
for (const name of listComponentNames) {
|
|
543
|
+
const component = components.get(name)
|
|
544
|
+
if (!component) fail(sourceFile, `Keyed list component ${name} must be declared at the top level in the same file`)
|
|
545
|
+
if (isExportedDeclaration(component.declaration)) fail(component.declaration, `Keyed list component ${name} cannot be exported`)
|
|
546
|
+
const calls = jsxTagUses(sourceFile, name)
|
|
547
|
+
if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
|
|
548
|
+
for (const call of calls) componentSpecializations.set(call, specializeComponentCall(call, component.function, sourceFile, factory, context, fail))
|
|
549
|
+
specializedDeclarations.add(component.declaration)
|
|
550
|
+
}
|
|
551
|
+
const renderedLists = new WeakMap()
|
|
552
|
+
for (const { node, parts: originalParts } of rawRenderedLists) {
|
|
553
|
+
if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
|
|
554
|
+
const specialization = componentSpecializations.get(originalParts.root)
|
|
555
|
+
const root = specialization?.root ?? originalParts.root
|
|
556
|
+
const callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
|
|
557
|
+
originalParts.callback,
|
|
558
|
+
originalParts.callback.modifiers,
|
|
559
|
+
originalParts.callback.typeParameters,
|
|
560
|
+
originalParts.callback.parameters,
|
|
561
|
+
originalParts.callback.type,
|
|
562
|
+
originalParts.callback.equalsGreaterThanToken,
|
|
563
|
+
root
|
|
564
|
+
)
|
|
565
|
+
if (callback !== originalParts.callback) {
|
|
566
|
+
ts.setParentRecursive(callback, false)
|
|
567
|
+
callback.parent = originalParts.callback.parent
|
|
568
|
+
}
|
|
569
|
+
const parts = { ...originalParts, root, callback }
|
|
570
|
+
for (const calculation of specialization?.calculations ?? []) {
|
|
571
|
+
ts.setParentRecursive(calculation, false)
|
|
572
|
+
calculation.parent = callback
|
|
573
|
+
validateListExpression(calculation, parts.item, originalParts.root, fail)
|
|
574
|
+
}
|
|
575
|
+
validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions)
|
|
576
|
+
renderedLists.set(node, parts)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const compileRenderExpression = (expression, anchor) => {
|
|
580
|
+
const parts = conditionalParts(expression)
|
|
581
|
+
if (!parts) return ts.visitNode(expression, visitor)
|
|
582
|
+
const setters = settersForNode(anchor, settersByFunction)
|
|
583
|
+
const usedStates = referencedStateNames(parts.condition, setters)
|
|
584
|
+
const captures = captureNames(parts.condition, parts.condition, setters)
|
|
585
|
+
if (!usedStates.size && !captures.size) return ts.visitEachChild(expression, visitor, context)
|
|
586
|
+
usesBehavior = true
|
|
587
|
+
usesConditional = true
|
|
588
|
+
return compileConditional(
|
|
589
|
+
parts.kind,
|
|
590
|
+
parts.condition,
|
|
591
|
+
compileRenderExpression(parts.truthy, anchor),
|
|
592
|
+
compileRenderExpression(parts.falsy, anchor),
|
|
593
|
+
setters,
|
|
594
|
+
factory,
|
|
595
|
+
context,
|
|
596
|
+
reactiveBindings,
|
|
597
|
+
handlerUrl
|
|
598
|
+
)
|
|
599
|
+
}
|
|
482
600
|
|
|
483
601
|
const visitor = node => {
|
|
602
|
+
if (specializedDeclarations.has(node)) return node
|
|
603
|
+
if (componentSpecializations.has(node)) return ts.visitNode(componentSpecializations.get(node).root, visitor)
|
|
604
|
+
|
|
484
605
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
|
|
485
606
|
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(modulePath(node.moduleSpecifier.text)), node.attributes)
|
|
486
607
|
}
|
|
@@ -504,18 +625,13 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
504
625
|
}
|
|
505
626
|
|
|
506
627
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && jsxLocalsByFunction.get(nearestFunction(node))?.has(node.name.text) && referencesIdentifier(nearestFunction(node).body, node.name.text)) {
|
|
507
|
-
const
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
usesConditional = true
|
|
515
|
-
const compiled = compileConditional(parts.kind, parts.condition, ts.visitNode(parts.truthy, visitor), ts.visitNode(parts.falsy, visitor), setters, factory, context, reactiveBindings, handlerUrl)
|
|
516
|
-
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, compiled)
|
|
517
|
-
}
|
|
518
|
-
}
|
|
628
|
+
const compiled = compileRenderExpression(node.initializer, node)
|
|
629
|
+
if (compiled !== node.initializer) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, compiled)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
|
|
633
|
+
const compiled = compileRenderExpression(node.expression, node)
|
|
634
|
+
if (compiled !== node.expression) return factory.updateReturnStatement(node, compiled)
|
|
519
635
|
}
|
|
520
636
|
|
|
521
637
|
if (ts.isJsxExpression(node) && node.expression && listConditions.has(node.expression)) {
|
|
@@ -546,19 +662,10 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
546
662
|
ts.visitNode(listParts.callback, visitor)
|
|
547
663
|
]))
|
|
548
664
|
}
|
|
549
|
-
const
|
|
550
|
-
if (
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
const captures = captureNames(parts.condition, parts.condition, setters)
|
|
554
|
-
if (usedStates.size || captures.size) {
|
|
555
|
-
usesBehavior = true
|
|
556
|
-
usesConditional = true
|
|
557
|
-
const truthy = ts.visitNode(parts.truthy, visitor)
|
|
558
|
-
const falsy = ts.visitNode(parts.falsy, visitor)
|
|
559
|
-
const compiled = compileConditional(parts.kind, parts.condition, truthy, falsy, setters, factory, context, reactiveBindings, handlerUrl)
|
|
560
|
-
return factory.updateJsxExpression(node, compiled)
|
|
561
|
-
}
|
|
665
|
+
const conditional = conditionalParts(node.expression)
|
|
666
|
+
if (conditional) {
|
|
667
|
+
const compiled = compileRenderExpression(node.expression, node)
|
|
668
|
+
if (compiled !== node.expression) return factory.updateJsxExpression(node, compiled)
|
|
562
669
|
}
|
|
563
670
|
const setters = settersForNode(node, settersByFunction)
|
|
564
671
|
const usedStates = referencedStateNames(node.expression, setters)
|
|
@@ -570,7 +677,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
570
677
|
}
|
|
571
678
|
}
|
|
572
679
|
|
|
573
|
-
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !/^on/i.test(node.name.getText()) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.getText().toLowerCase())) {
|
|
680
|
+
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !isContextProviderValue(node, contexts) && !/^on/i.test(node.name.getText()) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.getText().toLowerCase())) {
|
|
574
681
|
const expression = node.initializer.expression
|
|
575
682
|
const setters = settersForNode(node, settersByFunction)
|
|
576
683
|
const usedStates = referencedStateNames(expression, setters)
|
|
@@ -621,6 +728,112 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
621
728
|
}
|
|
622
729
|
}
|
|
623
730
|
|
|
731
|
+
function normalizeRenderControlFlow(sourceFile, factory, context) {
|
|
732
|
+
const normalizeStatements = statements => {
|
|
733
|
+
const nested = statements.map(statement => ts.visitEachChild(statement, visitNested, context))
|
|
734
|
+
const assigned = []
|
|
735
|
+
for (let index = 0; index < nested.length; index++) {
|
|
736
|
+
const statement = nested[index]
|
|
737
|
+
const next = nested[index + 1]
|
|
738
|
+
const declaration = singleUninitializedLet(statement)
|
|
739
|
+
const assignment = declaration && next && assignmentConditional(next, declaration.name.text, factory)
|
|
740
|
+
if (declaration && assignment) {
|
|
741
|
+
const updated = factory.updateVariableDeclaration(declaration, declaration.name, declaration.exclamationToken, declaration.type, assignment)
|
|
742
|
+
const list = factory.createVariableDeclarationList([updated], ts.NodeFlags.Const)
|
|
743
|
+
assigned.push(factory.updateVariableStatement(statement, statement.modifiers, list))
|
|
744
|
+
index++
|
|
745
|
+
} else {
|
|
746
|
+
assigned.push(statement)
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (!assigned.length) return assigned
|
|
751
|
+
const finalIf = returnConditional(assigned.at(-1), factory)
|
|
752
|
+
if (finalIf) return [...assigned.slice(0, -1), factory.createReturnStatement(finalIf)]
|
|
753
|
+
if (!ts.isReturnStatement(assigned.at(-1)) || !assigned.at(-1).expression) return assigned
|
|
754
|
+
let expression = assigned.at(-1).expression
|
|
755
|
+
let start = assigned.length - 1
|
|
756
|
+
while (start > 0) {
|
|
757
|
+
const previous = assigned[start - 1]
|
|
758
|
+
if (!ts.isIfStatement(previous) || previous.elseStatement) break
|
|
759
|
+
const truthy = returnOnlyExpression(previous.thenStatement)
|
|
760
|
+
if (!truthy) break
|
|
761
|
+
expression = factory.createConditionalExpression(previous.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), expression)
|
|
762
|
+
start--
|
|
763
|
+
}
|
|
764
|
+
return start === assigned.length - 1 ? assigned : [...assigned.slice(0, start), factory.createReturnStatement(expression)]
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const visitNested = node => {
|
|
768
|
+
if (ts.isBlock(node)) return factory.updateBlock(node, normalizeStatements([...node.statements]))
|
|
769
|
+
if (isFunctionLike(node) && ts.isBlock(node.body)) {
|
|
770
|
+
if (!isRenderFunction(node)) return node
|
|
771
|
+
const body = factory.updateBlock(node.body, normalizeStatements([...node.body.statements]))
|
|
772
|
+
if (ts.isFunctionDeclaration(node)) return factory.updateFunctionDeclaration(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
|
|
773
|
+
if (ts.isFunctionExpression(node)) return factory.updateFunctionExpression(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
|
|
774
|
+
if (ts.isArrowFunction(node)) return factory.updateArrowFunction(node, node.modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, body)
|
|
775
|
+
}
|
|
776
|
+
return ts.visitEachChild(node, visitNested, context)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return ts.visitEachChild(sourceFile, visitNested, context)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function isRenderFunction(node) {
|
|
783
|
+
if (ts.isFunctionDeclaration(node)) return node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) || Boolean(node.name && /^[A-Z]/.test(node.name.text))
|
|
784
|
+
const declaration = node.parent
|
|
785
|
+
return ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && /^[A-Z]/.test(declaration.name.text)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function singleUninitializedLet(statement) {
|
|
789
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Let) === 0 || statement.declarationList.declarations.length !== 1) return undefined
|
|
790
|
+
const declaration = statement.declarationList.declarations[0]
|
|
791
|
+
return ts.isIdentifier(declaration.name) && !declaration.initializer ? declaration : undefined
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function assignmentConditional(statement, name, factory) {
|
|
795
|
+
if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
|
|
796
|
+
const truthy = assignmentOnlyExpression(statement.thenStatement, name)
|
|
797
|
+
const falsy = ts.isIfStatement(statement.elseStatement)
|
|
798
|
+
? assignmentConditional(statement.elseStatement, name, factory)
|
|
799
|
+
: assignmentOnlyExpression(statement.elseStatement, name)
|
|
800
|
+
if (!truthy || !falsy) return undefined
|
|
801
|
+
return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function assignmentOnlyExpression(statement, name) {
|
|
805
|
+
const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
|
|
806
|
+
if (!ts.isExpressionStatement(candidate) || !ts.isBinaryExpression(candidate.expression) || candidate.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(candidate.expression.left) || candidate.expression.left.text !== name) return undefined
|
|
807
|
+
return candidate.expression.right
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function returnConditional(statement, factory) {
|
|
811
|
+
if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
|
|
812
|
+
const truthy = returnOnlyExpression(statement.thenStatement)
|
|
813
|
+
const falsy = ts.isIfStatement(statement.elseStatement)
|
|
814
|
+
? returnConditional(statement.elseStatement, factory)
|
|
815
|
+
: returnOnlyExpression(statement.elseStatement)
|
|
816
|
+
if (!truthy || !falsy) return undefined
|
|
817
|
+
return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function returnOnlyExpression(statement) {
|
|
821
|
+
const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
|
|
822
|
+
return ts.isReturnStatement(candidate) && candidate.expression ? candidate.expression : undefined
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function containsRenderControl(root, knownLocals) {
|
|
826
|
+
let found = false
|
|
827
|
+
const visit = node => {
|
|
828
|
+
if (isFunctionLike(node) && node !== root) return
|
|
829
|
+
if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, knownLocals)) found = true
|
|
830
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && containsJsx(node.right)) found = true
|
|
831
|
+
if (!found) ts.forEachChild(node, visit)
|
|
832
|
+
}
|
|
833
|
+
visit(root)
|
|
834
|
+
return found
|
|
835
|
+
}
|
|
836
|
+
|
|
624
837
|
function keyedListParts(expression, setters) {
|
|
625
838
|
const value = unwrapExpression(expression)
|
|
626
839
|
if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map" || !ts.isIdentifier(value.expression.expression)) return undefined
|
|
@@ -639,35 +852,13 @@ function keyedListParts(expression, setters) {
|
|
|
639
852
|
return { state, callback, root, item: callback.parameters[0].name.text, keyField: field }
|
|
640
853
|
}
|
|
641
854
|
|
|
642
|
-
function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions
|
|
855
|
+
function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions) {
|
|
643
856
|
const fail = (node, message) => {
|
|
644
857
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
645
858
|
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
|
|
646
859
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const rootTag = ts.isJsxElement(root) ? root.openingElement.tagName : root.tagName
|
|
650
|
-
if (ts.isIdentifier(rootTag) && rootTag.text[0] === rootTag.text[0].toUpperCase()) {
|
|
651
|
-
const component = functions.get(rootTag.text)
|
|
652
|
-
if (!component) fail(root, `Keyed list component ${rootTag.text} must be declared in the same file`)
|
|
653
|
-
const uses = jsxTagUses(sourceFile, rootTag.text)
|
|
654
|
-
if (uses.length !== 1 || uses[0] !== root) fail(root, `Keyed list component ${rootTag.text} may only be used as this list root`)
|
|
655
|
-
const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
656
|
-
let itemProp
|
|
657
|
-
for (const attribute of attributes.properties) {
|
|
658
|
-
if (ts.isJsxSpreadAttribute(attribute)) {
|
|
659
|
-
if (referencesIdentifier(attribute.expression, item)) fail(attribute, "Keyed list item spreads are not supported")
|
|
660
|
-
continue
|
|
661
|
-
}
|
|
662
|
-
if (attribute.name.getText() === "key" || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression || !referencesIdentifier(attribute.initializer.expression, item)) continue
|
|
663
|
-
if (!ts.isIdentifier(attribute.initializer.expression) || attribute.initializer.expression.text !== item || itemProp) fail(attribute, `Keyed list component ${rootTag.text} must receive the whole item through one direct prop`)
|
|
664
|
-
itemProp = attribute.name.getText()
|
|
665
|
-
}
|
|
666
|
-
if (ts.isJsxElement(root) && root.children.some(child => referencesIdentifier(child, item))) fail(root, `Keyed list component ${rootTag.text} must receive the whole item through one direct prop`)
|
|
667
|
-
if (!itemProp) fail(root, `Keyed list component ${rootTag.text} must receive the whole item through one direct prop`)
|
|
668
|
-
item = componentItemParameter(component, itemProp, node => fail(node, `Keyed list component ${rootTag.text} must destructure its item prop`))
|
|
669
|
-
root = componentJsxRoot(component, node => fail(node, `Keyed list component ${rootTag.text} must return one JSX element`))
|
|
670
|
-
}
|
|
860
|
+
const root = parts.root
|
|
861
|
+
const item = parts.item
|
|
671
862
|
const validateElement = node => {
|
|
672
863
|
const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
|
|
673
864
|
if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
|
|
@@ -717,24 +908,127 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
717
908
|
visit(root)
|
|
718
909
|
}
|
|
719
910
|
|
|
720
|
-
function
|
|
721
|
-
if (component.
|
|
722
|
-
|
|
723
|
-
if (
|
|
724
|
-
|
|
725
|
-
|
|
911
|
+
function specializeComponentCall(call, component, sourceFile, factory, context, fail) {
|
|
912
|
+
if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, "Keyed list components must be synchronous")
|
|
913
|
+
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, "Keyed list components must use one destructured props parameter")
|
|
914
|
+
if (ts.isJsxElement(call) && call.children.some(child => !ts.isJsxText(child) || child.text.trim())) fail(call, "Keyed list component children are not supported")
|
|
915
|
+
const callAttributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
916
|
+
const props = new Map()
|
|
917
|
+
let key
|
|
918
|
+
for (const attribute of callAttributes.properties) {
|
|
919
|
+
if (ts.isJsxSpreadAttribute(attribute)) fail(attribute, "Keyed list component prop spreads are not supported")
|
|
920
|
+
const name = attribute.name.getText()
|
|
921
|
+
if (props.has(name) || name === "key" && key) fail(attribute, `Duplicate keyed list component prop "${name}"`)
|
|
922
|
+
const value = !attribute.initializer
|
|
923
|
+
? factory.createTrue()
|
|
924
|
+
: ts.isStringLiteral(attribute.initializer)
|
|
925
|
+
? factory.createStringLiteral(attribute.initializer.text)
|
|
926
|
+
: ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression
|
|
927
|
+
? attribute.initializer.expression
|
|
928
|
+
: factory.createIdentifier("undefined")
|
|
929
|
+
if (name === "key") key = attribute
|
|
930
|
+
else props.set(name, value)
|
|
931
|
+
}
|
|
932
|
+
const substitutions = new Map()
|
|
933
|
+
const acceptedProps = new Set()
|
|
934
|
+
for (const element of component.parameters[0].name.elements) {
|
|
935
|
+
if (element.dotDotDotToken || element.initializer || !ts.isIdentifier(element.name)) fail(element, "Keyed list component props cannot use rest, defaults, or nested destructuring")
|
|
936
|
+
const prop = (element.propertyName ?? element.name).getText()
|
|
937
|
+
acceptedProps.add(prop)
|
|
938
|
+
substitutions.set(element.name.text, props.get(prop) ?? factory.createIdentifier("undefined"))
|
|
939
|
+
}
|
|
940
|
+
for (const prop of props.keys()) if (!acceptedProps.has(prop)) fail(call, `Unknown keyed list component prop "${prop}"`)
|
|
726
941
|
|
|
727
|
-
|
|
942
|
+
let returned
|
|
943
|
+
const calculations = []
|
|
728
944
|
if (!ts.isBlock(component.body)) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
945
|
+
returned = component.body
|
|
946
|
+
} else {
|
|
947
|
+
const statements = [...component.body.statements]
|
|
948
|
+
const last = statements.pop()
|
|
949
|
+
if (!last || !ts.isReturnStatement(last) || !last.expression) fail(component.body, "Keyed list component must end with one JSX return")
|
|
950
|
+
for (const statement of statements) {
|
|
951
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.declarationList.declarations.length !== 1) fail(statement, "Keyed list component locals must be single const declarations")
|
|
952
|
+
const declaration = statement.declarationList.declarations[0]
|
|
953
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, "Keyed list component locals must be initialized identifiers")
|
|
954
|
+
const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
|
|
955
|
+
calculations.push(calculation)
|
|
956
|
+
substitutions.set(declaration.name.text, calculation)
|
|
957
|
+
}
|
|
958
|
+
returned = last.expression
|
|
959
|
+
}
|
|
960
|
+
let root = unwrapExpression(substituteClone(returned, substitutions, factory, context))
|
|
961
|
+
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, "Keyed list component must return one JSX element")
|
|
962
|
+
const tag = jsxTagName(root)
|
|
963
|
+
if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, "Keyed list component must directly return an intrinsic JSX element")
|
|
964
|
+
const rootAttributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
965
|
+
if (rootAttributes.properties.some(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "key")) fail(root, "Keyed list component intrinsic root cannot declare key")
|
|
966
|
+
if (key) root = addJsxAttribute(root, cloneAst(key, factory, context), factory)
|
|
967
|
+
ts.setParentRecursive(root, false)
|
|
968
|
+
root.parent = call.parent
|
|
969
|
+
return { root, calculations }
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function substituteClone(root, substitutions, factory, context) {
|
|
973
|
+
const visit = (node, shadowed = new Set()) => {
|
|
974
|
+
if (ts.isShorthandPropertyAssignment(node) && substitutions.has(node.name.text) && !shadowed.has(node.name.text)) {
|
|
975
|
+
return factory.createPropertyAssignment(cloneAst(node.name, factory, context), cloneAst(substitutions.get(node.name.text), factory, context))
|
|
976
|
+
}
|
|
977
|
+
if (ts.isIdentifier(node) && substitutions.has(node.text) && !shadowed.has(node.text) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node)) {
|
|
978
|
+
return cloneAst(substitutions.get(node.text), factory, context)
|
|
979
|
+
}
|
|
980
|
+
const nextShadowed = isFunctionLike(node)
|
|
981
|
+
? new Set([...shadowed, ...node.parameters.flatMap(parameter => bindingNames(parameter.name))])
|
|
982
|
+
: shadowed
|
|
983
|
+
const clone = factory.cloneNode(node)
|
|
984
|
+
ts.setTextRange(clone, node)
|
|
985
|
+
ts.setOriginalNode(clone, node)
|
|
986
|
+
return ts.visitEachChild(clone, child => visit(child, nextShadowed), context)
|
|
987
|
+
}
|
|
988
|
+
return visit(root)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function cloneAst(root, factory, context) {
|
|
992
|
+
const visit = node => {
|
|
993
|
+
const clone = factory.cloneNode(node)
|
|
994
|
+
ts.setTextRange(clone, node)
|
|
995
|
+
ts.setOriginalNode(clone, node)
|
|
996
|
+
return ts.visitEachChild(clone, visit, context)
|
|
997
|
+
}
|
|
998
|
+
return visit(root)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function addJsxAttribute(root, attribute, factory) {
|
|
1002
|
+
if (ts.isJsxSelfClosingElement(root)) {
|
|
1003
|
+
return factory.updateJsxSelfClosingElement(root, root.tagName, root.typeArguments, factory.updateJsxAttributes(root.attributes, [attribute, ...root.attributes.properties]))
|
|
732
1004
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1005
|
+
const opening = factory.updateJsxOpeningElement(root.openingElement, root.openingElement.tagName, root.openingElement.typeArguments, factory.updateJsxAttributes(root.openingElement.attributes, [attribute, ...root.openingElement.attributes.properties]))
|
|
1006
|
+
return factory.updateJsxElement(root, opening, root.children, root.closingElement)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function jsxTagName(node) {
|
|
1010
|
+
return ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function isContextProviderValue(node, contexts) {
|
|
1014
|
+
if (node.name.getText() !== "value") return false
|
|
1015
|
+
const element = node.parent?.parent
|
|
1016
|
+
const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
|
|
1017
|
+
return ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && contexts.has(tag.expression.text)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function isJsxSyntaxIdentifier(node) {
|
|
1021
|
+
const parent = node.parent
|
|
1022
|
+
return (ts.isJsxOpeningElement(parent) || ts.isJsxClosingElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node || ts.isJsxAttribute(parent) && parent.name === node
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function isFunctionLike(node) {
|
|
1026
|
+
return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function isExportedDeclaration(node) {
|
|
1030
|
+
const statement = ts.isVariableDeclaration(node) ? node.parent?.parent : node
|
|
1031
|
+
return statement?.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
|
|
738
1032
|
}
|
|
739
1033
|
|
|
740
1034
|
function jsxTagUses(root, name) {
|
|
@@ -870,11 +1164,10 @@ function unwrapExpression(node) {
|
|
|
870
1164
|
return ts.isParenthesizedExpression(node) ? unwrapExpression(node.expression) : node
|
|
871
1165
|
}
|
|
872
1166
|
|
|
873
|
-
function
|
|
1167
|
+
function isLocalConst(node) {
|
|
874
1168
|
const list = node.parent
|
|
875
1169
|
const statement = list?.parent
|
|
876
|
-
|
|
877
|
-
return ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 && ts.isVariableStatement(statement) && statement.parent === owner?.body
|
|
1170
|
+
return ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 && ts.isVariableStatement(statement)
|
|
878
1171
|
}
|
|
879
1172
|
|
|
880
1173
|
function isJsxLocalValue(expression, known) {
|
package/framework/core.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { serializeStyle } from "./style.js"
|
|
2
2
|
|
|
3
3
|
const signalMarker = Symbol("kudzu.signal")
|
|
4
|
+
const setterMarker = Symbol("kudzu.setter")
|
|
4
5
|
const behaviorMarker = Symbol("kudzu.behavior")
|
|
5
6
|
const nativeBehaviorMarker = Symbol("kudzu.nativeBehavior")
|
|
6
7
|
const bindingMarker = Symbol("kudzu.binding")
|
|
@@ -35,10 +36,12 @@ export function useState(initialValue, name) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
return [signal, () => {
|
|
39
|
+
const setter = () => {
|
|
40
40
|
throw new Error("State setters are compiled into ordered browser behaviors")
|
|
41
|
-
}
|
|
41
|
+
}
|
|
42
|
+
Object.defineProperty(setter, setterMarker, { value: id })
|
|
43
|
+
renderContext.states[id] = { name: name ?? id, initialValue }
|
|
44
|
+
return [signal, setter]
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export function useRef(initialValue) {
|
|
@@ -190,6 +193,8 @@ function bindingDescriptor(value) {
|
|
|
190
193
|
function serializeCapture(name, value, seen) {
|
|
191
194
|
if (value?.[listItemMarker]) return { type: "list-item" }
|
|
192
195
|
if (value?.[refMarker]) return { type: "ref", id: value.id }
|
|
196
|
+
if (value?.[signalMarker]) return { type: "state", id: value.id }
|
|
197
|
+
if (typeof value === "function" && value[setterMarker]) return { type: "setter", id: value[setterMarker] }
|
|
193
198
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value
|
|
194
199
|
if (typeof value === "number") {
|
|
195
200
|
return Number.isFinite(value) && !Object.is(value, -0) ? value : { type: "number", value: String(value) }
|
|
@@ -375,7 +380,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
375
380
|
}
|
|
376
381
|
if (node?.[bindingMarker]) {
|
|
377
382
|
const descriptor = bindingDescriptor(node)
|
|
378
|
-
const reactive =
|
|
383
|
+
const reactive = reactiveStateIds(descriptor).size > 0
|
|
379
384
|
if (!reactive) return renderNode(node.value, namespace, selectValue)
|
|
380
385
|
renderContext.bindings.push({ target: "text", ...descriptor })
|
|
381
386
|
if (renderContext.conditionDepth || renderContext.listDepth) for (const stateId of reactiveStateIds(descriptor)) renderContext.conditionStates.add(stateId)
|
|
@@ -492,14 +497,14 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
492
497
|
}
|
|
493
498
|
if (value?.[signalMarker] || value?.[bindingMarker]) {
|
|
494
499
|
const initialValue = value[signalMarker] ? value.value : value.value
|
|
495
|
-
const
|
|
500
|
+
const descriptor = value[signalMarker]
|
|
501
|
+
? { state: value.id }
|
|
502
|
+
: bindingDescriptor(value)
|
|
503
|
+
const reactive = reactiveStateIds(descriptor).size > 0
|
|
496
504
|
if (!reactive) {
|
|
497
505
|
if (tag !== "select" || name !== "value") attributes += renderAttribute(name, initialValue)
|
|
498
506
|
continue
|
|
499
507
|
}
|
|
500
|
-
const descriptor = value[signalMarker]
|
|
501
|
-
? { state: value.id }
|
|
502
|
-
: bindingDescriptor(value)
|
|
503
508
|
if (tag !== "select" || name !== "value") attributes += renderAttribute(name, initialValue)
|
|
504
509
|
if (propertyTarget) attributes += ` data-k-bind-${name}='${escapeJsonAttribute(descriptor)}'`
|
|
505
510
|
else attributeBindings.push({ target: name, ...descriptor })
|
|
@@ -587,10 +592,19 @@ function reactiveStateIds(descriptor) {
|
|
|
587
592
|
return new Set([
|
|
588
593
|
...Object.values(descriptor.states),
|
|
589
594
|
...Object.values(descriptor.scopeStates),
|
|
595
|
+
...Object.values(descriptor.scope).flatMap(serializedStateIds),
|
|
590
596
|
...Object.values(descriptor.scopeBindings).flatMap(entry => [...reactiveStateIds(entry)])
|
|
591
597
|
])
|
|
592
598
|
}
|
|
593
599
|
|
|
600
|
+
function serializedStateIds(value) {
|
|
601
|
+
if (!value || typeof value !== "object") return []
|
|
602
|
+
if (value.type === "state") return [value.id]
|
|
603
|
+
if (value.type === "array") return value.value.flatMap(serializedStateIds)
|
|
604
|
+
if (value.type === "object") return value.value.flatMap(([, entry]) => serializedStateIds(entry))
|
|
605
|
+
return []
|
|
606
|
+
}
|
|
607
|
+
|
|
594
608
|
function renderAttribute(name, value) {
|
|
595
609
|
if (name === "style") {
|
|
596
610
|
const style = serializeStyle(value)
|
|
@@ -6,7 +6,6 @@ const registrations = new WeakMap()
|
|
|
6
6
|
export function createNativeContext(state, stateIds, commit, serializedScope = {}) {
|
|
7
7
|
const changed = new Set()
|
|
8
8
|
let scheduled = false
|
|
9
|
-
const scope = Object.fromEntries(Object.entries(serializedScope).map(([name, value]) => [name, deserialize(value)]))
|
|
10
9
|
|
|
11
10
|
const flush = () => {
|
|
12
11
|
scheduled = false
|
|
@@ -15,6 +14,18 @@ export function createNativeContext(state, stateIds, commit, serializedScope = {
|
|
|
15
14
|
for (const id of ids) commit(id, state.get(id))
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
const setId = globalThis.__KUDZU_CAPTURE_SETTER__ ? (id, value) => {
|
|
18
|
+
const current = state.get(id)
|
|
19
|
+
state.set(id, typeof value === "function" ? value(current) : value)
|
|
20
|
+
changed.add(id)
|
|
21
|
+
if (!scheduled) {
|
|
22
|
+
scheduled = true
|
|
23
|
+
queueMicrotask(flush)
|
|
24
|
+
}
|
|
25
|
+
} : undefined
|
|
26
|
+
|
|
27
|
+
const scope = Object.fromEntries(Object.entries(serializedScope).map(([name, value]) => [name, deserialize(value, id => state.get(id), globalThis.__KUDZU_CAPTURE_SETTER__ ? setId : undefined)]))
|
|
28
|
+
|
|
18
29
|
return {
|
|
19
30
|
get(name) {
|
|
20
31
|
return state.get(stateIds[name])
|
|
@@ -23,6 +34,10 @@ export function createNativeContext(state, stateIds, commit, serializedScope = {
|
|
|
23
34
|
return serializedScope[name]?.type === "state" ? state.get(serializedScope[name].id) : scope[name]
|
|
24
35
|
},
|
|
25
36
|
set(name, value) {
|
|
37
|
+
if (globalThis.__KUDZU_CAPTURE_SETTER__) {
|
|
38
|
+
setId(stateIds[name], value)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
26
41
|
const id = stateIds[name]
|
|
27
42
|
const current = state.get(id)
|
|
28
43
|
state.set(id, typeof value === "function" ? value(current) : value)
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
export function deserialize(value) {
|
|
1
|
+
export function deserialize(value, getState, setState) {
|
|
2
2
|
if (!value || typeof value !== "object") return value
|
|
3
3
|
if (value.type === "undefined") return undefined
|
|
4
4
|
if (value.type === "number") return value.value === "NaN" ? NaN : value.value === "Infinity" ? Infinity : value.value === "-Infinity" ? -Infinity : -0
|
|
5
5
|
if (value.type === "ref") return { get current() { return typeof document === "undefined" ? null : document.querySelector(`[data-k-ref="${value.id}"]`) } }
|
|
6
|
-
if (value.type === "
|
|
6
|
+
if (globalThis.__KUDZU_CAPTURE_STATE__ && value.type === "state") return getState?.(value.id)
|
|
7
|
+
if (globalThis.__KUDZU_CAPTURE_SETTER__ && value.type === "setter") return next => {
|
|
8
|
+
if (!setState) throw new Error("Captured state setter is not available in this context")
|
|
9
|
+
setState(value.id, next)
|
|
10
|
+
}
|
|
11
|
+
if (value.type === "array") {
|
|
12
|
+
const array = []
|
|
13
|
+
for (const [index, entry] of value.value.entries()) defineCapture(array, String(index), entry, getState, setState)
|
|
14
|
+
return array
|
|
15
|
+
}
|
|
7
16
|
if (value.type === "object") {
|
|
8
17
|
const object = value.nullPrototype ? Object.create(null) : {}
|
|
9
|
-
for (const [key, entry] of value.value)
|
|
18
|
+
for (const [key, entry] of value.value) defineCapture(object, key, entry, getState, setState)
|
|
10
19
|
return object
|
|
11
20
|
}
|
|
12
21
|
return value
|
|
13
22
|
}
|
|
23
|
+
|
|
24
|
+
function defineCapture(target, key, entry, getState, setState) {
|
|
25
|
+
const descriptor = globalThis.__KUDZU_CAPTURE_STATE__ && entry?.type === "state" && getState
|
|
26
|
+
? { get: () => getState(entry.id) }
|
|
27
|
+
: { value: deserialize(entry, getState, setState), writable: true }
|
|
28
|
+
Object.defineProperty(target, key, { ...descriptor, enumerable: true, configurable: true })
|
|
29
|
+
}
|