@kudzujs/core 0.5.2 → 0.5.5
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 +41 -13
- package/framework/binding-runtime.js +10 -1
- package/framework/build.mjs +322 -80
- 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,7 +281,7 @@ const rows = items.map(item =>
|
|
|
264
281
|
return <ul>{rows}</ul>
|
|
265
282
|
```
|
|
266
283
|
|
|
267
|
-
The root may also be a top-level same
|
|
284
|
+
The root may also be a top-level row component declared in the same file or imported from a relative TypeScript module. Default, named, aliased, and named re-export imports are resolved at build time. Kudzu specializes each call, so projected props, callback props, and simple local calculations compile to the same intrinsic list template:
|
|
268
285
|
|
|
269
286
|
```tsx
|
|
270
287
|
function ItemRow({ name, done, onRemove }: {
|
|
@@ -289,7 +306,7 @@ const rows = items.map(item => <ItemRow
|
|
|
289
306
|
|
|
290
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.
|
|
291
308
|
|
|
292
|
-
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
|
|
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 local or relative-imported 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 used inside calculations, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Package or namespace row imports, same-file exported rows, reusable aliases, 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>`.
|
|
293
310
|
|
|
294
311
|
## Normal JavaScript
|
|
295
312
|
|
|
@@ -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
|
@@ -29,10 +29,11 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
29
29
|
const cssFiles = projectFiles.filter(file => file.endsWith(".css")).sort()
|
|
30
30
|
if (!sourceFiles.length) throw new Error("No TypeScript files found in src/")
|
|
31
31
|
const sourceFileSet = new Set(sourceFiles)
|
|
32
|
+
const sourceIndex = new Map(await Promise.all(sourceFiles.map(async file => [file, await readFile(file, "utf8")])))
|
|
32
33
|
|
|
33
34
|
const handlerModules = []
|
|
34
35
|
for (const file of sourceFiles) {
|
|
35
|
-
const handlerModule = await compile(file, sourceFileSet, base)
|
|
36
|
+
const handlerModule = await compile(file, sourceFileSet, sourceIndex, base)
|
|
36
37
|
if (handlerModule) handlerModules.push(handlerModule)
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -82,6 +83,8 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
82
83
|
const nativeEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.native).map(event => event.event)))].sort()
|
|
83
84
|
const hasTextBindings = plans.some(plan => plan.bindings.some(binding => binding.target === "text"))
|
|
84
85
|
const hasListConditions = plans.some(plan => plan.lists.some(list => list.conditions))
|
|
86
|
+
const hasNestedStateCaptures = hasNestedCaptureState(plans)
|
|
87
|
+
const hasSetterCaptures = hasCaptureType(plans, "setter")
|
|
85
88
|
const nativeModules = handlerModules.filter(module => module.hasNativeHandlers).map(module => assetPath(base, `assets/${module.path}`))
|
|
86
89
|
const hasNativeHandlers = nativeModules.length > 0
|
|
87
90
|
if (behaviorCount) {
|
|
@@ -89,14 +92,20 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
89
92
|
const runtime = specializeRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), commandEvents, stateSeedCount > 0)
|
|
90
93
|
await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
|
|
91
94
|
}
|
|
92
|
-
if (bindingCount || hasNativeHandlers) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify
|
|
95
|
+
if (bindingCount || hasNativeHandlers) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify, {
|
|
96
|
+
"globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures),
|
|
97
|
+
"globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
|
|
98
|
+
})
|
|
93
99
|
if (bindingCount || listStyleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
|
|
94
100
|
if (bindingCount) {
|
|
95
101
|
const bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
|
|
96
102
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
97
103
|
.replace('"./serialization.js"', '"./kudzu-serialization.js"')
|
|
98
104
|
.replace('"./style.js"', '"./kudzu-style.js"')
|
|
99
|
-
await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify, {
|
|
105
|
+
await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify, {
|
|
106
|
+
"globalThis.__KUDZU_TEXT_BINDINGS__": String(hasTextBindings),
|
|
107
|
+
"globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures)
|
|
108
|
+
})
|
|
100
109
|
}
|
|
101
110
|
if (listCount) {
|
|
102
111
|
let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
|
|
@@ -115,7 +124,9 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
115
124
|
const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
|
|
116
125
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
117
126
|
.replace('"./serialization.js"', '"./kudzu-serialization.js"')
|
|
118
|
-
await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeNativeRuntime(nativeRuntime, nativeEvents, nativeModules), minify
|
|
127
|
+
await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeNativeRuntime(nativeRuntime, nativeEvents, nativeModules), minify, {
|
|
128
|
+
"globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
|
|
129
|
+
})
|
|
119
130
|
}
|
|
120
131
|
for (const handlerModule of handlerModules) {
|
|
121
132
|
const output = join(assetsDirectory, handlerModule.path)
|
|
@@ -171,6 +182,20 @@ function specializeNativeRuntime(source, events, modules) {
|
|
|
171
182
|
return `${imports}\n${specializeEvents(source, events).replace(/const modules = new Map\(\[[^\n]*\]\)/, `const modules = new Map([${entries}])`)}`
|
|
172
183
|
}
|
|
173
184
|
|
|
185
|
+
function hasCaptureType(value, type) {
|
|
186
|
+
if (!value || typeof value !== "object") return false
|
|
187
|
+
if (value.type === type) return true
|
|
188
|
+
return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasCaptureType(entry, type))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function hasNestedCaptureState(value, insideCapture = false) {
|
|
192
|
+
if (!value || typeof value !== "object") return false
|
|
193
|
+
if (value.type === "state") return insideCapture
|
|
194
|
+
if (value.type === "array") return value.value.some(entry => hasNestedCaptureState(entry, true))
|
|
195
|
+
if (value.type === "object") return value.value.some(([, entry]) => hasNestedCaptureState(entry, true))
|
|
196
|
+
return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasNestedCaptureState(entry, false))
|
|
197
|
+
}
|
|
198
|
+
|
|
174
199
|
export function specializeRuntime(source, events, hasStateSeed) {
|
|
175
200
|
const specialized = specializeEvents(source, events)
|
|
176
201
|
if (hasStateSeed) return specialized
|
|
@@ -179,8 +204,8 @@ export function specializeRuntime(source, events, hasStateSeed) {
|
|
|
179
204
|
.replace(/^ if \(initialState\).*\n/m, "")
|
|
180
205
|
}
|
|
181
206
|
|
|
182
|
-
async function writeJavaScript(file, source, minify) {
|
|
183
|
-
const code = minify ? (await transform(source, { format: "esm", legalComments: "none", minify
|
|
207
|
+
async function writeJavaScript(file, source, minify, define) {
|
|
208
|
+
const code = minify || define ? (await transform(source, { define, format: "esm", legalComments: "none", minify, target: "es2022" })).code : source
|
|
184
209
|
await writeFile(file, code)
|
|
185
210
|
}
|
|
186
211
|
|
|
@@ -345,8 +370,8 @@ function escapeHtml(value) {
|
|
|
345
370
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
|
346
371
|
}
|
|
347
372
|
|
|
348
|
-
async function compile(file, sourceFiles, base) {
|
|
349
|
-
const source =
|
|
373
|
+
async function compile(file, sourceFiles, sourceIndex, base) {
|
|
374
|
+
const source = sourceIndex.get(file)
|
|
350
375
|
const nativeHandlers = []
|
|
351
376
|
const reactiveBindings = []
|
|
352
377
|
const listExpressions = []
|
|
@@ -360,7 +385,7 @@ async function compile(file, sourceFiles, base) {
|
|
|
360
385
|
jsx: ts.JsxEmit.ReactJSX,
|
|
361
386
|
jsxImportSource: "@kudzujs/core"
|
|
362
387
|
},
|
|
363
|
-
transformers: { before: [createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, assetPath(base, `assets/${handlerPath}`), file, sourceFiles, clientImports)] },
|
|
388
|
+
transformers: { before: [createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, clientImports)] },
|
|
364
389
|
reportDiagnostics: true
|
|
365
390
|
})
|
|
366
391
|
|
|
@@ -389,13 +414,26 @@ async function compile(file, sourceFiles, base) {
|
|
|
389
414
|
return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, clientImports: [...clientImports] }
|
|
390
415
|
}
|
|
391
416
|
|
|
392
|
-
function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, clientImports) {
|
|
417
|
+
function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, clientImports) {
|
|
393
418
|
return context => sourceFile => {
|
|
394
419
|
const factory = context.factory
|
|
420
|
+
sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
|
|
421
|
+
ts.setParentRecursive(sourceFile, false)
|
|
395
422
|
const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
|
|
423
|
+
const importedSources = new Map()
|
|
424
|
+
const importedSource = target => {
|
|
425
|
+
let imported = importedSources.get(target)
|
|
426
|
+
if (!imported) {
|
|
427
|
+
imported = normalizeRenderControlFlow(parseSourceFile(target, sourceIndex.get(target)), factory, context)
|
|
428
|
+
ts.setParentRecursive(imported, false)
|
|
429
|
+
importedSources.set(target, imported)
|
|
430
|
+
}
|
|
431
|
+
return imported
|
|
432
|
+
}
|
|
396
433
|
const settersByFunction = new Map()
|
|
397
434
|
const functions = new Map()
|
|
398
435
|
const components = new Map()
|
|
436
|
+
const contexts = new Set()
|
|
399
437
|
const jsxLocalDeclarations = new Map()
|
|
400
438
|
const jsxLocalsByFunction = new Map()
|
|
401
439
|
const listLocalDeclarations = new WeakSet()
|
|
@@ -429,10 +467,13 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
429
467
|
functions.set(node.name.text, node.initializer)
|
|
430
468
|
if (node.parent?.parent?.parent === sourceFile) components.set(node.name.text, { function: node.initializer, declaration: node })
|
|
431
469
|
}
|
|
432
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer &&
|
|
470
|
+
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)
|
|
471
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
|
|
433
472
|
const owner = nearestFunction(node)
|
|
434
473
|
const declarations = jsxLocalDeclarations.get(owner) ?? new Map()
|
|
435
|
-
declarations.
|
|
474
|
+
const entries = declarations.get(node.name.text) ?? []
|
|
475
|
+
entries.push({ node, initializer: node.initializer })
|
|
476
|
+
declarations.set(node.name.text, entries)
|
|
436
477
|
jsxLocalDeclarations.set(owner, declarations)
|
|
437
478
|
}
|
|
438
479
|
ts.forEachChild(node, collect)
|
|
@@ -443,32 +484,41 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
443
484
|
let changed = true
|
|
444
485
|
while (changed) {
|
|
445
486
|
changed = false
|
|
446
|
-
for (const [name,
|
|
447
|
-
if (!names.has(name) && isJsxLocalValue(initializer, names)) {
|
|
487
|
+
for (const [name, entries] of declarations) {
|
|
488
|
+
if (!names.has(name) && entries.some(({ initializer }) => isJsxLocalValue(initializer, names))) {
|
|
448
489
|
names.add(name)
|
|
449
490
|
changed = true
|
|
450
491
|
}
|
|
451
492
|
}
|
|
452
493
|
}
|
|
494
|
+
for (const name of names) {
|
|
495
|
+
const entries = declarations.get(name)
|
|
496
|
+
if (entries.length > 1) {
|
|
497
|
+
const position = sourceFile.getLineAndCharacterOfPosition(entries[1].node.getStart(sourceFile))
|
|
498
|
+
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Block-scoped JSX local "${name}" must not shadow another local`)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
453
501
|
jsxLocalsByFunction.set(owner, names)
|
|
454
502
|
}
|
|
455
503
|
for (const [owner, declarations] of jsxLocalDeclarations) {
|
|
456
504
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
457
|
-
for (const [name,
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
505
|
+
for (const [name, entries] of declarations) {
|
|
506
|
+
for (const declaration of entries) {
|
|
507
|
+
const parts = keyedListParts(declaration.initializer, setters)
|
|
508
|
+
if (!parts) continue
|
|
509
|
+
const uses = []
|
|
510
|
+
const collectUses = node => {
|
|
511
|
+
if (ts.isJsxExpression(node) && node.initializer === undefined && ts.isIdentifier(node.expression) && node.expression.text === name && nearestFunction(node) === owner) uses.push(node)
|
|
512
|
+
ts.forEachChild(node, collectUses)
|
|
513
|
+
}
|
|
514
|
+
collectUses(owner.body)
|
|
515
|
+
const references = identifierReferenceCount(owner.body, name)
|
|
516
|
+
const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
|
|
517
|
+
if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
|
|
518
|
+
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`)
|
|
519
|
+
listLocalDeclarations.add(declaration.node)
|
|
520
|
+
if (uses.length) listLocalUses.set(uses[0], parts)
|
|
464
521
|
}
|
|
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
522
|
}
|
|
473
523
|
}
|
|
474
524
|
const rawRenderedLists = []
|
|
@@ -481,9 +531,18 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
481
531
|
}
|
|
482
532
|
collectRenderedLists(sourceFile)
|
|
483
533
|
const fail = (node, message) => {
|
|
484
|
-
|
|
485
|
-
|
|
534
|
+
throw sourceNodeError(node, sourceFile, message)
|
|
535
|
+
}
|
|
536
|
+
const rejectUnsupportedRenderControl = node => {
|
|
537
|
+
if (ts.isIfStatement(node) && containsRenderControl(node, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
|
|
538
|
+
const setters = settersForNode(node, settersByFunction)
|
|
539
|
+
if (referencedStateNames(node.expression, setters).size) {
|
|
540
|
+
fail(node, "Reactive render if statements must use terminal returns or exhaustive adjacent JSX assignment")
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
ts.forEachChild(node, rejectUnsupportedRenderControl)
|
|
486
544
|
}
|
|
545
|
+
rejectUnsupportedRenderControl(sourceFile)
|
|
487
546
|
const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
|
|
488
547
|
const tag = jsxTagName(parts.root)
|
|
489
548
|
return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
|
|
@@ -491,13 +550,19 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
491
550
|
const componentSpecializations = new WeakMap()
|
|
492
551
|
const specializedDeclarations = new WeakSet()
|
|
493
552
|
for (const name of listComponentNames) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (
|
|
553
|
+
let component = components.get(name)
|
|
554
|
+
const local = Boolean(component)
|
|
555
|
+
if (!component) {
|
|
556
|
+
const binding = importBindings.get(name)
|
|
557
|
+
if (!binding || binding.kind === "namespace") fail(sourceFile, `Keyed list component ${name} must be declared locally or imported from a relative TypeScript module`)
|
|
558
|
+
const imported = binding.kind === "default" ? "default" : binding.imported
|
|
559
|
+
component = { function: resolveComponentExport(binding.target, imported, importedSource, sourceFiles), declaration: undefined }
|
|
560
|
+
}
|
|
561
|
+
if (local && isExportedDeclaration(component.declaration)) fail(component.declaration, `Keyed list component ${name} cannot be exported`)
|
|
497
562
|
const calls = jsxTagUses(sourceFile, name)
|
|
498
|
-
if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
|
|
563
|
+
if (local && identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
|
|
499
564
|
for (const call of calls) componentSpecializations.set(call, specializeComponentCall(call, component.function, sourceFile, factory, context, fail))
|
|
500
|
-
specializedDeclarations.add(component.declaration)
|
|
565
|
+
if (local) specializedDeclarations.add(component.declaration)
|
|
501
566
|
}
|
|
502
567
|
const renderedLists = new WeakMap()
|
|
503
568
|
for (const { node, parts: originalParts } of rawRenderedLists) {
|
|
@@ -527,16 +592,40 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
527
592
|
renderedLists.set(node, parts)
|
|
528
593
|
}
|
|
529
594
|
|
|
595
|
+
const compileRenderExpression = (expression, anchor) => {
|
|
596
|
+
const parts = conditionalParts(expression)
|
|
597
|
+
if (!parts) return ts.visitNode(expression, visitor)
|
|
598
|
+
const setters = settersForNode(anchor, settersByFunction)
|
|
599
|
+
const usedStates = referencedStateNames(parts.condition, setters)
|
|
600
|
+
const captures = captureNames(parts.condition, parts.condition, setters)
|
|
601
|
+
if (!usedStates.size && !captures.size) return ts.visitEachChild(expression, visitor, context)
|
|
602
|
+
usesBehavior = true
|
|
603
|
+
usesConditional = true
|
|
604
|
+
return compileConditional(
|
|
605
|
+
parts.kind,
|
|
606
|
+
parts.condition,
|
|
607
|
+
compileRenderExpression(parts.truthy, anchor),
|
|
608
|
+
compileRenderExpression(parts.falsy, anchor),
|
|
609
|
+
setters,
|
|
610
|
+
factory,
|
|
611
|
+
context,
|
|
612
|
+
reactiveBindings,
|
|
613
|
+
handlerUrl
|
|
614
|
+
)
|
|
615
|
+
}
|
|
616
|
+
|
|
530
617
|
const visitor = node => {
|
|
531
618
|
if (specializedDeclarations.has(node)) return node
|
|
532
619
|
if (componentSpecializations.has(node)) return ts.visitNode(componentSpecializations.get(node).root, visitor)
|
|
533
620
|
|
|
534
621
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
|
|
535
|
-
|
|
622
|
+
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
623
|
+
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
|
|
536
624
|
}
|
|
537
625
|
|
|
538
626
|
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
|
|
539
|
-
|
|
627
|
+
const target = resolveSourceImport(file, node.moduleSpecifier.text, sourceFiles)
|
|
628
|
+
return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
|
|
540
629
|
}
|
|
541
630
|
|
|
542
631
|
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "useState" && node.initializer.arguments.length === 1) {
|
|
@@ -554,18 +643,13 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
554
643
|
}
|
|
555
644
|
|
|
556
645
|
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
|
-
}
|
|
646
|
+
const compiled = compileRenderExpression(node.initializer, node)
|
|
647
|
+
if (compiled !== node.initializer) return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, compiled)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, jsxLocalsByFunction.get(nearestFunction(node)) ?? new Set())) {
|
|
651
|
+
const compiled = compileRenderExpression(node.expression, node)
|
|
652
|
+
if (compiled !== node.expression) return factory.updateReturnStatement(node, compiled)
|
|
569
653
|
}
|
|
570
654
|
|
|
571
655
|
if (ts.isJsxExpression(node) && node.expression && listConditions.has(node.expression)) {
|
|
@@ -596,19 +680,10 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
596
680
|
ts.visitNode(listParts.callback, visitor)
|
|
597
681
|
]))
|
|
598
682
|
}
|
|
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
|
-
}
|
|
683
|
+
const conditional = conditionalParts(node.expression)
|
|
684
|
+
if (conditional) {
|
|
685
|
+
const compiled = compileRenderExpression(node.expression, node)
|
|
686
|
+
if (compiled !== node.expression) return factory.updateJsxExpression(node, compiled)
|
|
612
687
|
}
|
|
613
688
|
const setters = settersForNode(node, settersByFunction)
|
|
614
689
|
const usedStates = referencedStateNames(node.expression, setters)
|
|
@@ -620,7 +695,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
620
695
|
}
|
|
621
696
|
}
|
|
622
697
|
|
|
623
|
-
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !/^on/i.test(node.name.
|
|
698
|
+
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !isContextProviderValue(node, contexts) && !/^on/i.test(node.name.text) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.text.toLowerCase())) {
|
|
624
699
|
const expression = node.initializer.expression
|
|
625
700
|
const setters = settersForNode(node, settersByFunction)
|
|
626
701
|
const usedStates = referencedStateNames(expression, setters)
|
|
@@ -633,15 +708,16 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
633
708
|
}
|
|
634
709
|
}
|
|
635
710
|
|
|
636
|
-
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.
|
|
711
|
+
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
|
|
637
712
|
const setters = settersForNode(node, settersByFunction)
|
|
638
713
|
const event = compileEvent(node.initializer.expression, setters, functions, factory, nativeHandlers, handlerUrl, listEventItems.get(node), importBindings, clientImports)
|
|
639
714
|
if (event) {
|
|
640
715
|
usesBehavior = true
|
|
641
716
|
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, event))
|
|
642
717
|
}
|
|
718
|
+
if (ts.isIdentifier(node.initializer.expression) && isDestructuredParameter(node.initializer.expression, nearestFunction(node))) return node
|
|
643
719
|
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
|
|
644
|
-
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${node.name.
|
|
720
|
+
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${node.name.text} must reference a function`)
|
|
645
721
|
}
|
|
646
722
|
|
|
647
723
|
return ts.visitEachChild(node, visitor, context)
|
|
@@ -671,6 +747,112 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
671
747
|
}
|
|
672
748
|
}
|
|
673
749
|
|
|
750
|
+
function normalizeRenderControlFlow(sourceFile, factory, context) {
|
|
751
|
+
const normalizeStatements = statements => {
|
|
752
|
+
const nested = statements.map(statement => ts.visitEachChild(statement, visitNested, context))
|
|
753
|
+
const assigned = []
|
|
754
|
+
for (let index = 0; index < nested.length; index++) {
|
|
755
|
+
const statement = nested[index]
|
|
756
|
+
const next = nested[index + 1]
|
|
757
|
+
const declaration = singleUninitializedLet(statement)
|
|
758
|
+
const assignment = declaration && next && assignmentConditional(next, declaration.name.text, factory)
|
|
759
|
+
if (declaration && assignment) {
|
|
760
|
+
const updated = factory.updateVariableDeclaration(declaration, declaration.name, declaration.exclamationToken, declaration.type, assignment)
|
|
761
|
+
const list = factory.createVariableDeclarationList([updated], ts.NodeFlags.Const)
|
|
762
|
+
assigned.push(factory.updateVariableStatement(statement, statement.modifiers, list))
|
|
763
|
+
index++
|
|
764
|
+
} else {
|
|
765
|
+
assigned.push(statement)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (!assigned.length) return assigned
|
|
770
|
+
const finalIf = returnConditional(assigned.at(-1), factory)
|
|
771
|
+
if (finalIf) return [...assigned.slice(0, -1), factory.createReturnStatement(finalIf)]
|
|
772
|
+
if (!ts.isReturnStatement(assigned.at(-1)) || !assigned.at(-1).expression) return assigned
|
|
773
|
+
let expression = assigned.at(-1).expression
|
|
774
|
+
let start = assigned.length - 1
|
|
775
|
+
while (start > 0) {
|
|
776
|
+
const previous = assigned[start - 1]
|
|
777
|
+
if (!ts.isIfStatement(previous) || previous.elseStatement) break
|
|
778
|
+
const truthy = returnOnlyExpression(previous.thenStatement)
|
|
779
|
+
if (!truthy) break
|
|
780
|
+
expression = factory.createConditionalExpression(previous.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), expression)
|
|
781
|
+
start--
|
|
782
|
+
}
|
|
783
|
+
return start === assigned.length - 1 ? assigned : [...assigned.slice(0, start), factory.createReturnStatement(expression)]
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const visitNested = node => {
|
|
787
|
+
if (ts.isBlock(node)) return factory.updateBlock(node, normalizeStatements([...node.statements]))
|
|
788
|
+
if (isFunctionLike(node) && ts.isBlock(node.body)) {
|
|
789
|
+
if (!isRenderFunction(node)) return node
|
|
790
|
+
const body = factory.updateBlock(node.body, normalizeStatements([...node.body.statements]))
|
|
791
|
+
if (ts.isFunctionDeclaration(node)) return factory.updateFunctionDeclaration(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
|
|
792
|
+
if (ts.isFunctionExpression(node)) return factory.updateFunctionExpression(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, body)
|
|
793
|
+
if (ts.isArrowFunction(node)) return factory.updateArrowFunction(node, node.modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, body)
|
|
794
|
+
}
|
|
795
|
+
return ts.visitEachChild(node, visitNested, context)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return ts.visitEachChild(sourceFile, visitNested, context)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function isRenderFunction(node) {
|
|
802
|
+
if (ts.isFunctionDeclaration(node)) return node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword) || Boolean(node.name && /^[A-Z]/.test(node.name.text))
|
|
803
|
+
const declaration = node.parent
|
|
804
|
+
return ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name) && /^[A-Z]/.test(declaration.name.text)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function singleUninitializedLet(statement) {
|
|
808
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Let) === 0 || statement.declarationList.declarations.length !== 1) return undefined
|
|
809
|
+
const declaration = statement.declarationList.declarations[0]
|
|
810
|
+
return ts.isIdentifier(declaration.name) && !declaration.initializer ? declaration : undefined
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function assignmentConditional(statement, name, factory) {
|
|
814
|
+
if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
|
|
815
|
+
const truthy = assignmentOnlyExpression(statement.thenStatement, name)
|
|
816
|
+
const falsy = ts.isIfStatement(statement.elseStatement)
|
|
817
|
+
? assignmentConditional(statement.elseStatement, name, factory)
|
|
818
|
+
: assignmentOnlyExpression(statement.elseStatement, name)
|
|
819
|
+
if (!truthy || !falsy) return undefined
|
|
820
|
+
return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function assignmentOnlyExpression(statement, name) {
|
|
824
|
+
const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
|
|
825
|
+
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
|
|
826
|
+
return candidate.expression.right
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function returnConditional(statement, factory) {
|
|
830
|
+
if (!ts.isIfStatement(statement) || !statement.elseStatement) return undefined
|
|
831
|
+
const truthy = returnOnlyExpression(statement.thenStatement)
|
|
832
|
+
const falsy = ts.isIfStatement(statement.elseStatement)
|
|
833
|
+
? returnConditional(statement.elseStatement, factory)
|
|
834
|
+
: returnOnlyExpression(statement.elseStatement)
|
|
835
|
+
if (!truthy || !falsy) return undefined
|
|
836
|
+
return factory.createConditionalExpression(statement.expression, factory.createToken(ts.SyntaxKind.QuestionToken), truthy, factory.createToken(ts.SyntaxKind.ColonToken), falsy)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function returnOnlyExpression(statement) {
|
|
840
|
+
const candidate = ts.isBlock(statement) && statement.statements.length === 1 ? statement.statements[0] : statement
|
|
841
|
+
return ts.isReturnStatement(candidate) && candidate.expression ? candidate.expression : undefined
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function containsRenderControl(root, knownLocals) {
|
|
845
|
+
let found = false
|
|
846
|
+
const visit = node => {
|
|
847
|
+
if (isFunctionLike(node) && node !== root) return
|
|
848
|
+
if (ts.isReturnStatement(node) && node.expression && isJsxLocalValue(node.expression, knownLocals)) found = true
|
|
849
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && containsJsx(node.right)) found = true
|
|
850
|
+
if (!found) ts.forEachChild(node, visit)
|
|
851
|
+
}
|
|
852
|
+
visit(root)
|
|
853
|
+
return found
|
|
854
|
+
}
|
|
855
|
+
|
|
674
856
|
function keyedListParts(expression, setters) {
|
|
675
857
|
const value = unwrapExpression(expression)
|
|
676
858
|
if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map" || !ts.isIdentifier(value.expression.expression)) return undefined
|
|
@@ -691,8 +873,7 @@ function keyedListParts(expression, setters) {
|
|
|
691
873
|
|
|
692
874
|
function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions) {
|
|
693
875
|
const fail = (node, message) => {
|
|
694
|
-
|
|
695
|
-
throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
|
|
876
|
+
throw sourceNodeError(node, sourceFile, message)
|
|
696
877
|
}
|
|
697
878
|
const root = parts.root
|
|
698
879
|
const item = parts.item
|
|
@@ -706,7 +887,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
706
887
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
|
|
707
888
|
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, "Nested keyed lists are not supported")
|
|
708
889
|
if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
|
|
709
|
-
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.
|
|
890
|
+
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
|
|
710
891
|
listEventItems.set(node, item)
|
|
711
892
|
return
|
|
712
893
|
}
|
|
@@ -725,9 +906,9 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
725
906
|
return
|
|
726
907
|
}
|
|
727
908
|
const field = directProperty(expression, item)
|
|
728
|
-
const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.
|
|
909
|
+
const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.text === "key"
|
|
729
910
|
if (field && ["__proto__", "constructor", "prototype"].includes(field)) fail(node, `Keyed list item property "${field}" is not supported`)
|
|
730
|
-
if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.
|
|
911
|
+
if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
|
|
731
912
|
if (isRootKey) return
|
|
732
913
|
if (field) {
|
|
733
914
|
listValues.set(node.expression, { field })
|
|
@@ -735,7 +916,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
735
916
|
}
|
|
736
917
|
if (referencesIdentifier(expression, item)) {
|
|
737
918
|
validateListExpression(expression, item, node, fail)
|
|
738
|
-
if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.
|
|
919
|
+
if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
|
|
739
920
|
listValues.set(node.expression, { item })
|
|
740
921
|
return
|
|
741
922
|
}
|
|
@@ -808,6 +989,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
808
989
|
|
|
809
990
|
function substituteClone(root, substitutions, factory, context) {
|
|
810
991
|
const visit = (node, shadowed = new Set()) => {
|
|
992
|
+
if (ts.isTypeNode(node)) return cloneAst(node, factory, context)
|
|
811
993
|
if (ts.isShorthandPropertyAssignment(node) && substitutions.has(node.name.text) && !shadowed.has(node.name.text)) {
|
|
812
994
|
return factory.createPropertyAssignment(cloneAst(node.name, factory, context), cloneAst(substitutions.get(node.name.text), factory, context))
|
|
813
995
|
}
|
|
@@ -847,6 +1029,13 @@ function jsxTagName(node) {
|
|
|
847
1029
|
return ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
|
|
848
1030
|
}
|
|
849
1031
|
|
|
1032
|
+
function isContextProviderValue(node, contexts) {
|
|
1033
|
+
if (node.name.getText() !== "value") return false
|
|
1034
|
+
const element = node.parent?.parent
|
|
1035
|
+
const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
|
|
1036
|
+
return ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && contexts.has(tag.expression.text)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
850
1039
|
function isJsxSyntaxIdentifier(node) {
|
|
851
1040
|
const parent = node.parent
|
|
852
1041
|
return (ts.isJsxOpeningElement(parent) || ts.isJsxClosingElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node || ts.isJsxAttribute(parent) && parent.name === node
|
|
@@ -856,6 +1045,10 @@ function isFunctionLike(node) {
|
|
|
856
1045
|
return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
|
|
857
1046
|
}
|
|
858
1047
|
|
|
1048
|
+
function isDestructuredParameter(identifier, fn) {
|
|
1049
|
+
return fn?.parameters.some(parameter => ts.isObjectBindingPattern(parameter.name) && parameter.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === identifier.text)) ?? false
|
|
1050
|
+
}
|
|
1051
|
+
|
|
859
1052
|
function isExportedDeclaration(node) {
|
|
860
1053
|
const statement = ts.isVariableDeclaration(node) ? node.parent?.parent : node
|
|
861
1054
|
return statement?.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
|
|
@@ -887,6 +1080,7 @@ const assignmentOperators = new Set([
|
|
|
887
1080
|
|
|
888
1081
|
function validateListExpression(expression, item, source, fail) {
|
|
889
1082
|
const visit = node => {
|
|
1083
|
+
if (ts.isTypeNode(node)) return
|
|
890
1084
|
if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
|
|
891
1085
|
const key = node.argumentExpression
|
|
892
1086
|
if (!ts.isStringLiteral(key) && !ts.isNumericLiteral(key)) fail(source, "Derived keyed list item computed properties require a direct string or numeric literal key")
|
|
@@ -994,11 +1188,10 @@ function unwrapExpression(node) {
|
|
|
994
1188
|
return ts.isParenthesizedExpression(node) ? unwrapExpression(node.expression) : node
|
|
995
1189
|
}
|
|
996
1190
|
|
|
997
|
-
function
|
|
1191
|
+
function isLocalConst(node) {
|
|
998
1192
|
const list = node.parent
|
|
999
1193
|
const statement = list?.parent
|
|
1000
|
-
|
|
1001
|
-
return ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 && ts.isVariableStatement(statement) && statement.parent === owner?.body
|
|
1194
|
+
return ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0 && ts.isVariableStatement(statement)
|
|
1002
1195
|
}
|
|
1003
1196
|
|
|
1004
1197
|
function isJsxLocalValue(expression, known) {
|
|
@@ -1230,6 +1423,60 @@ function clientImportBindings(sourceFile, file, sourceFiles) {
|
|
|
1230
1423
|
return bindings
|
|
1231
1424
|
}
|
|
1232
1425
|
|
|
1426
|
+
function resolveComponentExport(file, exportName, getSource, sourceFiles, trail = []) {
|
|
1427
|
+
const key = `${file}:${exportName}`
|
|
1428
|
+
if (trail.includes(key)) throw new Error(`Imported keyed list component re-export cycle: ${[...trail, key].map(entry => relative(root, entry.slice(0, entry.lastIndexOf(":")))).join(" -> ")}`)
|
|
1429
|
+
const sourceFile = getSource(file)
|
|
1430
|
+
const nextTrail = [...trail, key]
|
|
1431
|
+
|
|
1432
|
+
for (const statement of sourceFile.statements) {
|
|
1433
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
1434
|
+
const isDefault = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)
|
|
1435
|
+
const isExported = statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
1436
|
+
if (exportName === "default" && isDefault || exportName !== "default" && isExported && statement.name?.text === exportName) return statement
|
|
1437
|
+
}
|
|
1438
|
+
if (ts.isVariableStatement(statement) && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword) && exportName !== "default") {
|
|
1439
|
+
const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === exportName)
|
|
1440
|
+
if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
|
|
1441
|
+
}
|
|
1442
|
+
if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals && ts.isIdentifier(statement.expression)) {
|
|
1443
|
+
const component = localComponentDeclaration(sourceFile, statement.expression.text)
|
|
1444
|
+
if (component) return component
|
|
1445
|
+
}
|
|
1446
|
+
if (ts.isExportDeclaration(statement) && ts.isNamedExports(statement.exportClause)) {
|
|
1447
|
+
const entry = statement.exportClause.elements.find(element => !element.isTypeOnly && element.name.text === exportName)
|
|
1448
|
+
if (!entry) continue
|
|
1449
|
+
const imported = (entry.propertyName ?? entry.name).text
|
|
1450
|
+
if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1451
|
+
if (!statement.moduleSpecifier.text.startsWith(".")) throw sourceNodeError(statement, sourceFile, "Imported keyed list components must use relative TypeScript re-exports")
|
|
1452
|
+
const target = resolveSourceImport(file, statement.moduleSpecifier.text, sourceFiles)
|
|
1453
|
+
return resolveComponentExport(target, imported, getSource, sourceFiles, nextTrail)
|
|
1454
|
+
}
|
|
1455
|
+
const component = localComponentDeclaration(sourceFile, imported)
|
|
1456
|
+
if (component) return component
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
throw new Error(`${relative(root, file)} does not export a statically analyzable keyed list component named ${JSON.stringify(exportName)}`)
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function localComponentDeclaration(sourceFile, name) {
|
|
1463
|
+
for (const statement of sourceFile.statements) {
|
|
1464
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) return statement
|
|
1465
|
+
if (ts.isVariableStatement(statement)) {
|
|
1466
|
+
const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === name)
|
|
1467
|
+
if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return declaration.initializer
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return undefined
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function sourceNodeError(node, fallbackSource, message) {
|
|
1474
|
+
const original = ts.getOriginalNode(node)
|
|
1475
|
+
const sourceFile = original.getSourceFile?.()?.fileName ? original.getSourceFile() : fallbackSource
|
|
1476
|
+
const position = sourceFile.getLineAndCharacterOfPosition(original.getStart(sourceFile))
|
|
1477
|
+
return new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1233
1480
|
function printClientImports(entries, handlerPath) {
|
|
1234
1481
|
const unique = new Map(entries.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry]))
|
|
1235
1482
|
const groups = Map.groupBy(unique.values(), entry => entry.target)
|
|
@@ -1305,7 +1552,7 @@ function resolveSourceImport(importer, specifier, sourceFiles) {
|
|
|
1305
1552
|
const stem = /\.(?:js|jsx|ts|tsx)$/.test(extension) ? base.slice(0, -extension.length) : base
|
|
1306
1553
|
const candidates = extension === ".ts" || extension === ".tsx"
|
|
1307
1554
|
? [base]
|
|
1308
|
-
: [`${stem}.ts`, `${stem}.tsx
|
|
1555
|
+
: [`${stem}.ts`, `${stem}.tsx`, join(stem, "index.ts"), join(stem, "index.tsx")]
|
|
1309
1556
|
const matches = candidates.filter(candidate => sourceFiles.has(candidate))
|
|
1310
1557
|
if (matches.length !== 1) throw new Error(`${relative(root, importer)} Relative import ${JSON.stringify(specifier)} must resolve to one TypeScript file in src/`)
|
|
1311
1558
|
return matches[0]
|
|
@@ -1498,11 +1745,6 @@ function numericExpression(factory, value, negative) {
|
|
|
1498
1745
|
return negative ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, literal) : literal
|
|
1499
1746
|
}
|
|
1500
1747
|
|
|
1501
|
-
function modulePath(value) {
|
|
1502
|
-
if (/\.(?:ts|tsx|js|jsx)$/.test(value)) return value.replace(/\.(?:ts|tsx|js|jsx)$/, ".mjs")
|
|
1503
|
-
return `${value}.mjs`
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
1748
|
function compiledPath(file) {
|
|
1507
1749
|
return join(workDirectory, relative(sourceDirectory, file)).replace(/\.(?:ts|tsx)$/, ".mjs")
|
|
1508
1750
|
}
|
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
|
+
}
|