@kudzujs/core 0.5.2 → 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 +39 -11
- package/framework/binding-runtime.js +10 -1
- package/framework/build.mjs +221 -52
- 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
|
|
|
@@ -370,7 +387,7 @@ Supported:
|
|
|
370
387
|
- Default, nested, and reactive context providers
|
|
371
388
|
- Controlled `value` and `checked` form properties
|
|
372
389
|
- Conditional child `&&` and ternary DOM patches
|
|
373
|
-
- Top-level
|
|
390
|
+
- Top-level and block-scoped JSX locals, terminal early returns, and exhaustive JSX assignment
|
|
374
391
|
- Direct keyed local-state lists
|
|
375
392
|
|
|
376
393
|
Not implemented yet:
|
|
@@ -411,13 +428,24 @@ The same native counter calculation was measured inline and through one relative
|
|
|
411
428
|
|
|
412
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.
|
|
413
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
|
+
|
|
414
442
|
#### Wrapper-Free Derived Text
|
|
415
443
|
|
|
416
|
-
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.
|
|
417
445
|
|
|
418
446
|
| Text target | Files | JS gzip | Total output | Clean build | Update |
|
|
419
447
|
|---|---:|---:|---:|---:|---:|
|
|
420
|
-
|
|
|
448
|
+
| Legacy span target | 7 | **4,453 B** | **10,065 B** | **404 ms** | **4.83 µs** |
|
|
421
449
|
| Comment range | 7 | 4,763 B | 10,922 B | 426 ms | 5.03 µs |
|
|
422
450
|
|
|
423
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.
|
|
@@ -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,10 +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()
|
|
398
424
|
const components = new Map()
|
|
425
|
+
const contexts = new Set()
|
|
399
426
|
const jsxLocalDeclarations = new Map()
|
|
400
427
|
const jsxLocalsByFunction = new Map()
|
|
401
428
|
const listLocalDeclarations = new WeakSet()
|
|
@@ -429,10 +456,13 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
429
456
|
functions.set(node.name.text, node.initializer)
|
|
430
457
|
if (node.parent?.parent?.parent === sourceFile) components.set(node.name.text, { function: node.initializer, declaration: node })
|
|
431
458
|
}
|
|
432
|
-
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)) {
|
|
433
461
|
const owner = nearestFunction(node)
|
|
434
462
|
const declarations = jsxLocalDeclarations.get(owner) ?? new Map()
|
|
435
|
-
declarations.
|
|
463
|
+
const entries = declarations.get(node.name.text) ?? []
|
|
464
|
+
entries.push({ node, initializer: node.initializer })
|
|
465
|
+
declarations.set(node.name.text, entries)
|
|
436
466
|
jsxLocalDeclarations.set(owner, declarations)
|
|
437
467
|
}
|
|
438
468
|
ts.forEachChild(node, collect)
|
|
@@ -443,32 +473,41 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
443
473
|
let changed = true
|
|
444
474
|
while (changed) {
|
|
445
475
|
changed = false
|
|
446
|
-
for (const [name,
|
|
447
|
-
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))) {
|
|
448
478
|
names.add(name)
|
|
449
479
|
changed = true
|
|
450
480
|
}
|
|
451
481
|
}
|
|
452
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
|
+
}
|
|
453
490
|
jsxLocalsByFunction.set(owner, names)
|
|
454
491
|
}
|
|
455
492
|
for (const [owner, declarations] of jsxLocalDeclarations) {
|
|
456
493
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
457
|
-
for (const [name,
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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)
|
|
464
510
|
}
|
|
465
|
-
collectUses(owner.body)
|
|
466
|
-
const references = identifierReferenceCount(owner.body, name)
|
|
467
|
-
const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
|
|
468
|
-
if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
|
|
469
|
-
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`)
|
|
470
|
-
listLocalDeclarations.add(declaration.node)
|
|
471
|
-
if (uses.length) listLocalUses.set(uses[0], parts)
|
|
472
511
|
}
|
|
473
512
|
}
|
|
474
513
|
const rawRenderedLists = []
|
|
@@ -484,6 +523,16 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
484
523
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
485
524
|
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
|
|
486
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)
|
|
487
536
|
const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
|
|
488
537
|
const tag = jsxTagName(parts.root)
|
|
489
538
|
return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
|
|
@@ -527,6 +576,28 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
527
576
|
renderedLists.set(node, parts)
|
|
528
577
|
}
|
|
529
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
|
+
}
|
|
600
|
+
|
|
530
601
|
const visitor = node => {
|
|
531
602
|
if (specializedDeclarations.has(node)) return node
|
|
532
603
|
if (componentSpecializations.has(node)) return ts.visitNode(componentSpecializations.get(node).root, visitor)
|
|
@@ -554,18 +625,13 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
554
625
|
}
|
|
555
626
|
|
|
556
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)) {
|
|
557
|
-
const
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
usesConditional = true
|
|
565
|
-
const compiled = compileConditional(parts.kind, parts.condition, ts.visitNode(parts.truthy, visitor), ts.visitNode(parts.falsy, visitor), setters, factory, context, reactiveBindings, handlerUrl)
|
|
566
|
-
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, compiled)
|
|
567
|
-
}
|
|
568
|
-
}
|
|
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)
|
|
569
635
|
}
|
|
570
636
|
|
|
571
637
|
if (ts.isJsxExpression(node) && node.expression && listConditions.has(node.expression)) {
|
|
@@ -596,19 +662,10 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
596
662
|
ts.visitNode(listParts.callback, visitor)
|
|
597
663
|
]))
|
|
598
664
|
}
|
|
599
|
-
const
|
|
600
|
-
if (
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
const captures = captureNames(parts.condition, parts.condition, setters)
|
|
604
|
-
if (usedStates.size || captures.size) {
|
|
605
|
-
usesBehavior = true
|
|
606
|
-
usesConditional = true
|
|
607
|
-
const truthy = ts.visitNode(parts.truthy, visitor)
|
|
608
|
-
const falsy = ts.visitNode(parts.falsy, visitor)
|
|
609
|
-
const compiled = compileConditional(parts.kind, parts.condition, truthy, falsy, setters, factory, context, reactiveBindings, handlerUrl)
|
|
610
|
-
return factory.updateJsxExpression(node, compiled)
|
|
611
|
-
}
|
|
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)
|
|
612
669
|
}
|
|
613
670
|
const setters = settersForNode(node, settersByFunction)
|
|
614
671
|
const usedStates = referencedStateNames(node.expression, setters)
|
|
@@ -620,7 +677,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
620
677
|
}
|
|
621
678
|
}
|
|
622
679
|
|
|
623
|
-
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())) {
|
|
624
681
|
const expression = node.initializer.expression
|
|
625
682
|
const setters = settersForNode(node, settersByFunction)
|
|
626
683
|
const usedStates = referencedStateNames(expression, setters)
|
|
@@ -671,6 +728,112 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
671
728
|
}
|
|
672
729
|
}
|
|
673
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
|
+
|
|
674
837
|
function keyedListParts(expression, setters) {
|
|
675
838
|
const value = unwrapExpression(expression)
|
|
676
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
|
|
@@ -847,6 +1010,13 @@ function jsxTagName(node) {
|
|
|
847
1010
|
return ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
|
|
848
1011
|
}
|
|
849
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
|
+
|
|
850
1020
|
function isJsxSyntaxIdentifier(node) {
|
|
851
1021
|
const parent = node.parent
|
|
852
1022
|
return (ts.isJsxOpeningElement(parent) || ts.isJsxClosingElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node || ts.isJsxAttribute(parent) && parent.name === node
|
|
@@ -994,11 +1164,10 @@ function unwrapExpression(node) {
|
|
|
994
1164
|
return ts.isParenthesizedExpression(node) ? unwrapExpression(node.expression) : node
|
|
995
1165
|
}
|
|
996
1166
|
|
|
997
|
-
function
|
|
1167
|
+
function isLocalConst(node) {
|
|
998
1168
|
const list = node.parent
|
|
999
1169
|
const statement = list?.parent
|
|
1000
|
-
|
|
1001
|
-
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)
|
|
1002
1171
|
}
|
|
1003
1172
|
|
|
1004
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
|
+
}
|