@kudzujs/core 0.5.5 → 0.5.7
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 +47 -3
- package/framework/README.md +3 -2
- package/framework/build.mjs +398 -49
- package/framework/core.d.ts +9 -0
- package/framework/core.mjs +72 -10
- package/framework/effect-runtime.js +39 -0
- package/framework/list-runtime.js +50 -42
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
HTML-first TSX framework with synchronous state semantics and no virtual DOM.
|
|
8
8
|
|
|
9
|
-
Kudzu keeps the familiar function-component, props, children, event-handler,
|
|
9
|
+
Kudzu keeps the familiar function-component, props, children, event-handler, `useState`, and mount-effect shape. Static components compile to HTML. Simple interactions compile to small behavior commands, while normal sync or async JavaScript handlers and mount effects compile to external ESM.
|
|
10
10
|
|
|
11
11
|
> Experimental `0.4.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
@@ -97,6 +97,27 @@ export default function Post({ title }: { title: string }) {
|
|
|
97
97
|
|
|
98
98
|
This emits `/posts/oak` and `/posts/pine`. Parameter values must be safe single path segments; missing, unsafe, and duplicate routes fail the build.
|
|
99
99
|
|
|
100
|
+
When a bracket value exists only in the request URL, opt into one static fallback document and read it with `useParams()`:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
// src/pages/items/[id].tsx
|
|
104
|
+
import { useEffect, useParams } from "@kudzujs/core"
|
|
105
|
+
|
|
106
|
+
export const runtimeParams = true
|
|
107
|
+
|
|
108
|
+
export default function ItemPage() {
|
|
109
|
+
const { id } = useParams<{ id: string }>()
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
fetch(`/api/items/${encodeURIComponent(id)}`)
|
|
113
|
+
}, [])
|
|
114
|
+
|
|
115
|
+
return <h1>Item {id}</h1>
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This emits `dist/items/[id]/index.html` and a route-specific pathname matcher. `getStaticPaths()` and `runtimeParams` are mutually exclusive. Runtime parameters occupy complete path segments, decode once, and reject empty, malformed, separator, control, and traversal-like values. The development server resolves deep links automatically. Production static hosts must try exact files first, then internally rewrite matching paths to the fallback file while preserving the browser URL; `.kudzu/kudzu-plan.json` and `afterBuild()` expose ordered `rewrites` for host adapters. Navigation remains ordinary `<a>` document navigation, not an SPA router.
|
|
120
|
+
|
|
100
121
|
Static trusted HTML can be rendered without a transform layer:
|
|
101
122
|
|
|
102
123
|
```tsx
|
|
@@ -110,8 +131,8 @@ Every CSS file under `src` is copied to the same relative path under `dist/asset
|
|
|
110
131
|
```js
|
|
111
132
|
export default {
|
|
112
133
|
base: "/newsletter",
|
|
113
|
-
async afterBuild({ outDir, routes, plans, base }) {
|
|
114
|
-
// Write RSS, sitemap,
|
|
134
|
+
async afterBuild({ outDir, routes, plans, rewrites, base }) {
|
|
135
|
+
// Write host rewrites, RSS, sitemap, or other static artifacts.
|
|
115
136
|
}
|
|
116
137
|
}
|
|
117
138
|
```
|
|
@@ -308,6 +329,27 @@ The original component remains reusable across multiple lists and ordinary JSX.
|
|
|
308
329
|
|
|
309
330
|
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>`.
|
|
310
331
|
|
|
332
|
+
## Mount Effects
|
|
333
|
+
|
|
334
|
+
Browser-only initial work uses the familiar empty-dependency effect shape:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
import { useEffect, useState } from "@kudzujs/core"
|
|
338
|
+
|
|
339
|
+
const [items, setItems] = useState([])
|
|
340
|
+
|
|
341
|
+
useEffect(async () => {
|
|
342
|
+
const response = await fetch("/api/items")
|
|
343
|
+
setItems(await response.json())
|
|
344
|
+
}, [])
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Kudzu does not execute the effect during static rendering and does not ship the component. It emits one route-specific effect entry that invokes the compiled callback against existing logical state and direct DOM commit capabilities. Effects may update reactive text, attributes, conditions, and keyed lists. Multiple effects start independently in source order, and one synchronous or asynchronous failure is reported without suppressing later effects.
|
|
348
|
+
|
|
349
|
+
Only inline block-bodied callbacks with a literal empty dependency array are supported. Dependencies, cleanup or other return values, callback parameters, and non-serializable captures are rejected at build time. Pages without effects receive no effect entry and retain their existing output.
|
|
350
|
+
|
|
351
|
+
A matched mount-fetch benchmark renders a title and two keyed rows from local JSON. With one warm-up and seven rotating clean builds, Kudzu shipped initial HTML, 3.4 KB initial JS gzip, 8.1 KB total output, and built in 374 ms. React CSR shipped no initial content, 59.3 KB initial JS gzip, 189.2 KB total output, and built in 992 ms. Hand-written ESM shipped 534 B initial JS gzip, 1.2 KB total output, and built in 210 ms. Fresh-profile Chrome medians to loaded data were 157.9 ms, 166.5 ms, and 153.4 ms respectively.
|
|
352
|
+
|
|
311
353
|
## Normal JavaScript
|
|
312
354
|
|
|
313
355
|
Command-only setters use the smallest optimized path. Conditions, local variables, browser globals, events, and `async`/`await` compile to external ESM without `eval`, `new Function`, or inline executable code.
|
|
@@ -374,9 +416,11 @@ Supported:
|
|
|
374
416
|
- File-based static routes
|
|
375
417
|
- Build-time async components
|
|
376
418
|
- Dynamic static routes with build-time props
|
|
419
|
+
- Runtime bracket parameters with static fallback documents and host rewrite metadata
|
|
377
420
|
- Static trusted `dangerouslySetInnerHTML`
|
|
378
421
|
- Base-path deployments, multiple CSS files, and `afterBuild`
|
|
379
422
|
- Primitive `useState` bindings
|
|
423
|
+
- Mount-only `useEffect(fn, [])` compiled to route-specific ESM
|
|
380
424
|
- Synchronous and async event handlers
|
|
381
425
|
- Relative imported helpers in native handlers
|
|
382
426
|
- Serializable component-local captures
|
package/framework/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Framework Internals
|
|
2
2
|
|
|
3
|
-
- `build.mjs`: TSX compilation, static
|
|
3
|
+
- `build.mjs`: TSX compilation, static, `getStaticPaths`, and runtime-fallback routes, base paths, CSS collection, post-build hooks, behavior extraction, static HTML output, and the development server.
|
|
4
4
|
- `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
|
|
5
5
|
- `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
|
|
6
6
|
- `runtime.js`: command-only runtime for direct state-to-text patches.
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
- `binding-runtime.js`: optional generic attributes, form properties, comment-bounded text patches, and conditional range patches.
|
|
9
9
|
- `list-runtime.js`: optional keyed list validation, external item-expression evaluation, item-local conditional ranges, dynamic styles and item-handler scopes, moves, and cleanup.
|
|
10
10
|
- `serialization.js`: capture deserialization shared by binding and native handlers.
|
|
11
|
+
- `effect-runtime.js`: optional state and capture context for route-specific mount-effect entries.
|
|
11
12
|
- `native-runtime.js`: optional runtime for normal synchronous and asynchronous ESM handlers.
|
|
12
13
|
- `dev-state.js`: dev-only, short-lived logical-state snapshot validation and restoration.
|
|
13
14
|
- `*.d.ts`: public TypeScript and JSX declarations.
|
|
14
15
|
|
|
15
|
-
Static routes receive no browser runtime. Command routes receive `runtime.js`; reactive attributes and conditions add `binding-runtime.js`; keyed lists add `list-runtime.js`; native handlers add `native-runtime.js
|
|
16
|
+
Static routes receive no browser runtime. Command routes receive `runtime.js`; runtime bracket pages using `useParams()` add one route-specific pathname matcher; reactive attributes and conditions add `binding-runtime.js`; keyed lists add `list-runtime.js`; native handlers add `native-runtime.js`; mount effects add `effect-runtime.js` and one route-specific entry. List builds remove unused text-range, attribute, event, expression, condition, seed, and mount branches. Effect builds omit capture deserialization entirely when every effect scope is empty. Capability runtimes share state and lifecycle hooks through `shared-runtime.js`. Generated evaluators and their bundled relative TypeScript helpers live under `dist/assets/handlers/`; shared helper chunks are emitted only when multiple handler entries need them. Runtime fallback rewrites are ordered by specificity in `.kudzu/kudzu-plan.json` and passed to `afterBuild()`; exact static files take precedence in development. The dev server derives stable state identities from route-unique state variable names in each route plan; every state sharing a duplicate name is omitted. It then injects its SSE reload, short-lived full-URL-scoped logical-state snapshot, and build-error client into responses only, never into `dist/`. Snapshots are consumed even when the next page is static or broken. Reload restoration covers compatible framework state, not uncontrolled DOM state, focus, selection, or imperative mutations.
|
|
16
17
|
|
|
17
18
|
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime.
|
package/framework/build.mjs
CHANGED
|
@@ -46,6 +46,9 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
46
46
|
let listStyleCount = 0
|
|
47
47
|
let stateSeedCount = 0
|
|
48
48
|
const plans = []
|
|
49
|
+
const effectEntries = []
|
|
50
|
+
const paramEntries = []
|
|
51
|
+
const rewrites = []
|
|
49
52
|
const emittedRoutes = new Set()
|
|
50
53
|
const styleUrls = cssFiles.map(file => assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`))
|
|
51
54
|
|
|
@@ -54,21 +57,40 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
54
57
|
const module = await import(`${pathToFileURL(compiledFile).href}?v=${Date.now()}`)
|
|
55
58
|
if (typeof module.default !== "function") throw new Error(`${relative(root, pageFile)} must export a default component`)
|
|
56
59
|
|
|
57
|
-
const
|
|
60
|
+
const runtimeSchema = runtimeRouteSchema(module, pageFile)
|
|
61
|
+
if (runtimeSchema) {
|
|
62
|
+
const conflicting = rewrites.find(rewrite => sameRuntimePrecedence(rewrite, runtimeSchema))
|
|
63
|
+
if (conflicting) throw new Error(`Ambiguous runtime routes: ${conflicting.route} and ${runtimeSchema.route}`)
|
|
64
|
+
rewrites.push({
|
|
65
|
+
route: runtimeSchema.route,
|
|
66
|
+
pattern: withBase(base, `/${runtimeSchema.route}`),
|
|
67
|
+
file: `${runtimeSchema.route}/index.html`,
|
|
68
|
+
params: runtimeSchema.params,
|
|
69
|
+
segments: runtimeSchema.segments
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
const entries = runtimeSchema ? [{ params: {}, props: {} }] : await staticPathEntries(module, pageFile)
|
|
58
73
|
for (const { params, props } of entries) {
|
|
59
|
-
const route = routeFromPage(pageFile, params)
|
|
74
|
+
const route = runtimeSchema?.route ?? routeFromPage(pageFile, params)
|
|
60
75
|
const routePath = withBase(base, `/${route}`)
|
|
76
|
+
const effectPath = `effects/${route ? `${route}/index` : "index"}.js`
|
|
77
|
+
const paramPath = `params/${route}/index.js`
|
|
61
78
|
if (emittedRoutes.has(routePath)) throw new Error(`Duplicate route: ${routePath}`)
|
|
62
79
|
emittedRoutes.add(routePath)
|
|
63
80
|
const result = await renderPage(module.default, {
|
|
64
81
|
...(module.metadata ?? {}),
|
|
65
82
|
styles: styleUrls.length ? styleUrls : false,
|
|
66
|
-
base
|
|
83
|
+
base,
|
|
84
|
+
effectAsset: assetPath(base, `assets/${effectPath}`),
|
|
85
|
+
paramAsset: assetPath(base, `assets/${paramPath}`),
|
|
86
|
+
runtimeParams: runtimeSchema?.params
|
|
67
87
|
}, props)
|
|
68
88
|
const routeDirectory = join(outputDirectory, route)
|
|
69
89
|
await mkdir(routeDirectory, { recursive: true })
|
|
70
90
|
await writeFile(join(routeDirectory, "index.html"), result.html)
|
|
71
91
|
plans.push({ route: routePath, ...result.plan })
|
|
92
|
+
if (result.hasParams) paramEntries.push({ path: paramPath, schema: runtimeSchema, params: result.plan.params })
|
|
93
|
+
if (result.hasEffects) effectEntries.push({ path: effectPath, effects: result.plan.effects, paramPath: result.hasParams ? paramPath : undefined })
|
|
72
94
|
if (result.hasBehaviors) behaviorCount++
|
|
73
95
|
if (result.hasBindings) bindingCount++
|
|
74
96
|
if (result.hasLists) listCount++
|
|
@@ -83,19 +105,37 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
83
105
|
const nativeEvents = [...new Set(plans.flatMap(plan => plan.events.filter(event => event.native).map(event => event.event)))].sort()
|
|
84
106
|
const hasTextBindings = plans.some(plan => plan.bindings.some(binding => binding.target === "text"))
|
|
85
107
|
const hasListConditions = plans.some(plan => plan.lists.some(list => list.conditions))
|
|
108
|
+
const hasListTextRanges = plans.some(plan => plan.lists.some(list => list.textRanges))
|
|
109
|
+
const hasListAttributes = plans.some(plan => plan.lists.some(list => list.attributes))
|
|
110
|
+
const hasListEvents = plans.some(plan => plan.lists.some(list => list.events))
|
|
111
|
+
const hasListExpressions = plans.some(plan => plan.lists.some(list => list.expressions))
|
|
112
|
+
const hasListExpressionAttributes = plans.some(plan => plan.lists.some(list => list.expressionAttributes))
|
|
113
|
+
const hasListSeeds = plans.some(plan => plan.lists.some(list => list.seed))
|
|
114
|
+
const hasListAsyncParts = hasListExpressions || hasListExpressionAttributes || hasListConditions
|
|
115
|
+
const hasListMounts = hasListConditions || plans.some(plan => plan.lists.some(list => list.mount))
|
|
86
116
|
const hasNestedStateCaptures = hasNestedCaptureState(plans)
|
|
87
117
|
const hasSetterCaptures = hasCaptureType(plans, "setter")
|
|
118
|
+
const hasEffectCaptures = plans.some(plan => plan.effects.some(effect => Object.keys(effect.scope).length))
|
|
88
119
|
const nativeModules = handlerModules.filter(module => module.hasNativeHandlers).map(module => assetPath(base, `assets/${module.path}`))
|
|
89
120
|
const hasNativeHandlers = nativeModules.length > 0
|
|
121
|
+
const hasEffects = effectEntries.length > 0
|
|
90
122
|
if (behaviorCount) {
|
|
91
123
|
const runtimeFile = bindingCount || listCount || hasNativeHandlers ? "./shared-runtime.js" : "./runtime.js"
|
|
92
124
|
const runtime = specializeRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), commandEvents, stateSeedCount > 0)
|
|
93
125
|
await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
|
|
94
126
|
}
|
|
95
|
-
if (bindingCount || hasNativeHandlers) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify, {
|
|
127
|
+
if (bindingCount || hasNativeHandlers || hasEffectCaptures) await writeJavaScript(join(assetsDirectory, "kudzu-serialization.js"), await readFile(new URL("./serialization.js", import.meta.url), "utf8"), minify, {
|
|
96
128
|
"globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures),
|
|
97
129
|
"globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
|
|
98
130
|
})
|
|
131
|
+
if (hasEffects) {
|
|
132
|
+
let effectRuntime = await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8")
|
|
133
|
+
effectRuntime = hasEffectCaptures ? effectRuntime.replace('"./serialization.js"', '"./kudzu-serialization.js"') : effectRuntime.replace(/^import[^\n]+\n/, "")
|
|
134
|
+
await writeBundledJavaScript(join(assetsDirectory, "kudzu-effect.js"), effectRuntime, minify, {
|
|
135
|
+
"globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures),
|
|
136
|
+
"globalThis.__KUDZU_EFFECT_CAPTURES__": String(hasEffectCaptures)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
99
139
|
if (bindingCount || listStyleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
|
|
100
140
|
if (bindingCount) {
|
|
101
141
|
const bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
|
|
@@ -118,7 +158,17 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
118
158
|
}`
|
|
119
159
|
listRuntime = listRuntime.replace(" /* list-style */", listStyleCount ? stylePatch : "")
|
|
120
160
|
if (listStyleCount) listRuntime = `import { serializeStyle } from "./kudzu-style.js"\n${listRuntime}`
|
|
121
|
-
await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), listRuntime, minify, {
|
|
161
|
+
await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), listRuntime, minify, {
|
|
162
|
+
__KUDZU_LIST_CONDITIONS__: String(hasListConditions),
|
|
163
|
+
__KUDZU_LIST_TEXT_RANGES__: String(hasListTextRanges),
|
|
164
|
+
__KUDZU_LIST_ATTRIBUTES__: String(hasListAttributes),
|
|
165
|
+
__KUDZU_LIST_EVENTS__: String(hasListEvents),
|
|
166
|
+
__KUDZU_LIST_EXPRESSIONS__: String(hasListExpressions),
|
|
167
|
+
__KUDZU_LIST_EXPRESSION_ATTRIBUTES__: String(hasListExpressionAttributes),
|
|
168
|
+
__KUDZU_LIST_SEEDS__: String(hasListSeeds),
|
|
169
|
+
__KUDZU_LIST_ASYNC_PARTS__: String(hasListAsyncParts),
|
|
170
|
+
__KUDZU_LIST_MOUNTS__: String(hasListMounts)
|
|
171
|
+
})
|
|
122
172
|
}
|
|
123
173
|
if (hasNativeHandlers) {
|
|
124
174
|
const nativeRuntime = (await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"))
|
|
@@ -133,6 +183,16 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
133
183
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
134
184
|
await writeJavaScript(output, handlerModule.code, minify)
|
|
135
185
|
}
|
|
186
|
+
for (const entry of paramEntries) {
|
|
187
|
+
const output = join(assetsDirectory, entry.path)
|
|
188
|
+
await mkdir(dirname(output), { recursive: true })
|
|
189
|
+
await writeJavaScript(output, printParamEntry(entry.schema, entry.params, output, assetsDirectory, base), minify)
|
|
190
|
+
}
|
|
191
|
+
for (const entry of effectEntries) {
|
|
192
|
+
const output = join(assetsDirectory, entry.path)
|
|
193
|
+
await mkdir(dirname(output), { recursive: true })
|
|
194
|
+
await writeJavaScript(output, printEffectEntry(entry.effects, output, handlerModules, assetsDirectory, base, entry.paramPath), minify)
|
|
195
|
+
}
|
|
136
196
|
const clientModules = await collectClientModules(handlerModules.flatMap(module => module.clientImports), sourceFileSet)
|
|
137
197
|
for (const file of clientModules) {
|
|
138
198
|
const output = join(assetsDirectory, clientModulePath(file))
|
|
@@ -157,7 +217,8 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
157
217
|
})
|
|
158
218
|
await rm(join(assetsDirectory, "modules"), { recursive: true, force: true })
|
|
159
219
|
}
|
|
160
|
-
|
|
220
|
+
const sortedRewrites = rewrites.sort((left, right) => runtimeSpecificity(right) - runtimeSpecificity(left) || left.pattern.localeCompare(right.pattern))
|
|
221
|
+
await writeFile(join(workDirectory, "kudzu-plan.json"), JSON.stringify({ routes: plans, rewrites: sortedRewrites }, null, 2))
|
|
161
222
|
for (const file of cssFiles) {
|
|
162
223
|
const output = join(assetsDirectory, relative(sourceDirectory, file))
|
|
163
224
|
await mkdir(dirname(output), { recursive: true })
|
|
@@ -166,7 +227,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
166
227
|
if (await exists(join(root, "public"))) await cp(join(root, "public"), outputDirectory, { recursive: true })
|
|
167
228
|
if (config.afterBuild !== undefined) {
|
|
168
229
|
if (typeof config.afterBuild !== "function") throw new Error("kudzu.config afterBuild must be a function")
|
|
169
|
-
await config.afterBuild({ root, outDir: outputDirectory, sourceDir: sourceDirectory, base, routes: plans.map(plan => plan.route), plans })
|
|
230
|
+
await config.afterBuild({ root, outDir: outputDirectory, sourceDir: sourceDirectory, base, routes: plans.map(plan => plan.route), plans, rewrites: sortedRewrites })
|
|
170
231
|
}
|
|
171
232
|
|
|
172
233
|
if (!quiet) console.log(`Built ${plans.length} page(s), ${behaviorCount} interactive page(s) into dist/`)
|
|
@@ -182,6 +243,69 @@ function specializeNativeRuntime(source, events, modules) {
|
|
|
182
243
|
return `${imports}\n${specializeEvents(source, events).replace(/const modules = new Map\(\[[^\n]*\]\)/, `const modules = new Map([${entries}])`)}`
|
|
183
244
|
}
|
|
184
245
|
|
|
246
|
+
function printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath) {
|
|
247
|
+
const moduleUrls = [...new Set(effects.map(effect => effect.module))]
|
|
248
|
+
const modules = moduleUrls.map(url => {
|
|
249
|
+
const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
|
|
250
|
+
if (!module) throw new Error(`Effect handler module was not emitted: ${url}`)
|
|
251
|
+
return module
|
|
252
|
+
})
|
|
253
|
+
const imports = [
|
|
254
|
+
`import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
|
|
255
|
+
`import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
|
|
256
|
+
...(paramPath ? [`import ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, paramPath)))}`] : []),
|
|
257
|
+
...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
|
|
258
|
+
]
|
|
259
|
+
const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
|
|
260
|
+
return `${imports.join("\n")}
|
|
261
|
+
const effects = ${inlineJson(effects)}
|
|
262
|
+
const modules = new Map([${entries}])
|
|
263
|
+
for (const effect of effects) {
|
|
264
|
+
try {
|
|
265
|
+
const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope))
|
|
266
|
+
if (result && typeof result.then === "function") result.catch(error => console.error(error))
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error(error)
|
|
269
|
+
}
|
|
270
|
+
}`
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function printParamEntry(schema, params, output, assetsDirectory, base) {
|
|
274
|
+
return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}
|
|
275
|
+
const base = ${inlineJson(browserPath(base).slice(1).split("/").filter(Boolean).map(segment => decodeURIComponent(segment)))}
|
|
276
|
+
const schema = ${inlineJson(schema.segments)}
|
|
277
|
+
const params = ${inlineJson(params)}
|
|
278
|
+
let path = location.pathname
|
|
279
|
+
if (base.length) {
|
|
280
|
+
const pathSegments = path.slice(1).split("/")
|
|
281
|
+
if (pathSegments.length < base.length || base.some((segment, index) => decodeSegment(pathSegments[index], false) !== segment)) throw new Error("Runtime route is outside the configured base")
|
|
282
|
+
path = "/" + pathSegments.slice(base.length).join("/")
|
|
283
|
+
}
|
|
284
|
+
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1)
|
|
285
|
+
const segments = path.slice(1).split("/")
|
|
286
|
+
if (segments.length !== schema.length) throw new Error("Runtime route does not match its fallback pattern")
|
|
287
|
+
const values = Object.create(null)
|
|
288
|
+
for (let index = 0; index < schema.length; index++) {
|
|
289
|
+
const segment = schema[index]
|
|
290
|
+
const value = decodeSegment(segments[index], Boolean(segment.param))
|
|
291
|
+
if (segment.literal !== undefined && value !== segment.literal) throw new Error("Runtime route literal does not match")
|
|
292
|
+
if (segment.param) values[segment.param] = value
|
|
293
|
+
}
|
|
294
|
+
for (const param of params) {
|
|
295
|
+
const value = values[param.name]
|
|
296
|
+
browserState.set(param.id, value)
|
|
297
|
+
commitDom(param.id, value)
|
|
298
|
+
}
|
|
299
|
+
function decodeSegment(raw, param) {
|
|
300
|
+
if (param && /%(?:2f|5c)/i.test(raw)) throw new Error("Runtime route parameter contains an encoded separator")
|
|
301
|
+
let value
|
|
302
|
+
try { value = decodeURIComponent(raw) } catch { throw new Error("Runtime route parameter has malformed encoding") }
|
|
303
|
+
const decodedDots = value.replace(/%2e/gi, ".")
|
|
304
|
+
if (param && (!value || value === "." || value === ".." || decodedDots === "." || decodedDots === ".." || /[\\/?#]/.test(value) || [...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159) || /%(?:2f|5c)/i.test(value))) throw new Error("Runtime route parameter is invalid")
|
|
305
|
+
return value
|
|
306
|
+
}`
|
|
307
|
+
}
|
|
308
|
+
|
|
185
309
|
function hasCaptureType(value, type) {
|
|
186
310
|
if (!value || typeof value !== "object") return false
|
|
187
311
|
if (value.type === type) return true
|
|
@@ -253,7 +377,8 @@ export async function dev({ port = parseDevPort(process.env.PORT) } = {}) {
|
|
|
253
377
|
const server = createServer(async (request, response) => {
|
|
254
378
|
try {
|
|
255
379
|
const url = new URL(request.url, "http://localhost")
|
|
256
|
-
const
|
|
380
|
+
const rawPathname = url.pathname
|
|
381
|
+
const pathname = decodeURIComponent(rawPathname)
|
|
257
382
|
if (pathname === "/__kudzu_reload") {
|
|
258
383
|
response.writeHead(200, {
|
|
259
384
|
"content-type": "text/event-stream; charset=utf-8",
|
|
@@ -273,15 +398,24 @@ export async function dev({ port = parseDevPort(process.env.PORT) } = {}) {
|
|
|
273
398
|
return
|
|
274
399
|
}
|
|
275
400
|
|
|
276
|
-
const relativePath =
|
|
401
|
+
const relativePath = stripBaseStrict(pathname, decodeURIComponent(base)).replace(/^\/+/, "")
|
|
277
402
|
let file = resolve(outputDirectory, relativePath)
|
|
278
403
|
if (!file.startsWith(`${outputDirectory}${sep}`) && file !== outputDirectory) throw new Error("Invalid path")
|
|
279
404
|
|
|
280
405
|
if ((await exists(file)) && (await stat(file)).isDirectory()) file = join(file, "index.html")
|
|
281
406
|
if (!(await exists(file)) && !extname(file)) file = join(file, "index.html")
|
|
407
|
+
let matchedRoute
|
|
408
|
+
if (!(await exists(file)) && !buildError) {
|
|
409
|
+
const plan = JSON.parse(await readFile(join(workDirectory, "kudzu-plan.json"), "utf8"))
|
|
410
|
+
const rewrite = plan.rewrites?.find(entry => runtimePathValues(rawPathname, entry, browserPath(base)))
|
|
411
|
+
if (rewrite) {
|
|
412
|
+
file = resolve(outputDirectory, rewrite.file)
|
|
413
|
+
matchedRoute = rewrite.pattern
|
|
414
|
+
}
|
|
415
|
+
}
|
|
282
416
|
const isHtml = extname(file) === ".html"
|
|
283
417
|
const content = isHtml
|
|
284
|
-
? injectDevClient(buildError ? errorPage(buildError) : await readFile(file, "utf8"), session, revision, buildError ? [] : await devSchema(pathname))
|
|
418
|
+
? injectDevClient(buildError ? errorPage(buildError) : await readFile(file, "utf8"), session, revision, buildError ? [] : await devSchema(withBase(base, stripBaseStrict(pathname, decodeURIComponent(base))), matchedRoute))
|
|
285
419
|
: await readFile(file)
|
|
286
420
|
response.writeHead(200, {
|
|
287
421
|
"content-type": contentType(file),
|
|
@@ -334,22 +468,58 @@ function injectDevClient(html, session, revision, schema) {
|
|
|
334
468
|
return `${html}${devClient(session, revision, schema)}`
|
|
335
469
|
}
|
|
336
470
|
|
|
337
|
-
function
|
|
471
|
+
function stripBaseStrict(path, base) {
|
|
338
472
|
if (!base) return path
|
|
339
473
|
if (path === base) return "/"
|
|
340
|
-
|
|
474
|
+
if (path.startsWith(`${base}/`)) return path.slice(base.length)
|
|
475
|
+
throw new Error("Path is outside the configured base")
|
|
341
476
|
}
|
|
342
477
|
|
|
343
|
-
async function devSchema(pathname) {
|
|
478
|
+
async function devSchema(pathname, matchedRoute) {
|
|
344
479
|
try {
|
|
345
480
|
const plan = JSON.parse(await readFile(join(workDirectory, "kudzu-plan.json"), "utf8"))
|
|
346
|
-
const route = pathname.replace(/\/(?:index\.html)?$/, "") || "/"
|
|
481
|
+
const route = matchedRoute ?? (pathname.replace(/\/(?:index\.html)?$/, "") || "/")
|
|
347
482
|
return stateSchema(plan.routes.find(entry => entry.route === route)?.states ?? [])
|
|
348
483
|
} catch {
|
|
349
484
|
return []
|
|
350
485
|
}
|
|
351
486
|
}
|
|
352
487
|
|
|
488
|
+
function runtimePathValues(pathname, rewrite, base) {
|
|
489
|
+
try {
|
|
490
|
+
let path = stripBrowserBase(pathname, base)
|
|
491
|
+
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1)
|
|
492
|
+
const rawSegments = path.slice(1).split("/")
|
|
493
|
+
if (rawSegments.length !== rewrite.segments.length) return undefined
|
|
494
|
+
const values = Object.create(null)
|
|
495
|
+
for (let index = 0; index < rewrite.segments.length; index++) {
|
|
496
|
+
const segment = rewrite.segments[index]
|
|
497
|
+
const value = decodeRuntimeSegment(rawSegments[index], Boolean(segment.param))
|
|
498
|
+
if (segment.literal !== undefined && value !== segment.literal) return undefined
|
|
499
|
+
if (segment.param) values[segment.param] = value
|
|
500
|
+
}
|
|
501
|
+
return values
|
|
502
|
+
} catch {
|
|
503
|
+
return undefined
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function stripBrowserBase(path, base) {
|
|
508
|
+
if (!base) return path
|
|
509
|
+
const pathSegments = path.slice(1).split("/")
|
|
510
|
+
const baseSegments = base.slice(1).split("/").map(segment => decodeURIComponent(segment))
|
|
511
|
+
if (pathSegments.length < baseSegments.length || baseSegments.some((segment, index) => decodeRuntimeSegment(pathSegments[index], false) !== segment)) throw new Error("Path is outside the configured base")
|
|
512
|
+
return `/${pathSegments.slice(baseSegments.length).join("/")}`
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function decodeRuntimeSegment(raw, param) {
|
|
516
|
+
if (param && /%(?:2f|5c)/i.test(raw)) throw new Error("Encoded separator")
|
|
517
|
+
const value = decodeURIComponent(raw)
|
|
518
|
+
const decodedDots = value.replace(/%2e/gi, ".")
|
|
519
|
+
if (param && (!value || value === "." || value === ".." || decodedDots === "." || decodedDots === ".." || /[\\/?#]/.test(value) || [...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159) || /%(?:2f|5c)/i.test(value))) throw new Error("Invalid runtime parameter")
|
|
520
|
+
return value
|
|
521
|
+
}
|
|
522
|
+
|
|
353
523
|
function inlineJson(value) {
|
|
354
524
|
return JSON.stringify(value).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029")
|
|
355
525
|
}
|
|
@@ -373,6 +543,7 @@ function escapeHtml(value) {
|
|
|
373
543
|
async function compile(file, sourceFiles, sourceIndex, base) {
|
|
374
544
|
const source = sourceIndex.get(file)
|
|
375
545
|
const nativeHandlers = []
|
|
546
|
+
const effectHandlers = []
|
|
376
547
|
const reactiveBindings = []
|
|
377
548
|
const listExpressions = []
|
|
378
549
|
const clientImports = new Set()
|
|
@@ -385,7 +556,7 @@ async function compile(file, sourceFiles, sourceIndex, base) {
|
|
|
385
556
|
jsx: ts.JsxEmit.ReactJSX,
|
|
386
557
|
jsxImportSource: "@kudzujs/core"
|
|
387
558
|
},
|
|
388
|
-
transformers: { before: [createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, clientImports)] },
|
|
559
|
+
transformers: { before: [createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, assetPath(base, `assets/${handlerPath}`), file, sourceFiles, sourceIndex, clientImports)] },
|
|
389
560
|
reportDiagnostics: true
|
|
390
561
|
})
|
|
391
562
|
|
|
@@ -398,10 +569,11 @@ async function compile(file, sourceFiles, sourceIndex, base) {
|
|
|
398
569
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
399
570
|
await writeFile(output, result.outputText)
|
|
400
571
|
|
|
401
|
-
if (!nativeHandlers.length && !reactiveBindings.length && !listExpressions.length) return undefined
|
|
572
|
+
if (!nativeHandlers.length && !effectHandlers.length && !reactiveBindings.length && !listExpressions.length) return undefined
|
|
573
|
+
const callbacks = [...nativeHandlers, ...effectHandlers]
|
|
402
574
|
const moduleSource = [
|
|
403
|
-
printClientImports(
|
|
404
|
-
...
|
|
575
|
+
printClientImports(callbacks.flatMap(handler => handler.imports), handlerPath),
|
|
576
|
+
...callbacks.map(handler => printNativeHandler(handler)),
|
|
405
577
|
...reactiveBindings.map(entry => printReactiveBinding(entry)),
|
|
406
578
|
...listExpressions.map(entry => printListExpression(entry))
|
|
407
579
|
].join("\n")
|
|
@@ -411,15 +583,16 @@ async function compile(file, sourceFiles, sourceIndex, base) {
|
|
|
411
583
|
})
|
|
412
584
|
const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
|
|
413
585
|
if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
|
|
414
|
-
return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, clientImports: [...clientImports] }
|
|
586
|
+
return { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, hasEffects: effectHandlers.length > 0, clientImports: [...clientImports] }
|
|
415
587
|
}
|
|
416
588
|
|
|
417
|
-
function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, clientImports) {
|
|
589
|
+
function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, clientImports) {
|
|
418
590
|
return context => sourceFile => {
|
|
419
591
|
const factory = context.factory
|
|
420
592
|
sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
|
|
421
593
|
ts.setParentRecursive(sourceFile, false)
|
|
422
594
|
const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
|
|
595
|
+
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && statement.moduleSpecifier.text === "@kudzujs/core" && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
|
|
423
596
|
const importedSources = new Map()
|
|
424
597
|
const importedSource = target => {
|
|
425
598
|
let imported = importedSources.get(target)
|
|
@@ -628,6 +801,31 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
628
801
|
return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
|
|
629
802
|
}
|
|
630
803
|
|
|
804
|
+
if (hasUseEffectImport && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useEffect") {
|
|
805
|
+
if (node.arguments.length !== 2) fail(node, "useEffect() requires exactly a callback and literal empty dependency array")
|
|
806
|
+
const [callback, dependencies] = node.arguments
|
|
807
|
+
if (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) fail(callback, "useEffect() callback must be an inline function")
|
|
808
|
+
if (ts.isFunctionExpression(callback) && callback.name) fail(callback, "useEffect() callback function must be anonymous")
|
|
809
|
+
if (callback.parameters.length) fail(callback, "useEffect() callback cannot declare parameters")
|
|
810
|
+
if (!ts.isArrayLiteralExpression(dependencies) || dependencies.elements.length) fail(dependencies, "useEffect() dependencies must be a literal empty array")
|
|
811
|
+
if (!nearestFunction(node)) fail(node, "useEffect() cannot be used outside a Kudzu component")
|
|
812
|
+
if (returnsCleanup(callback)) fail(callback, "useEffect() cleanup functions are not supported")
|
|
813
|
+
if (!ts.isBlock(callback.body)) fail(callback, "useEffect() callback must use a block body")
|
|
814
|
+
if (returnsEffectValue(callback)) fail(callback, "useEffect() return values are not supported")
|
|
815
|
+
const setters = settersForNode(node, settersByFunction)
|
|
816
|
+
const descriptor = compileNativeCallback(callback, setters, factory, effectHandlers, importBindings, clientImports, "effect", undefined, true)
|
|
817
|
+
usesBehavior = true
|
|
818
|
+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
819
|
+
callback,
|
|
820
|
+
dependencies,
|
|
821
|
+
factory.createStringLiteral(handlerUrl),
|
|
822
|
+
factory.createStringLiteral(descriptor.exportName),
|
|
823
|
+
descriptor.states,
|
|
824
|
+
descriptor.scope,
|
|
825
|
+
factory.createStringLiteral(sourceLocation(node, sourceFile))
|
|
826
|
+
])
|
|
827
|
+
}
|
|
828
|
+
|
|
631
829
|
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) {
|
|
632
830
|
const stateElement = node.name.elements[0]
|
|
633
831
|
if (!stateElement || !ts.isBindingElement(stateElement) || !ts.isIdentifier(stateElement.name)) return node
|
|
@@ -1273,26 +1471,37 @@ function compileEvent(expression, setters, functions, factory, nativeHandlers, h
|
|
|
1273
1471
|
const optimized = compileOptimizedEvent(expression, setters, factory)
|
|
1274
1472
|
if (optimized) return optimized
|
|
1275
1473
|
|
|
1474
|
+
const descriptor = compileNativeCallback(expression, setters, factory, nativeHandlers, importBindings, clientImports, "handler", listItem)
|
|
1475
|
+
return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
|
|
1476
|
+
factory.createStringLiteral(handlerUrl),
|
|
1477
|
+
factory.createStringLiteral(descriptor.exportName),
|
|
1478
|
+
descriptor.states,
|
|
1479
|
+
descriptor.scope
|
|
1480
|
+
])
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function compileNativeCallback(expression, setters, factory, entries, importBindings, clientImports, prefix, listItem, deferValues = false) {
|
|
1276
1484
|
const allCaptures = nativeCaptureNames(expression, setters)
|
|
1277
1485
|
const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
|
|
1278
1486
|
const captures = new Set([...allCaptures].filter(name => !importBindings.has(name)))
|
|
1279
1487
|
for (const entry of imports) clientImports.add(entry.target)
|
|
1280
1488
|
const usedStates = nativeStateNames(expression, setters)
|
|
1281
|
-
const exportName =
|
|
1282
|
-
|
|
1283
|
-
const
|
|
1284
|
-
factory.
|
|
1285
|
-
factory.createIdentifier(name)
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
factory.
|
|
1289
|
-
factory.createStringLiteral(exportName),
|
|
1290
|
-
factory.createArrayLiteralExpression(states),
|
|
1291
|
-
factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
|
|
1489
|
+
const exportName = `${prefix}${entries.length}`
|
|
1490
|
+
entries.push({ exportName, expression, captures, imports, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))) })
|
|
1491
|
+
const value = name => deferValues
|
|
1492
|
+
? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
|
|
1493
|
+
: factory.createIdentifier(name)
|
|
1494
|
+
return {
|
|
1495
|
+
exportName,
|
|
1496
|
+
states: factory.createArrayLiteralExpression([...usedStates].map(name => factory.createArrayLiteralExpression([
|
|
1292
1497
|
factory.createStringLiteral(name),
|
|
1293
|
-
|
|
1498
|
+
value(name)
|
|
1499
|
+
]))),
|
|
1500
|
+
scope: factory.createArrayLiteralExpression([...captures].map(name => factory.createArrayLiteralExpression([
|
|
1501
|
+
factory.createStringLiteral(name),
|
|
1502
|
+
name === listItem ? factory.createCallExpression(factory.createIdentifier("__kListItem"), undefined, []) : value(name)
|
|
1294
1503
|
])))
|
|
1295
|
-
|
|
1504
|
+
}
|
|
1296
1505
|
}
|
|
1297
1506
|
|
|
1298
1507
|
function nativeStateNames(expression, setters) {
|
|
@@ -1303,7 +1512,8 @@ function referencedStateNames(root, setters, scopeRoot = root) {
|
|
|
1303
1512
|
const stateNames = new Set(setters.values())
|
|
1304
1513
|
const used = new Set()
|
|
1305
1514
|
const visit = node => {
|
|
1306
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text)) used.add(setters.get(node.expression.text))
|
|
1515
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, scopeRoot)) used.add(setters.get(node.expression.text))
|
|
1516
|
+
if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(setters.get(node.text))
|
|
1307
1517
|
if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, scopeRoot)) used.add(node.text)
|
|
1308
1518
|
ts.forEachChild(node, visit)
|
|
1309
1519
|
}
|
|
@@ -1342,20 +1552,22 @@ function referencedImportedBindings(expression, imports) {
|
|
|
1342
1552
|
|
|
1343
1553
|
function captureNames(declarationRoot, referenceRoot, setters) {
|
|
1344
1554
|
const local = new Set()
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1555
|
+
if (!isFunctionLike(declarationRoot)) {
|
|
1556
|
+
const collectDeclarations = node => {
|
|
1557
|
+
if (ts.isVariableDeclaration(node)) for (const name of bindingNames(node.name)) local.add(name)
|
|
1558
|
+
if (ts.isParameter(node)) for (const name of bindingNames(node.name)) local.add(name)
|
|
1559
|
+
if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) local.add(node.name.text)
|
|
1560
|
+
ts.forEachChild(node, collectDeclarations)
|
|
1561
|
+
}
|
|
1562
|
+
collectDeclarations(declarationRoot)
|
|
1350
1563
|
}
|
|
1351
|
-
collectDeclarations(declarationRoot)
|
|
1352
|
-
|
|
1353
1564
|
const stateNames = new Set(setters.values())
|
|
1354
1565
|
const captures = new Set()
|
|
1355
1566
|
const visit = node => {
|
|
1356
1567
|
if (ts.isTypeNode(node)) return
|
|
1357
|
-
if (ts.isIdentifier(node)
|
|
1358
|
-
|
|
1568
|
+
if (ts.isIdentifier(node)) {
|
|
1569
|
+
const declared = isFunctionLike(declarationRoot) ? isShadowedIdentifier(node, declarationRoot) : local.has(node.text)
|
|
1570
|
+
if (isReferenceIdentifier(node) && !declared && !setters.has(node.text) && !stateNames.has(node.text) && !nativeGlobals.has(node.text)) captures.add(node.text)
|
|
1359
1571
|
}
|
|
1360
1572
|
ts.forEachChild(node, visit)
|
|
1361
1573
|
}
|
|
@@ -1377,7 +1589,7 @@ function isReferenceIdentifier(node) {
|
|
|
1377
1589
|
(ts.isVariableDeclaration(parent) && parent.name === node) ||
|
|
1378
1590
|
(ts.isParameter(parent) && parent.name === node) ||
|
|
1379
1591
|
(ts.isFunctionDeclaration(parent) && parent.name === node) ||
|
|
1380
|
-
(ts.isBindingElement(parent) && parent.name === node) ||
|
|
1592
|
+
(ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node)) ||
|
|
1381
1593
|
ts.isImportSpecifier(parent) || ts.isImportClause(parent)) return false
|
|
1382
1594
|
return true
|
|
1383
1595
|
}
|
|
@@ -1397,6 +1609,43 @@ function isShadowedByParameter(node, scopeRoot) {
|
|
|
1397
1609
|
return false
|
|
1398
1610
|
}
|
|
1399
1611
|
|
|
1612
|
+
function isShadowedIdentifier(node, scopeRoot) {
|
|
1613
|
+
if (isShadowedByParameter(node, scopeRoot)) return true
|
|
1614
|
+
if (node === scopeRoot) return false
|
|
1615
|
+
if (isFunctionLike(scopeRoot) && scopeRoot.name?.text === node.text) return true
|
|
1616
|
+
if (isFunctionLike(scopeRoot) && functionVarDeclaresName(scopeRoot, node.text)) return true
|
|
1617
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
1618
|
+
if (current === scopeRoot) break
|
|
1619
|
+
if (ts.isBlock(current) && current.statements.some(statement => statementDeclaresName(statement, node.text))) return true
|
|
1620
|
+
if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statementDeclaresName(statement, node.text)))) return true
|
|
1621
|
+
if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
|
|
1622
|
+
if ((ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) && loopDeclaresName(current, node.text)) return true
|
|
1623
|
+
if (isFunctionLike(current) && functionVarDeclaresName(current, node.text)) return true
|
|
1624
|
+
}
|
|
1625
|
+
return false
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function statementDeclaresName(statement, name) {
|
|
1629
|
+
if (ts.isVariableStatement(statement)) return statement.declarationList.declarations.some(declaration => bindingNames(declaration.name).includes(name))
|
|
1630
|
+
return (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name?.text === name
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function loopDeclaresName(loop, name) {
|
|
1634
|
+
const declaration = ts.isForStatement(loop) ? loop.initializer : loop.initializer
|
|
1635
|
+
return declaration && ts.isVariableDeclarationList(declaration) && declaration.declarations.some(entry => bindingNames(entry.name).includes(name))
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function functionVarDeclaresName(fn, name) {
|
|
1639
|
+
let found = false
|
|
1640
|
+
const visit = node => {
|
|
1641
|
+
if (found || node !== fn.body && isFunctionLike(node)) return
|
|
1642
|
+
if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.BlockScoped) === 0 && node.declarations.some(entry => bindingNames(entry.name).includes(name))) found = true
|
|
1643
|
+
if (!found) ts.forEachChild(node, visit)
|
|
1644
|
+
}
|
|
1645
|
+
if (fn.body) visit(fn.body)
|
|
1646
|
+
return found
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1400
1649
|
function settersForNode(node, settersByFunction) {
|
|
1401
1650
|
for (let current = node.parent; current; current = current.parent) {
|
|
1402
1651
|
if (!ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) continue
|
|
@@ -1477,6 +1726,42 @@ function sourceNodeError(node, fallbackSource, message) {
|
|
|
1477
1726
|
return new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
|
|
1478
1727
|
}
|
|
1479
1728
|
|
|
1729
|
+
function sourceLocation(node, fallbackSource) {
|
|
1730
|
+
const original = ts.getOriginalNode(node)
|
|
1731
|
+
const sourceFile = original.getSourceFile?.()?.fileName ? original.getSourceFile() : fallbackSource
|
|
1732
|
+
const position = sourceFile.getLineAndCharacterOfPosition(original.getStart(sourceFile))
|
|
1733
|
+
return `${sourceFile.fileName}:${position.line + 1}:${position.character + 1}`
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function returnsCleanup(callback) {
|
|
1737
|
+
if (ts.isArrowFunction(callback) && !ts.isBlock(callback.body)) {
|
|
1738
|
+
const body = unwrapExpression(callback.body)
|
|
1739
|
+
return ts.isArrowFunction(body) || ts.isFunctionExpression(body)
|
|
1740
|
+
}
|
|
1741
|
+
let found = false
|
|
1742
|
+
const visit = node => {
|
|
1743
|
+
if (found || node !== callback.body && isFunctionLike(node)) return
|
|
1744
|
+
if (ts.isReturnStatement(node) && node.expression) {
|
|
1745
|
+
const expression = unwrapExpression(node.expression)
|
|
1746
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) found = true
|
|
1747
|
+
}
|
|
1748
|
+
if (!found) ts.forEachChild(node, visit)
|
|
1749
|
+
}
|
|
1750
|
+
visit(callback.body)
|
|
1751
|
+
return found
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function returnsEffectValue(callback) {
|
|
1755
|
+
let found = false
|
|
1756
|
+
const visit = node => {
|
|
1757
|
+
if (found || node !== callback.body && isFunctionLike(node)) return
|
|
1758
|
+
if (ts.isReturnStatement(node) && node.expression) found = true
|
|
1759
|
+
if (!found) ts.forEachChild(node, visit)
|
|
1760
|
+
}
|
|
1761
|
+
visit(callback.body)
|
|
1762
|
+
return found
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1480
1765
|
function printClientImports(entries, handlerPath) {
|
|
1481
1766
|
const unique = new Map(entries.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry]))
|
|
1482
1767
|
const groups = Map.groupBy(unique.values(), entry => entry.target)
|
|
@@ -1594,24 +1879,33 @@ function printNativeHandler({ exportName, expression, captures, setters }) {
|
|
|
1594
1879
|
const stateNames = new Set(setters.values())
|
|
1595
1880
|
const transformer = context => root => {
|
|
1596
1881
|
const visitor = node => {
|
|
1597
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text)) {
|
|
1882
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
|
|
1598
1883
|
return factory.createCallExpression(
|
|
1599
1884
|
factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"),
|
|
1600
1885
|
undefined,
|
|
1601
1886
|
[factory.createStringLiteral(setters.get(node.expression.text)), ...node.arguments.map(argument => ts.visitNode(argument, visitor))]
|
|
1602
1887
|
)
|
|
1603
1888
|
}
|
|
1604
|
-
if (ts.
|
|
1889
|
+
if (ts.isShorthandPropertyAssignment(node) && setters.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
|
|
1890
|
+
return factory.createPropertyAssignment(node.name, setterReference(factory, setters.get(node.name.text)))
|
|
1891
|
+
}
|
|
1892
|
+
if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
|
|
1893
|
+
return setterReference(factory, setters.get(node.text))
|
|
1894
|
+
}
|
|
1895
|
+
if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
|
|
1896
|
+
return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
|
|
1897
|
+
}
|
|
1898
|
+
if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
|
|
1605
1899
|
return factory.createCallExpression(
|
|
1606
1900
|
factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
|
|
1607
1901
|
undefined,
|
|
1608
1902
|
[factory.createStringLiteral(node.text)]
|
|
1609
1903
|
)
|
|
1610
1904
|
}
|
|
1611
|
-
if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) {
|
|
1905
|
+
if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
|
|
1612
1906
|
return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
|
|
1613
1907
|
}
|
|
1614
|
-
if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) {
|
|
1908
|
+
if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
|
|
1615
1909
|
return scopeRead(factory, node.text)
|
|
1616
1910
|
}
|
|
1617
1911
|
return ts.visitEachChild(node, visitor, context)
|
|
@@ -1640,6 +1934,17 @@ function printNativeHandler({ exportName, expression, captures, setters }) {
|
|
|
1640
1934
|
}
|
|
1641
1935
|
}
|
|
1642
1936
|
|
|
1937
|
+
function setterReference(factory, stateName) {
|
|
1938
|
+
return factory.createArrowFunction(
|
|
1939
|
+
undefined,
|
|
1940
|
+
undefined,
|
|
1941
|
+
[factory.createParameterDeclaration(undefined, undefined, "value")],
|
|
1942
|
+
undefined,
|
|
1943
|
+
factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
1944
|
+
factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(stateName), factory.createIdentifier("value")])
|
|
1945
|
+
)
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1643
1948
|
function printReactiveBinding({ exportName, expression, captures, states }) {
|
|
1644
1949
|
const factory = ts.factory
|
|
1645
1950
|
const transformer = context => root => {
|
|
@@ -1762,10 +2067,17 @@ async function loadConfig() {
|
|
|
1762
2067
|
|
|
1763
2068
|
function normalizeBase(value) {
|
|
1764
2069
|
if (value == null || value === "" || value === "/") return ""
|
|
1765
|
-
if (typeof value !== "string" || !value.startsWith("/") || /[?#\0]/.test(value) ||
|
|
2070
|
+
if (typeof value !== "string" || !value.startsWith("/") || /[?#\0]/.test(value) || /%(?:2f|5c)/i.test(value)) throw new Error("kudzu.config base must be a root-relative path")
|
|
2071
|
+
let decoded
|
|
2072
|
+
try { decoded = decodeURIComponent(value) } catch { throw new Error("kudzu.config base must be a root-relative path") }
|
|
2073
|
+
if (/[\\?#\0]/.test(decoded) || decoded.split("/").includes("..") || [...decoded].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159)) throw new Error("kudzu.config base must be a root-relative path")
|
|
1766
2074
|
return value.replace(/\/+$/, "")
|
|
1767
2075
|
}
|
|
1768
2076
|
|
|
2077
|
+
function browserPath(path) {
|
|
2078
|
+
return path ? new URL(path, "http://kudzu.local").pathname : ""
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1769
2081
|
function assetPath(base, path) {
|
|
1770
2082
|
return `${base}/${path}`
|
|
1771
2083
|
}
|
|
@@ -1788,6 +2100,43 @@ async function staticPathEntries(module, file) {
|
|
|
1788
2100
|
})
|
|
1789
2101
|
}
|
|
1790
2102
|
|
|
2103
|
+
function runtimeRouteSchema(module, file) {
|
|
2104
|
+
if (!Object.hasOwn(module, "runtimeParams")) return undefined
|
|
2105
|
+
if (module.runtimeParams !== true) throw new Error(`${relative(root, file)} runtimeParams must be exactly true`)
|
|
2106
|
+
if (typeof module.getStaticPaths === "function") throw new Error(`${relative(root, file)} runtimeParams cannot be combined with getStaticPaths()`)
|
|
2107
|
+
const route = pageRoutePattern(file)
|
|
2108
|
+
if (route.includes("[...")) throw new Error(`Catch-all routes are not supported: ${route}`)
|
|
2109
|
+
const names = new Set()
|
|
2110
|
+
const segments = route.split("/").map(segment => {
|
|
2111
|
+
const match = segment.match(/^\[([^\]]+)\]$/)
|
|
2112
|
+
if (!match) {
|
|
2113
|
+
if (/[\[\]]/.test(segment)) throw new Error(`${relative(root, file)} runtime parameters must occupy a complete path segment`)
|
|
2114
|
+
return { literal: segment }
|
|
2115
|
+
}
|
|
2116
|
+
const name = match[1]
|
|
2117
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) || ["__proto__", "constructor", "prototype"].includes(name)) throw new Error(`${relative(root, file)} invalid runtime parameter name ${JSON.stringify(name)}`)
|
|
2118
|
+
if (names.has(name)) throw new Error(`${relative(root, file)} duplicate runtime parameter ${JSON.stringify(name)}`)
|
|
2119
|
+
names.add(name)
|
|
2120
|
+
return { param: name }
|
|
2121
|
+
})
|
|
2122
|
+
if (!names.size) throw new Error(`${relative(root, file)} runtimeParams requires a bracket page`)
|
|
2123
|
+
return { route, segments, params: [...names] }
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
function pageRoutePattern(file) {
|
|
2127
|
+
const page = relative(pagesDirectory, file).replace(/\\/g, "/").replace(/\.tsx$/, "")
|
|
2128
|
+
return page === "index" ? "" : page.replace(/\/index$/, "")
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function runtimeSpecificity(schema) {
|
|
2132
|
+
return schema.segments.filter(segment => segment.literal !== undefined).length
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function sameRuntimePrecedence(left, right) {
|
|
2136
|
+
if (left.segments.length !== right.segments.length || runtimeSpecificity(left) !== runtimeSpecificity(right)) return false
|
|
2137
|
+
return left.segments.every((segment, index) => segment.literal === undefined || right.segments[index].literal === undefined || segment.literal === right.segments[index].literal)
|
|
2138
|
+
}
|
|
2139
|
+
|
|
1791
2140
|
function routeFromPage(file, params = {}) {
|
|
1792
2141
|
const page = relative(pagesDirectory, file).replace(/\\/g, "/").replace(/\.tsx$/, "")
|
|
1793
2142
|
if (page.includes("[...")) throw new Error(`Catch-all routes are not supported: ${page}`)
|
package/framework/core.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type StateSetter<T> = (value: T | ((previous: T) => T)) => void
|
|
2
2
|
|
|
3
3
|
export function useState<T>(initialValue: T): [T, StateSetter<T>]
|
|
4
|
+
export function useEffect(effect: () => void | Promise<void>, dependencies: readonly []): void
|
|
5
|
+
export function useParams<Params extends Record<string, string> = Record<string, string>>(): Readonly<Params>
|
|
4
6
|
|
|
5
7
|
export interface RefObject<T> {
|
|
6
8
|
readonly current: T | null
|
|
@@ -46,22 +48,29 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
46
48
|
manifest?: string
|
|
47
49
|
styles?: boolean | string[]
|
|
48
50
|
base?: string
|
|
51
|
+
effectAsset?: string
|
|
52
|
+
paramAsset?: string
|
|
53
|
+
runtimeParams?: string[]
|
|
49
54
|
},
|
|
50
55
|
props?: Props
|
|
51
56
|
): Promise<{
|
|
52
57
|
html: string
|
|
53
58
|
hasBehaviors: boolean
|
|
59
|
+
hasEffects: boolean
|
|
60
|
+
hasParams: boolean
|
|
54
61
|
hasBindings: boolean
|
|
55
62
|
hasLists: boolean
|
|
56
63
|
hasListStyles: boolean
|
|
57
64
|
hasStateSeed: boolean
|
|
58
65
|
plan: {
|
|
59
66
|
states: Array<{ id: string; name: string; initialValue: unknown }>
|
|
67
|
+
params: Array<{ name: string; id: string }>
|
|
60
68
|
events: Array<{
|
|
61
69
|
event: string
|
|
62
70
|
commands?: Array<[string, string, unknown]>
|
|
63
71
|
native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
|
|
64
72
|
}>
|
|
73
|
+
effects: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }>
|
|
65
74
|
bindings: Array<{
|
|
66
75
|
target: string
|
|
67
76
|
state?: string
|
package/framework/core.mjs
CHANGED
|
@@ -24,10 +24,37 @@ export function useState(initialValue, name) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const id = `s${renderContext.nextState++}`
|
|
27
|
-
const signal =
|
|
27
|
+
const signal = createSignal(id, initialValue)
|
|
28
|
+
|
|
29
|
+
const setter = () => {
|
|
30
|
+
throw new Error("State setters are compiled into ordered browser behaviors")
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(setter, setterMarker, { value: id })
|
|
33
|
+
renderContext.states[id] = { name: name ?? id, initialValue }
|
|
34
|
+
return [signal, setter]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useParams() {
|
|
38
|
+
if (!renderContext?.runtimeParamNames?.length) throw new Error("useParams() requires export const runtimeParams = true on a bracket page")
|
|
39
|
+
if (!renderContext.params) {
|
|
40
|
+
const params = Object.create(null)
|
|
41
|
+
renderContext.paramEntries = renderContext.runtimeParamNames.map((name, index) => {
|
|
42
|
+
const id = `p${index}`
|
|
43
|
+
params[name] = createSignal(id, "")
|
|
44
|
+
return { name, id }
|
|
45
|
+
})
|
|
46
|
+
renderContext.params = Object.freeze(params)
|
|
47
|
+
renderContext.hasBehaviors = true
|
|
48
|
+
renderContext.hasParams = true
|
|
49
|
+
}
|
|
50
|
+
return renderContext.params
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createSignal(id, value) {
|
|
54
|
+
return {
|
|
28
55
|
[signalMarker]: true,
|
|
29
56
|
id,
|
|
30
|
-
value
|
|
57
|
+
value,
|
|
31
58
|
valueOf() {
|
|
32
59
|
return this.value
|
|
33
60
|
},
|
|
@@ -35,13 +62,16 @@ export function useState(initialValue, name) {
|
|
|
35
62
|
return String(this.value)
|
|
36
63
|
}
|
|
37
64
|
}
|
|
65
|
+
}
|
|
38
66
|
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
export function useEffect(callback, dependencies, module, handler, states, scope, source) {
|
|
68
|
+
if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
|
|
69
|
+
if (typeof callback !== "function" || !Array.isArray(dependencies) || dependencies.length || !module || !handler) {
|
|
70
|
+
throw new Error("useEffect() must be compiled with a literal empty dependency array")
|
|
41
71
|
}
|
|
42
|
-
|
|
43
|
-
renderContext.
|
|
44
|
-
|
|
72
|
+
renderContext.effects.push({ module, handler, states, scope, source })
|
|
73
|
+
renderContext.hasBehaviors = true
|
|
74
|
+
renderContext.hasEffects = true
|
|
45
75
|
}
|
|
46
76
|
|
|
47
77
|
export function useRef(initialValue) {
|
|
@@ -82,8 +112,14 @@ export function nativeBehavior(module, handler, states, scope) {
|
|
|
82
112
|
[nativeBehaviorMarker]: true,
|
|
83
113
|
module,
|
|
84
114
|
handler,
|
|
115
|
+
...nativeDescriptor(states, scope)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function nativeDescriptor(states, scope) {
|
|
120
|
+
return {
|
|
85
121
|
states: Object.fromEntries(states.map(([name, signal]) => {
|
|
86
|
-
if (!signal?.[signalMarker]) throw new Error("A native
|
|
122
|
+
if (!signal?.[signalMarker]) throw new Error("A native callback must target framework state")
|
|
87
123
|
return [name, signal.id]
|
|
88
124
|
})),
|
|
89
125
|
scope: Object.fromEntries(scope.map(([name, value]) => [name, value?.[signalMarker] ? { type: "state", id: value.id } : serializeCapture(name, value, new Set())]))
|
|
@@ -226,10 +262,21 @@ function serializeCapture(name, value, seen) {
|
|
|
226
262
|
}
|
|
227
263
|
|
|
228
264
|
export async function renderPage(component, metadata = {}, props = {}) {
|
|
229
|
-
renderContext = { nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], bindings: [], textBindings: [], conditions: [], lists: [], hasBehaviors: false, hasNativeBehaviors: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
265
|
+
renderContext = { nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
230
266
|
|
|
231
267
|
try {
|
|
232
268
|
const body = await renderNode({ type: component, props })
|
|
269
|
+
renderContext.effects = renderContext.effects.map(effect => {
|
|
270
|
+
try {
|
|
271
|
+
return {
|
|
272
|
+
module: effect.module,
|
|
273
|
+
handler: effect.handler,
|
|
274
|
+
...nativeDescriptor(effect.states.map(([name, read]) => [name, read()]), effect.scope.map(([name, read]) => [name, read()]))
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
throw new Error(`${effect.source} ${error.message}`)
|
|
278
|
+
}
|
|
279
|
+
})
|
|
233
280
|
const title = escapeHtml(metadata.title ?? "Kudzu")
|
|
234
281
|
const head = renderMetadata(metadata)
|
|
235
282
|
const styles = metadata.styles === false
|
|
@@ -241,12 +288,18 @@ export async function renderPage(component, metadata = {}, props = {}) {
|
|
|
241
288
|
const nativeRuntime = renderContext.hasNativeBehaviors
|
|
242
289
|
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu-native.js")}"></script>`
|
|
243
290
|
: ""
|
|
291
|
+
const paramRuntime = renderContext.hasParams
|
|
292
|
+
? `<script type="module" src="${escapeAttribute(metadata.paramAsset)}"></script>`
|
|
293
|
+
: ""
|
|
244
294
|
const bindingRuntime = renderContext.hasBindings
|
|
245
295
|
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu-binding.js")}"></script>`
|
|
246
296
|
: ""
|
|
247
297
|
const listRuntime = renderContext.hasLists
|
|
248
298
|
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu-list.js")}"></script>`
|
|
249
299
|
: ""
|
|
300
|
+
const effectRuntime = renderContext.hasEffects
|
|
301
|
+
? `<script type="module" src="${escapeAttribute(metadata.effectAsset)}"></script>`
|
|
302
|
+
: ""
|
|
250
303
|
const listStates = new Set(renderContext.lists.map(list => list.state))
|
|
251
304
|
const seededListStates = new Set(renderContext.lists.filter(list => list.seed && !renderContext.textStates.has(list.state) && !renderContext.conditionStates.has(list.state)).map(list => list.state))
|
|
252
305
|
const initialState = renderContext.hasBehaviors
|
|
@@ -263,15 +316,19 @@ export async function renderPage(component, metadata = {}, props = {}) {
|
|
|
263
316
|
: ""
|
|
264
317
|
|
|
265
318
|
return {
|
|
266
|
-
html: `<!doctype html><html lang="${escapeAttribute(metadata.lang ?? "en")}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>${head}${styles}</head><body${state}${textBindings}>${body}${runtime}${bindingRuntime}${listRuntime}${nativeRuntime}</body></html>`,
|
|
319
|
+
html: `<!doctype html><html lang="${escapeAttribute(metadata.lang ?? "en")}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>${head}${styles}</head><body${state}${textBindings}>${body}${runtime}${paramRuntime}${bindingRuntime}${listRuntime}${nativeRuntime}${effectRuntime}</body></html>`,
|
|
267
320
|
hasBehaviors: renderContext.hasBehaviors,
|
|
321
|
+
hasEffects: renderContext.hasEffects,
|
|
322
|
+
hasParams: renderContext.hasParams,
|
|
268
323
|
hasBindings: renderContext.hasBindings,
|
|
269
324
|
hasLists: renderContext.hasLists,
|
|
270
325
|
hasListStyles: renderContext.hasListStyles,
|
|
271
326
|
hasStateSeed: initialState.length > 0,
|
|
272
327
|
plan: {
|
|
273
328
|
states: Object.entries(renderContext.states).map(([id, state]) => ({ id, ...state })),
|
|
329
|
+
params: renderContext.paramEntries,
|
|
274
330
|
events: renderContext.events,
|
|
331
|
+
effects: renderContext.effects,
|
|
275
332
|
bindings: renderContext.bindings,
|
|
276
333
|
conditions: renderContext.conditions,
|
|
277
334
|
lists: renderContext.lists
|
|
@@ -552,6 +609,11 @@ async function renderList(node, namespace, selectValue) {
|
|
|
552
609
|
const template = await renderNode(node.render({}), namespace, selectValue)
|
|
553
610
|
if (template.includes("data-k-native-")) descriptor.mount = true
|
|
554
611
|
if (template.includes("data-k-list-condition")) descriptor.conditions = true
|
|
612
|
+
if (template.includes("data-k-list-text-end")) descriptor.textRanges = true
|
|
613
|
+
if (template.includes("data-k-list-attrs")) descriptor.attributes = true
|
|
614
|
+
if (template.includes("data-k-list-events")) descriptor.events = true
|
|
615
|
+
if (template.includes("data-k-list-expression=")) descriptor.expressions = true
|
|
616
|
+
if (template.includes("data-k-list-expression-attrs")) descriptor.expressionAttributes = true
|
|
555
617
|
const seed = listSeed(node.items.value, renderContext.listFields)
|
|
556
618
|
if (seed) descriptor.seed = seed
|
|
557
619
|
let current = ""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { deserialize } from "./serialization.js"
|
|
2
|
+
|
|
3
|
+
export function createEffectContext(state, stateIds, commit, serializedScope = {}) {
|
|
4
|
+
const changed = new Set()
|
|
5
|
+
let scheduled = false
|
|
6
|
+
|
|
7
|
+
const flush = () => {
|
|
8
|
+
scheduled = false
|
|
9
|
+
const ids = [...changed]
|
|
10
|
+
changed.clear()
|
|
11
|
+
for (const id of ids) commit(id, state.get(id))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const setId = (id, value) => {
|
|
15
|
+
const current = state.get(id)
|
|
16
|
+
state.set(id, typeof value === "function" ? value(current) : value)
|
|
17
|
+
changed.add(id)
|
|
18
|
+
if (!scheduled) {
|
|
19
|
+
scheduled = true
|
|
20
|
+
queueMicrotask(flush)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const scope = globalThis.__KUDZU_EFFECT_CAPTURES__
|
|
25
|
+
? Object.fromEntries(Object.entries(serializedScope).map(([name, value]) => [name, deserialize(value, id => state.get(id), globalThis.__KUDZU_CAPTURE_SETTER__ ? setId : undefined)]))
|
|
26
|
+
: undefined
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
get(name) {
|
|
30
|
+
return state.get(stateIds[name])
|
|
31
|
+
},
|
|
32
|
+
scope(name) {
|
|
33
|
+
return globalThis.__KUDZU_EFFECT_CAPTURES__ ? serializedScope[name]?.type === "state" ? state.get(serializedScope[name].id) : scope[name] : undefined
|
|
34
|
+
},
|
|
35
|
+
set(name, value) {
|
|
36
|
+
setId(stateIds[name], value)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -3,11 +3,11 @@ import { browserState, mountDom, registerCommitter, registerMountHook, registerU
|
|
|
3
3
|
const listTargets = new Map()
|
|
4
4
|
const listRegistrations = new WeakMap()
|
|
5
5
|
const mountedLists = new WeakSet()
|
|
6
|
-
const imports = new Map()
|
|
7
|
-
const revisions = new WeakMap()
|
|
6
|
+
const imports = __KUDZU_LIST_ASYNC_PARTS__ ? new Map() : undefined
|
|
7
|
+
const revisions = __KUDZU_LIST_ASYNC_PARTS__ ? new WeakMap() : undefined
|
|
8
8
|
const itemParts = new WeakMap()
|
|
9
|
-
const conditionOwners = new WeakMap()
|
|
10
|
-
const itemPartsSelector = `[data-k-list-text],[data-k-list-attrs],[data-k-list-events],[data-k-list-expression],[data-k-list-expression-attrs]${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}`
|
|
9
|
+
const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
|
|
10
|
+
const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}`
|
|
11
11
|
|
|
12
12
|
function commitLists(id) {
|
|
13
13
|
const lists = listTargets.get(id)
|
|
@@ -34,13 +34,13 @@ function mountLists(root) {
|
|
|
34
34
|
const templateRoot = start.content.firstElementChild
|
|
35
35
|
const parts = listItemPartPlan(templateRoot)
|
|
36
36
|
for (const root of roots) __KUDZU_LIST_CONDITIONS__ && descriptor.conditions ? listItemParts(root) : mapListItemParts(parts, root)
|
|
37
|
-
if (descriptor.seed && !browserState.has(descriptor.state)) browserState.set(descriptor.state, roots.map((root, index) => seedListItem(root, descriptor, index)))
|
|
37
|
+
if (__KUDZU_LIST_SEEDS__ && descriptor.seed && !browserState.has(descriptor.state)) browserState.set(descriptor.state, roots.map((root, index) => seedListItem(root, descriptor, index)))
|
|
38
38
|
const items = browserState.get(descriptor.state)
|
|
39
39
|
const list = {
|
|
40
40
|
start,
|
|
41
41
|
descriptor,
|
|
42
42
|
parts,
|
|
43
|
-
seedFields: descriptor.seed && Object.keys(descriptor.seed),
|
|
43
|
+
seedFields: __KUDZU_LIST_SEEDS__ && descriptor.seed && Object.keys(descriptor.seed),
|
|
44
44
|
roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
|
|
45
45
|
values: new Map(),
|
|
46
46
|
container: roots[0]?.parentNode,
|
|
@@ -77,7 +77,7 @@ function updateList(list) {
|
|
|
77
77
|
const key = item?.[list.descriptor.key]
|
|
78
78
|
if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
|
|
79
79
|
assertListItem(item)
|
|
80
|
-
const seededValue = list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed)
|
|
80
|
+
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed) : undefined
|
|
81
81
|
if (seededValue === undefined) assertListValue(item, seen, true)
|
|
82
82
|
const token = keyToken(key)
|
|
83
83
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
@@ -108,13 +108,13 @@ function updateList(list) {
|
|
|
108
108
|
}
|
|
109
109
|
for (const [token, node] of list.roots) {
|
|
110
110
|
if (keys.has(token)) continue
|
|
111
|
-
if (list.descriptor.mount) {
|
|
111
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) {
|
|
112
112
|
unmountDom(node)
|
|
113
113
|
node.remove()
|
|
114
114
|
} else node.remove()
|
|
115
115
|
}
|
|
116
116
|
if (added) {
|
|
117
|
-
if (list.descriptor.mount) mountDom(additions)
|
|
117
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) mountDom(additions)
|
|
118
118
|
parent.insertBefore(additions, list.boundary)
|
|
119
119
|
list.container ??= parent
|
|
120
120
|
}
|
|
@@ -142,8 +142,8 @@ function updateList(list) {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
function fillListItem(root, item) {
|
|
145
|
-
const revision = (revisions.get(root) ?? 0) + 1
|
|
146
|
-
revisions.set(root, revision)
|
|
145
|
+
const revision = __KUDZU_LIST_ASYNC_PARTS__ ? (revisions.get(root) ?? 0) + 1 : 0
|
|
146
|
+
if (__KUDZU_LIST_ASYNC_PARTS__) revisions.set(root, revision)
|
|
147
147
|
const parts = listItemParts(root)
|
|
148
148
|
fillListParts(root, parts, item, revision)
|
|
149
149
|
}
|
|
@@ -159,30 +159,38 @@ function fillListParts(root, parts, item, revision) {
|
|
|
159
159
|
node.textContent = value
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
|
|
163
|
-
patchListText(marker, "template[data-k-list-text-end]", item?.[field])
|
|
162
|
+
if (__KUDZU_LIST_TEXT_RANGES__) {
|
|
163
|
+
for (const [marker, field] of parts.texts) patchListText(marker, "template[data-k-list-text-end]", item?.[field])
|
|
164
164
|
}
|
|
165
|
-
|
|
166
|
-
for (const [
|
|
167
|
-
|
|
168
|
-
for (const [node, events] of parts.events) {
|
|
169
|
-
for (const [event, native] of JSON.parse(events)) {
|
|
170
|
-
native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value]))
|
|
171
|
-
node.dataset[`kNative${capitalize(event)}`] = JSON.stringify(native)
|
|
165
|
+
if (__KUDZU_LIST_ATTRIBUTES__) {
|
|
166
|
+
for (const [node, attributes] of parts.attributes) {
|
|
167
|
+
for (const [target, field] of attributes) patchBinding(node, target, item?.[field])
|
|
172
168
|
}
|
|
173
169
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
if (__KUDZU_LIST_EVENTS__) {
|
|
171
|
+
for (const [node, events] of parts.events) {
|
|
172
|
+
for (const [event, native] of JSON.parse(events)) {
|
|
173
|
+
native.scope = Object.fromEntries(Object.entries(native.scope).map(([name, value]) => [name, value?.type === "list-item" ? serializeItem(item) : value]))
|
|
174
|
+
node.dataset[`kNative${capitalize(event)}`] = JSON.stringify(native)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
178
177
|
}
|
|
179
|
-
|
|
180
|
-
for (const [
|
|
181
|
-
evaluate(
|
|
182
|
-
if (revisions.get(root) === revision && root.isConnected)
|
|
178
|
+
if (__KUDZU_LIST_EXPRESSIONS__) {
|
|
179
|
+
for (const [marker, descriptor] of parts.expressions) {
|
|
180
|
+
evaluate(descriptor, item).then(value => {
|
|
181
|
+
if (revisions.get(root) === revision && root.isConnected) patchListText(marker, "template[data-k-list-expression-end]", value)
|
|
183
182
|
}).catch(error => console.error(error))
|
|
184
183
|
}
|
|
185
184
|
}
|
|
185
|
+
if (__KUDZU_LIST_EXPRESSION_ATTRIBUTES__) {
|
|
186
|
+
for (const [node, attributes] of parts.expressionAttributes) {
|
|
187
|
+
for (const [target, module, handler] of attributes) {
|
|
188
|
+
evaluate({ module, handler }, item).then(value => {
|
|
189
|
+
if (revisions.get(root) === revision && root.isConnected) patchBinding(node, target, value)
|
|
190
|
+
}).catch(error => console.error(error))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
186
194
|
if (__KUDZU_LIST_CONDITIONS__) {
|
|
187
195
|
for (const [marker, descriptor] of parts.conditions) {
|
|
188
196
|
evaluate(descriptor, item).then(value => {
|
|
@@ -198,10 +206,10 @@ function listItemParts(root) {
|
|
|
198
206
|
parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [], conditions: [] }
|
|
199
207
|
for (const node of matching(root, itemPartsSelector)) {
|
|
200
208
|
if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push([node, node.dataset.kListText])
|
|
201
|
-
if (node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
|
|
202
|
-
if (node.hasAttribute("data-k-list-events")) parts.events.push([node, node.dataset.kListEvents])
|
|
203
|
-
if (node.hasAttribute("data-k-list-expression")) parts.expressions.push([node, JSON.parse(node.dataset.kListExpression)])
|
|
204
|
-
if (node.hasAttribute("data-k-list-expression-attrs")) parts.expressionAttributes.push([node, JSON.parse(node.dataset.kListExpressionAttrs)])
|
|
209
|
+
if (__KUDZU_LIST_ATTRIBUTES__ && node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
|
|
210
|
+
if (__KUDZU_LIST_EVENTS__ && node.hasAttribute("data-k-list-events")) parts.events.push([node, node.dataset.kListEvents])
|
|
211
|
+
if (__KUDZU_LIST_EXPRESSIONS__ && node.hasAttribute("data-k-list-expression")) parts.expressions.push([node, JSON.parse(node.dataset.kListExpression)])
|
|
212
|
+
if (__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ && node.hasAttribute("data-k-list-expression-attrs")) parts.expressionAttributes.push([node, JSON.parse(node.dataset.kListExpressionAttrs)])
|
|
205
213
|
if (__KUDZU_LIST_CONDITIONS__ && node.hasAttribute("data-k-list-condition")) {
|
|
206
214
|
parts.conditions.push([node, JSON.parse(node.dataset.kListCondition)])
|
|
207
215
|
conditionOwners.set(node, root)
|
|
@@ -217,11 +225,11 @@ function listItemPartPlan(template) {
|
|
|
217
225
|
const parts = listItemParts(template)
|
|
218
226
|
return {
|
|
219
227
|
directTexts: parts.directTexts.map(([node, field]) => [indexes.get(node), field]),
|
|
220
|
-
texts: parts.texts.map(([node, field]) => [indexes.get(node), field]),
|
|
221
|
-
attributes: parts.attributes.map(([node, attributes]) => [indexes.get(node), attributes]),
|
|
222
|
-
events: parts.events.map(([node, events]) => [indexes.get(node), events]),
|
|
223
|
-
expressions: parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]),
|
|
224
|
-
expressionAttributes: parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes]),
|
|
228
|
+
texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([node, field]) => [indexes.get(node), field]) : [],
|
|
229
|
+
attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([node, attributes]) => [indexes.get(node), attributes]) : [],
|
|
230
|
+
events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([node, events]) => [indexes.get(node), events]) : [],
|
|
231
|
+
expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
|
|
232
|
+
expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes]) : [],
|
|
225
233
|
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : []
|
|
226
234
|
}
|
|
227
235
|
}
|
|
@@ -230,11 +238,11 @@ function mapListItemParts(parts, root) {
|
|
|
230
238
|
const target = [root, ...root.querySelectorAll("*")]
|
|
231
239
|
itemParts.set(root, {
|
|
232
240
|
directTexts: parts.directTexts.map(([index, field]) => [target[index], field]),
|
|
233
|
-
texts: parts.texts.map(([index, field]) => [target[index], field]),
|
|
234
|
-
attributes: parts.attributes.map(([index, attributes]) => [target[index], attributes]),
|
|
235
|
-
events: parts.events.map(([index, events]) => [target[index], events]),
|
|
236
|
-
expressions: parts.expressions.map(([index, descriptor]) => [target[index], descriptor]),
|
|
237
|
-
expressionAttributes: parts.expressionAttributes.map(([index, attributes]) => [target[index], attributes]),
|
|
241
|
+
texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([index, field]) => [target[index], field]) : [],
|
|
242
|
+
attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([index, attributes]) => [target[index], attributes]) : [],
|
|
243
|
+
events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([index, events]) => [target[index], events]) : [],
|
|
244
|
+
expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([index, descriptor]) => [target[index], descriptor]) : [],
|
|
245
|
+
expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([index, attributes]) => [target[index], attributes]) : [],
|
|
238
246
|
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([index, descriptor]) => {
|
|
239
247
|
conditionOwners.set(target[index], root)
|
|
240
248
|
return [target[index], descriptor]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kudzujs/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"build": "node ./bin/kudzu.mjs build",
|
|
48
48
|
"dev": "node ./bin/kudzu.mjs dev",
|
|
49
49
|
"check": "tsc --noEmit && tsc -p test/fixtures/tsconfig.json --noEmit && node ./bin/kudzu.mjs build",
|
|
50
|
-
"test": "node --test",
|
|
50
|
+
"test": "node --test test/*.test.mjs",
|
|
51
51
|
"prepublishOnly": "npm run check && npm test",
|
|
52
52
|
"deploy": "wrangler deploy",
|
|
53
53
|
"preview": "wrangler dev"
|