@kudzujs/core 0.6.7 → 0.6.9
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 +5 -3
- package/framework/README.md +1 -1
- package/framework/build.mjs +41 -9
- package/framework/core.d.ts +1 -0
- package/framework/core.mjs +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -330,7 +330,7 @@ const rows = items.map(item => <ItemRow
|
|
|
330
330
|
/>)
|
|
331
331
|
```
|
|
332
332
|
|
|
333
|
-
The map may also stay inside
|
|
333
|
+
The map may also stay inside a component that receives the local state array directly. The component may be declared in the page or imported by default or name from a relative TypeScript module; direct named re-exports are resolved at build time:
|
|
334
334
|
|
|
335
335
|
```tsx
|
|
336
336
|
function ItemList({ items }: { items: Item[] }) {
|
|
@@ -340,12 +340,14 @@ function ItemList({ items }: { items: Item[] }) {
|
|
|
340
340
|
return <ItemList items={items} />
|
|
341
341
|
```
|
|
342
342
|
|
|
343
|
-
The original row component remains reusable across multiple lists and ordinary JSX. State-backed list wrappers and row components are specialized to intrinsic JSX at build time; 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, in one top-level immutable `const` rendered once as a JSX child, or in one
|
|
343
|
+
The original row component remains reusable across multiple lists and ordinary JSX. State-backed list wrappers and row components are specialized to intrinsic JSX at build time; 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, in one top-level immutable `const` rendered once as a JSX child, or in one synchronous wrapper receiving the state identifier as a direct prop. 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 and effects receive the latest JSON-safe item for their key. Effects mount after a row is connected and clean up when it is removed. A direct primitive item dependency such as `[item.name]`, optionally mixed with state as `[version, item.name]`, reruns only rows whose selected value changed; the replacement setup receives the complete latest item. Unrelated fields and reorder do not rerun it, while a key change removes and mounts the row. The item remains stored once in shared list state; runtime descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
|
|
344
344
|
|
|
345
|
-
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>}`. State-backed list wrappers
|
|
345
|
+
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>}`. State-backed list wrappers use one destructured props parameter, an intrinsic return root, no effects, and a direct local-state prop. Same-file wrappers must be unexported and state-backed at every call; relative default, named/aliased, and direct named re-export wrappers are specialized per qualifying call. Row components accept destructured projected props, top-level single-`const` calculations and inline effects before one intrinsic return. Effect dependencies inside a row may be empty, direct primitive Kudzu state identifiers, or direct `item.<field>` properties whose selected values remain JSON-safe primitives. Whole-item, computed, nested, derived, `__proto__`, `prototype`, and `constructor` dependencies are rejected. 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, namespace, and star-export list wrappers, 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>`.
|
|
346
346
|
|
|
347
347
|
The focused wrapper fixture emits 1,393 B raw / 500 B gzip HTML and 10,719 B raw / 4,665 B gzip JavaScript across its route capabilities. After one warm-up, seven clean builds measured 314.1, 325.3, 322.3, 327.2, 336.1, 322.4, and 315.0 ms, with a 322.4 ms median.
|
|
348
348
|
|
|
349
|
+
The three-wrapper relative-import fixture emits 2,279 B raw / 629 B gzip HTML and 11,370 B raw / 4,828 B gzip JavaScript, including one imported-wrapper item expression; its unused component handler module is not emitted. After one warm-up, seven clean builds measured 340.2, 349.4, 352.8, 335.0, 354.1, 364.3, and 349.5 ms, with a 349.5 ms median.
|
|
350
|
+
|
|
349
351
|
## Effects
|
|
350
352
|
|
|
351
353
|
Browser-only initial work uses the familiar empty-dependency effect shape:
|
package/framework/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Exact relative `.worker.ts` constructors in inline effects are validated and bun
|
|
|
21
21
|
|
|
22
22
|
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime. Source CSS and global `kudzu.config` styles are emitted in document heads before `afterBuild()` runs; static stylesheet links in component JSX fail compilation instead of loading from the body.
|
|
23
23
|
|
|
24
|
-
Same-file components receiving a direct local-state array prop are specialized to intrinsic JSX before keyed-list analysis, so their component function is not retained in the browser. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal, unrelated fields do nothing, and key changes remain remove plus mount. Builds without item dependencies emit no item reader or list-state subscription code.
|
|
24
|
+
Same-file and relative-imported components receiving a direct local-state array prop are specialized to intrinsic JSX before keyed-list analysis, so their component function is not retained in the browser. Handler modules are emitted only when a rendered descriptor references them, preventing specialized imported components from adding dead browser assets. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal, unrelated fields do nothing, and key changes remain remove plus mount. Builds without item dependencies emit no item reader or list-state subscription code.
|
|
25
25
|
|
|
26
26
|
`kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
|
|
27
27
|
|
package/framework/build.mjs
CHANGED
|
@@ -70,6 +70,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
70
70
|
const emittedRoutes = new Set()
|
|
71
71
|
const emittedApplicationRoutes = new Set()
|
|
72
72
|
const emittedNavigationRecords = []
|
|
73
|
+
const renderedHandlerUrls = new Set()
|
|
73
74
|
const styleUrls = [...new Set([
|
|
74
75
|
...cssFiles.map(file => assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)),
|
|
75
76
|
...configuredStyles
|
|
@@ -129,6 +130,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
129
130
|
runtimeParams: runtimeSchema?.params,
|
|
130
131
|
...(navigable ? { navigationAsset: navigationGroup.assetPath, applicationId: navigationGroup.applicationId, layoutId: navigationGroup.layoutId, routeId: applicationRoute } : {})
|
|
131
132
|
}, props, module.layout)
|
|
133
|
+
for (const url of result.handlerModules) renderedHandlerUrls.add(url)
|
|
132
134
|
if (navigationGroup) {
|
|
133
135
|
navigationGroup.hasEffects ||= result.hasEffects
|
|
134
136
|
navigationGroup.hasParams ||= result.hasParams
|
|
@@ -163,11 +165,12 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
163
165
|
|
|
164
166
|
const assetsDirectory = join(outputDirectory, "assets")
|
|
165
167
|
await mkdir(assetsDirectory, { recursive: true })
|
|
168
|
+
const emittedHandlerModules = handlerModules.filter(module => renderedHandlerUrls.has(assetPath(base, `assets/${module.path}`)))
|
|
166
169
|
const renderedEffects = new Set(plans.flatMap(plan => plan.effects.map(effect => `${effect.module}:${effect.handler}`)))
|
|
167
170
|
const renderedWorkerReferences = workerReferences.filter(reference => renderedEffects.has(`${reference.module}:${reference.handler}`))
|
|
168
171
|
if (renderedWorkerReferences.length && await exists(join(root, "public", "assets", "workers"))) throw new Error("public/assets/workers collides with Kudzu's generated Worker asset namespace")
|
|
169
172
|
const workerAssets = await emitWorkers(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
|
|
170
|
-
for (const module of
|
|
173
|
+
for (const module of emittedHandlerModules) {
|
|
171
174
|
for (const reference of workerReferences) {
|
|
172
175
|
if (reference.module !== assetPath(base, `assets/${module.path}`)) continue
|
|
173
176
|
const url = workerAssets.get(reference.placeholder) ?? "about:blank"
|
|
@@ -192,7 +195,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
192
195
|
const hasNestedStateCaptures = hasNestedCaptureState(plans)
|
|
193
196
|
const hasSetterCaptures = hasCaptureType(plans, "setter")
|
|
194
197
|
const hasEffectCaptures = plans.some(plan => plan.effects.some(effect => Object.keys(effect.scope).length))
|
|
195
|
-
const nativeModules =
|
|
198
|
+
const nativeModules = emittedHandlerModules.filter(module => module.hasNativeHandlers).map(module => assetPath(base, `assets/${module.path}`))
|
|
196
199
|
const hasNativeHandlers = nativeModules.length > 0
|
|
197
200
|
const hasEffects = effectEntries.length > 0
|
|
198
201
|
const hasNavigableEffects = effectEntries.some(entry => entry.navigable)
|
|
@@ -290,7 +293,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
290
293
|
await writeJavaScript(join(assetsDirectory, group.assetName), specializeNavigationEffects(navigationRuntime, group.hasEffects || group.hasParams), minify)
|
|
291
294
|
}
|
|
292
295
|
}
|
|
293
|
-
for (const handlerModule of
|
|
296
|
+
for (const handlerModule of emittedHandlerModules) {
|
|
294
297
|
const output = join(assetsDirectory, handlerModule.path)
|
|
295
298
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
296
299
|
await writeJavaScript(output, handlerModule.code, minify)
|
|
@@ -305,11 +308,11 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
305
308
|
await mkdir(dirname(output), { recursive: true })
|
|
306
309
|
await writeJavaScript(output, entry.navigable
|
|
307
310
|
? entry.effects.some(effect => effect.owner)
|
|
308
|
-
? printOwnedNavigableEffectEntry(entry.effects, output,
|
|
309
|
-
: printNavigableEffectEntry(entry.effects, output,
|
|
310
|
-
: printEffectEntry(entry.effects, output,
|
|
311
|
+
? printOwnedNavigableEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base)
|
|
312
|
+
: printNavigableEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base)
|
|
313
|
+
: printEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base, entry.paramPath, runtimeName(entry.usesDependencyRuntime)), minify)
|
|
311
314
|
}
|
|
312
|
-
const clientModules = await collectClientModules(
|
|
315
|
+
const clientModules = await collectClientModules(emittedHandlerModules.flatMap(module => module.clientImports), sourceFileSet)
|
|
313
316
|
for (const file of clientModules) {
|
|
314
317
|
const output = join(assetsDirectory, clientModulePath(file))
|
|
315
318
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
@@ -317,7 +320,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
317
320
|
}
|
|
318
321
|
if (clientModules.length) {
|
|
319
322
|
await bundle({
|
|
320
|
-
entryPoints:
|
|
323
|
+
entryPoints: emittedHandlerModules.map(module => join(assetsDirectory, module.path)),
|
|
321
324
|
outbase: join(assetsDirectory, "handlers"),
|
|
322
325
|
outdir: join(assetsDirectory, "handlers"),
|
|
323
326
|
entryNames: "[dir]/[name]",
|
|
@@ -1724,6 +1727,26 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1724
1727
|
specializedDeclarations.add(component.declaration)
|
|
1725
1728
|
stateBackedComponentFunctions.add(component.function)
|
|
1726
1729
|
}
|
|
1730
|
+
for (const [name, binding] of importBindings) {
|
|
1731
|
+
if (binding.kind === "namespace") continue
|
|
1732
|
+
const calls = jsxTagUses(sourceFile, name)
|
|
1733
|
+
if (!calls.some(call => jsxCallHasDirectStateProp(call, settersByFunction.get(nearestFunction(call)) ?? new Map()))) continue
|
|
1734
|
+
const imported = binding.kind === "default" ? "default" : binding.imported
|
|
1735
|
+
let component
|
|
1736
|
+
try {
|
|
1737
|
+
component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
if (error.message.includes("does not export a statically analyzable keyed list component")) continue
|
|
1740
|
+
throw error
|
|
1741
|
+
}
|
|
1742
|
+
const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
|
|
1743
|
+
for (const call of stateBackedCalls) {
|
|
1744
|
+
const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail)
|
|
1745
|
+
if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
|
|
1746
|
+
componentSpecializations.set(call, specialization)
|
|
1747
|
+
stateBackedComponentRoots.push(specialization.root)
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1727
1750
|
const rawRenderedLists = []
|
|
1728
1751
|
const collectRenderedLists = node => {
|
|
1729
1752
|
const specialization = componentSpecializations.get(node)
|
|
@@ -2164,7 +2187,7 @@ function keyedListParts(expression, setters) {
|
|
|
2164
2187
|
const root = unwrapExpression(callback.body)
|
|
2165
2188
|
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) throw new Error("Keyed list map callback must return one JSX element")
|
|
2166
2189
|
const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
|
|
2167
|
-
const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && attribute.name
|
|
2190
|
+
const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
|
|
2168
2191
|
const field = key && ts.isJsxAttribute(key) && key.initializer && ts.isJsxExpression(key.initializer) && key.initializer.expression && directProperty(key.initializer.expression, callback.parameters[0].name.text)
|
|
2169
2192
|
if (!field) throw new Error(`Keyed list root must have key={${callback.parameters[0].name.text}.<field>}`)
|
|
2170
2193
|
return { state, callback, root, item: callback.parameters[0].name.text, keyField: field }
|
|
@@ -2200,6 +2223,15 @@ function isStateBackedListComponentCall(call, component, setters) {
|
|
|
2200
2223
|
return found
|
|
2201
2224
|
}
|
|
2202
2225
|
|
|
2226
|
+
function jsxCallHasDirectStateProp(call, setters) {
|
|
2227
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
2228
|
+
const stateNames = new Set(setters.values())
|
|
2229
|
+
return attributes.properties.some(attribute => {
|
|
2230
|
+
const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
|
|
2231
|
+
return value && ts.isIdentifier(value) && stateNames.has(value.text)
|
|
2232
|
+
})
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2203
2235
|
function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions) {
|
|
2204
2236
|
const fail = (node, message) => {
|
|
2205
2237
|
throw sourceNodeError(node, sourceFile, message)
|
package/framework/core.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
70
70
|
hasLists: boolean
|
|
71
71
|
hasListStyles: boolean
|
|
72
72
|
hasStateSeed: boolean
|
|
73
|
+
handlerModules: string[]
|
|
73
74
|
plan: {
|
|
74
75
|
states: Array<{ id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route" }>
|
|
75
76
|
params: Array<{ name: string; id: string }>
|
package/framework/core.mjs
CHANGED
|
@@ -99,6 +99,7 @@ export function useEffect(callback, dependencies, module, handler, states, scope
|
|
|
99
99
|
owners.push(owner)
|
|
100
100
|
}
|
|
101
101
|
if (!renderContext.listDepth || list) renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(itemDependencies.length ? { itemDependencies, listState: renderContext.listRoot.state } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
102
|
+
renderContext.handlerModules.add(module)
|
|
102
103
|
renderContext.hasBehaviors = true
|
|
103
104
|
renderContext.hasEffects = true
|
|
104
105
|
}
|
|
@@ -141,6 +142,7 @@ export function behavior(commands) {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
export function nativeBehavior(module, handler, states, scope) {
|
|
145
|
+
renderContext?.handlerModules.add(module)
|
|
144
146
|
return {
|
|
145
147
|
[nativeBehaviorMarker]: true,
|
|
146
148
|
module,
|
|
@@ -187,6 +189,7 @@ export function listField(read, field) {
|
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
export function listExpression(read, module, handler) {
|
|
192
|
+
renderContext?.handlerModules.add(module)
|
|
190
193
|
const value = renderContext?.listTemplate ? undefined : read()
|
|
191
194
|
if (value && typeof value.then === "function") throw new Error("Derived keyed list item expressions must return synchronous values")
|
|
192
195
|
return { [listExpressionMarker]: true, module, handler, value }
|
|
@@ -197,6 +200,7 @@ export function listItem() {
|
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
export function listConditional(kind, read, truthy, falsy, module, handler) {
|
|
203
|
+
renderContext?.handlerModules.add(module)
|
|
200
204
|
return { [listConditionalMarker]: true, kind, value: renderContext?.listTemplate ? undefined : read(), truthy, falsy, module, handler }
|
|
201
205
|
}
|
|
202
206
|
|
|
@@ -230,6 +234,7 @@ function assertListValue(value, seen) {
|
|
|
230
234
|
}
|
|
231
235
|
|
|
232
236
|
function reactiveDescriptor(module, handler, states, scope) {
|
|
237
|
+
renderContext?.handlerModules.add(module)
|
|
233
238
|
const scopeStates = {}
|
|
234
239
|
const serializedScope = {}
|
|
235
240
|
const scopeBindings = {}
|
|
@@ -295,7 +300,7 @@ function serializeCapture(name, value, seen) {
|
|
|
295
300
|
}
|
|
296
301
|
|
|
297
302
|
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
298
|
-
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], effectOwners: [], 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 }
|
|
303
|
+
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], effectOwners: [], contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerModules: new Set(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
299
304
|
|
|
300
305
|
try {
|
|
301
306
|
const page = { [routeScopeMarker]: true, component, props }
|
|
@@ -370,6 +375,7 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
370
375
|
hasLists: renderContext.hasLists,
|
|
371
376
|
hasListStyles: renderContext.hasListStyles,
|
|
372
377
|
hasStateSeed: initialState.length > 0,
|
|
378
|
+
handlerModules: [...renderContext.handlerModules],
|
|
373
379
|
plan: {
|
|
374
380
|
states: Object.entries(renderContext.states).map(([id, state]) => ({ id, ...state })),
|
|
375
381
|
params: renderContext.paramEntries,
|