@kudzujs/core 0.6.21 → 0.6.23

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 CHANGED
@@ -128,18 +128,26 @@ Static trusted HTML can be rendered without a transform layer:
128
128
 
129
129
  The HTML is intentionally not sanitized. Use only trusted or previously sanitized build-time content. Reactive raw HTML, children on the same element, void elements, and keyed-list raw HTML are rejected.
130
130
 
131
- Every CSS file under `src` is copied to the same relative path under `dist/assets` and linked in deterministic order. Stylesheets produced by another build step can be declared globally so Kudzu still emits them in every document `<head>`. Configured root-relative URLs receive `base`; absolute HTTP URLs are preserved. Project-page deployments and post-build artifacts use `kudzu.config.mjs`:
131
+ Every CSS file under `src` is copied to the same relative path under `dist/assets` and linked in deterministic order. Configured root-relative URLs receive `base`; absolute HTTP URLs are preserved. A source style entry reads CSS, optionally transforms it, writes its declared output, and links it without an `afterBuild` file pipeline. `publicDir` defaults to `public` and may point elsewhere. Global or page `metadata` may be an object or a function of `{ route, params, props }`, so route props can set document language and head resources before rendering:
132
132
 
133
133
  ```js
134
134
  export default {
135
135
  base: "/newsletter",
136
- styles: ["/assets/generated.css"],
136
+ publicDir: "../public",
137
+ styles: [{
138
+ source: "../src/styles/global.css",
139
+ output: "/assets/styles.css",
140
+ transform: css => transformCss(css)
141
+ }],
142
+ metadata: ({ props }) => ({ lang: props.locale, manifest: "/manifest.json" }),
137
143
  async afterBuild({ outDir, routes, plans, rewrites, base }) {
138
- // Write generated.css, host rewrites, RSS, sitemap, or other static artifacts.
144
+ // Write host rewrites, RSS, sitemap, or other non-document artifacts.
139
145
  }
140
146
  }
141
147
  ```
142
148
 
149
+ The transform may return CSS text or an object with a `css` string, matching common CSS processor results. Page-exported `metadata` takes precedence over config metadata and may use the same function form.
150
+
143
151
  Do not render `<link rel="stylesheet">` from page or component JSX. Kudzu rejects direct static body stylesheets with a source location and catches computed JSX stylesheet output during rendering. Trusted `dangerouslySetInnerHTML` remains unparsed and is responsible for its own resource tags.
144
152
 
145
153
  ## State Semantics
@@ -393,13 +401,13 @@ One nested keyed map may read a direct array property of its parent item. This s
393
401
  ```tsx
394
402
  {categories.map(category => <section key={category.id}>
395
403
  <h2>{category.title}</h2>
396
- <ul>{category.items.map(item => <li key={item.id}>{item.title}</li>)}</ul>
404
+ <ul>{category.items.map(item => <ItemCard key={item.id} item={item} />)}</ul>
397
405
  </section>)}
398
406
  ```
399
407
 
400
- The nested collection must be `parent.<field>`, the child row must have one intrinsic root, and child handlers may capture the child item. A second child list, third nesting level, computed collection, parent-item capture from the child row, child conditions, child effects, child components, and child row-local state remain unsupported.
408
+ The nested collection must be `parent.<field>`. Its child row may be intrinsic or a same-file or relative-imported component that specializes to one intrinsic root. Child handlers receive the latest child item, and one level of child-local `&&` or ternary JSX conditions patches bounded DOM without remounting the child row. A second child list, third nesting level, computed collection, parent-item capture from the child row, conditions inside child conditions, child effects, child component tags below the specialized row root, and child row-local state remain unsupported.
401
409
 
402
- 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 beyond the direct one-level form above, component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
410
+ 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 direct outer 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, conditions nested inside item conditions, component tags below a specialized row root, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
403
411
 
404
412
  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.
405
413
 
@@ -20,6 +20,7 @@ export async function build({ quiet = false, minify = true } = {}) {
20
20
  const config = await loadConfig()
21
21
  const base = normalizeBase(config.base)
22
22
  const configuredStyles = normalizeStyles(config.styles, base)
23
+ const publicDirectory = normalizePublicDirectory(config.publicDir)
23
24
  const navigationGroups = normalizeNavigation(config.navigation)
24
25
  const navigationRoutes = navigationGroups.flatMap(group => group.routes)
25
26
  const navigationByRoute = new Map(navigationGroups.flatMap(group => group.routes.map(route => [route, group])))
@@ -39,7 +40,8 @@ export async function build({ quiet = false, minify = true } = {}) {
39
40
 
40
41
  const projectFiles = await walk(sourceDirectory)
41
42
  const sourceFiles = projectFiles.filter(file => /\.(?:ts|tsx)$/.test(file)).sort()
42
- const cssFiles = projectFiles.filter(file => file.endsWith(".css")).sort()
43
+ const configuredStyleSources = new Set(configuredStyles.sources.map(style => style.source))
44
+ const cssFiles = projectFiles.filter(file => file.endsWith(".css") && !configuredStyleSources.has(file)).sort()
43
45
  if (!sourceFiles.length) throw new Error("No TypeScript files found in src/")
44
46
  const sourceFileSet = new Set(sourceFiles)
45
47
  const sourceIndex = new Map(await Promise.all(sourceFiles.map(async file => [file, await readFile(file, "utf8")])))
@@ -74,7 +76,7 @@ export async function build({ quiet = false, minify = true } = {}) {
74
76
  const renderedHandlerUrls = new Set()
75
77
  const styleUrls = [...new Set([
76
78
  ...cssFiles.map(file => assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)),
77
- ...configuredStyles
79
+ ...configuredStyles.urls
78
80
  ])]
79
81
  const runtimePlaceholder = `/__kudzu_runtime_${randomUUID()}.js`
80
82
 
@@ -101,6 +103,9 @@ export async function build({ quiet = false, minify = true } = {}) {
101
103
  const route = runtimeSchema?.route ?? routeFromPage(pageFile, params)
102
104
  const applicationRoute = `/${route}`
103
105
  const routePath = withBase(base, `/${route}`)
106
+ const metadataContext = { route: routePath, params, props }
107
+ const configuredMetadata = await resolveDocumentMetadata(config.metadata, metadataContext, "kudzu.config metadata")
108
+ const pageMetadata = await resolveDocumentMetadata(module.metadata, metadataContext, `${relative(root, pageFile)} metadata`)
104
109
  const navigationGroup = navigationByRoute.get(applicationRoute)
105
110
  const navigable = Boolean(navigationGroup)
106
111
  const effectPath = `effects/${route ? `${route}/index` : "index"}.js`
@@ -123,7 +128,8 @@ export async function build({ quiet = false, minify = true } = {}) {
123
128
  navigationGroup.routeRecords.push(routeRecord)
124
129
  }
125
130
  const result = await renderPage(module.default, {
126
- ...(module.metadata ?? {}),
131
+ ...configuredMetadata,
132
+ ...pageMetadata,
127
133
  styles: styleUrls.length ? styleUrls : false,
128
134
  base,
129
135
  runtimeAsset: runtimePlaceholder,
@@ -175,7 +181,7 @@ export async function build({ quiet = false, minify = true } = {}) {
175
181
  const emittedHandlerModules = handlerModules.filter(module => renderedHandlerUrls.has(assetPath(base, `assets/${module.path}`)))
176
182
  const renderedEffects = new Set(plans.flatMap(plan => plan.effects.map(effect => `${effect.module}:${effect.handler}`)))
177
183
  const renderedWorkerReferences = workerReferences.filter(reference => renderedEffects.has(`${reference.module}:${reference.handler}`))
178
- if (renderedWorkerReferences.length && await exists(join(root, "public", "assets", "workers"))) throw new Error("public/assets/workers collides with Kudzu's generated Worker asset namespace")
184
+ if (renderedWorkerReferences.length && await exists(join(publicDirectory, "assets", "workers"))) throw new Error("public/assets/workers collides with Kudzu's generated Worker asset namespace")
179
185
  const workerAssets = await emitWorkers(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
180
186
  for (const module of emittedHandlerModules) {
181
187
  for (const reference of workerReferences) {
@@ -354,7 +360,18 @@ export async function build({ quiet = false, minify = true } = {}) {
354
360
  await mkdir(dirname(output), { recursive: true })
355
361
  await cp(file, output)
356
362
  }
357
- if (await exists(join(root, "public"))) await cp(join(root, "public"), outputDirectory, { recursive: true })
363
+ for (const style of configuredStyles.sources) {
364
+ let css = await readFile(style.source, "utf8")
365
+ if (style.transform) {
366
+ const result = await style.transform(css, { source: style.source, output: style.output })
367
+ css = typeof result === "string" ? result : result?.css
368
+ if (typeof css !== "string") throw new Error(`${style.label}.transform must return CSS text or an object with a css string`)
369
+ }
370
+ const output = join(outputDirectory, style.output.slice(1))
371
+ await mkdir(dirname(output), { recursive: true })
372
+ await writeFile(output, css)
373
+ }
374
+ if (await exists(publicDirectory)) await cp(publicDirectory, outputDirectory, { recursive: true })
358
375
  if (config.afterBuild !== undefined) {
359
376
  if (typeof config.afterBuild !== "function") throw new Error("kudzu.config afterBuild must be a function")
360
377
  await config.afterBuild({ root, outDir: outputDirectory, sourceDir: sourceDirectory, base, routes: plans.map(plan => plan.route), plans, rewrites: sortedRewrites })
@@ -1957,10 +1974,25 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1957
1974
  ts.forEachChild(node, rejectUnsupportedRenderControl)
1958
1975
  }
1959
1976
  rejectUnsupportedRenderControl(sourceFile)
1960
- const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
1977
+ const nestedComponentCalls = new Set()
1978
+ for (const { parts } of rawRenderedLists) {
1979
+ const collectNestedComponents = node => {
1980
+ if (ts.isJsxExpression(node) && node.expression) {
1981
+ const nested = nestedKeyedListParts(node.expression, parts.item)
1982
+ if (nested) {
1983
+ const tag = jsxTagName(nested.root)
1984
+ if (tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase()) nestedComponentCalls.add(nested.root)
1985
+ return
1986
+ }
1987
+ }
1988
+ ts.forEachChild(node, collectNestedComponents)
1989
+ }
1990
+ collectNestedComponents(parts.root)
1991
+ }
1992
+ const listComponentNames = new Set([...rawRenderedLists.flatMap(({ parts }) => {
1961
1993
  const tag = jsxTagName(parts.root)
1962
1994
  return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
1963
- }))
1995
+ }), ...[...nestedComponentCalls].map(call => jsxTagName(call).text)])
1964
1996
  const keyedComponentCalls = new Set(rawRenderedLists.map(({ parts }) => parts.root))
1965
1997
  for (const call of reducerRowStateCalls) if (!keyedComponentCalls.has(call)) fail(call, "Reducer-dispatch component useState() is only supported in a direct keyed row")
1966
1998
  for (const name of listComponentNames) {
@@ -1984,6 +2016,10 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1984
2016
  ? componentSpecializations.get(call)
1985
2017
  : specializeComponentCall(call, component.function, sourceFile, factory, context, fail)
1986
2018
  if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
2019
+ if (!local) {
2020
+ specialization.root = mergeSpecializedImports(specialization.root, component.function.getSourceFile(), call)
2021
+ synthesizeTree(specialization.root)
2022
+ }
1987
2023
  componentSpecializations.set(call, specialization)
1988
2024
  }
1989
2025
  if (local) specializedDeclarations.add(component.declaration)
@@ -2021,7 +2057,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2021
2057
  calculation.parent = callback
2022
2058
  validateListExpression(calculation, parts.item, originalParts.root, fail)
2023
2059
  }
2024
- validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization?.rowState, nestedLists)
2060
+ validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization?.rowState, nestedLists, componentSpecializations, factory)
2025
2061
  if (specialization?.effects.length) {
2026
2062
  usesListEffects = true
2027
2063
  const statements = specialization.effects.map(entry => {
@@ -2503,7 +2539,7 @@ function insideJsxEventHandler(node, root) {
2503
2539
  return false
2504
2540
  }
2505
2541
 
2506
- function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowState, nestedLists) {
2542
+ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowState, nestedLists, componentSpecializations, factory) {
2507
2543
  const fail = (node, message) => {
2508
2544
  throw sourceNodeError(node, sourceFile, message)
2509
2545
  }
@@ -2533,14 +2569,33 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2533
2569
  if (nestedList) fail(expression, "Keyed list rows support one nested keyed list")
2534
2570
  if (referenceIdentifiers(nested.callback, item).length) fail(nested.root, "Nested keyed list rows cannot capture the parent item")
2535
2571
  nestedList = nested
2536
- const nestedParts = { ...nested, state: parts.state, nested: true }
2572
+ const specialization = componentSpecializations.get(nested.root)
2573
+ const root = specialization?.root ?? nested.root
2574
+ const callback = root === nested.root ? nested.callback : factory.updateArrowFunction(
2575
+ nested.callback,
2576
+ nested.callback.modifiers,
2577
+ nested.callback.typeParameters,
2578
+ nested.callback.parameters,
2579
+ nested.callback.type,
2580
+ nested.callback.equalsGreaterThanToken,
2581
+ root
2582
+ )
2583
+ if (callback !== nested.callback) {
2584
+ ts.setParentRecursive(callback, false)
2585
+ callback.parent = nested.callback.parent
2586
+ }
2587
+ const nestedParts = { ...nested, root, callback, state: parts.state, nested: true }
2588
+ for (const calculation of specialization?.calculations ?? []) {
2589
+ ts.setParentRecursive(calculation, false)
2590
+ calculation.parent = callback
2591
+ validateListExpression(calculation, nested.item, nested.root, fail)
2592
+ }
2537
2593
  nestedLists.set(expression, nestedParts)
2538
- validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, undefined, nestedLists)
2594
+ validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, undefined, nestedLists, componentSpecializations, factory)
2539
2595
  return
2540
2596
  }
2541
2597
  const condition = conditionalParts(expression)
2542
2598
  if (condition && containsJsx(expression)) {
2543
- if (parts.nested) fail(node, "Nested keyed list item conditions are not supported")
2544
2599
  if (conditionDepth) fail(node, "Nested item conditions are not supported in keyed lists")
2545
2600
  if (rowState && referencedStateNames(condition.condition, setters).has(rowState.state)) {
2546
2601
  conditionDepth++
@@ -3897,16 +3952,49 @@ async function loadConfig() {
3897
3952
  }
3898
3953
 
3899
3954
  function normalizeStyles(value, base) {
3900
- if (value === undefined) return []
3901
- if (!Array.isArray(value)) throw new Error("kudzu.config styles must be an array of URLs")
3902
- return value.map((style, index) => {
3903
- if (typeof style !== "string" || !style) throw new Error(`kudzu.config styles[${index}] must be a non-empty URL`)
3904
- if (style.startsWith("//")) throw new Error(`kudzu.config styles[${index}] must be root-relative or an absolute HTTP URL`)
3905
- if (style.startsWith("/")) return withBase(base, style)
3906
- if (!/^https?:\/\//i.test(style)) throw new Error(`kudzu.config styles[${index}] must be root-relative or an absolute HTTP URL`)
3907
- try { new URL(style) } catch { throw new Error(`kudzu.config styles[${index}] must be root-relative or an absolute HTTP URL`) }
3908
- return style
3909
- })
3955
+ if (value === undefined) return { urls: [], sources: [] }
3956
+ if (!Array.isArray(value)) throw new Error("kudzu.config styles must be an array")
3957
+ const urls = []
3958
+ const sources = []
3959
+ for (let index = 0; index < value.length; index++) {
3960
+ const style = value[index]
3961
+ const label = `kudzu.config styles[${index}]`
3962
+ if (typeof style === "string") {
3963
+ if (!style) throw new Error(`${label} must be a non-empty URL`)
3964
+ if (style.startsWith("//")) throw new Error(`${label} must be root-relative or an absolute HTTP URL`)
3965
+ if (style.startsWith("/")) {
3966
+ urls.push(withBase(base, style))
3967
+ continue
3968
+ }
3969
+ if (!/^https?:\/\//i.test(style)) throw new Error(`${label} must be root-relative or an absolute HTTP URL`)
3970
+ try { new URL(style) } catch { throw new Error(`${label} must be root-relative or an absolute HTTP URL`) }
3971
+ urls.push(style)
3972
+ continue
3973
+ }
3974
+ if (!isPlainRecord(style) || Object.keys(style).some(key => !["source", "output", "transform"].includes(key))) throw new Error(`${label} must be a URL or a source style object`)
3975
+ if (typeof style.source !== "string" || !style.source) throw new Error(`${label}.source must be a non-empty file path`)
3976
+ if (typeof style.output !== "string" || !style.output.startsWith("/") || style.output.startsWith("//") || /[%?#\\\0]/.test(style.output) || style.output.split("/").includes("..") || !style.output.endsWith(".css")) throw new Error(`${label}.output must be a root-relative .css path without query, hash, or traversal`)
3977
+ if (style.transform !== undefined && typeof style.transform !== "function") throw new Error(`${label}.transform must be a function`)
3978
+ const entry = { label, source: resolve(root, style.source), output: style.output, transform: style.transform }
3979
+ sources.push(entry)
3980
+ urls.push(withBase(base, style.output))
3981
+ }
3982
+ return { urls, sources }
3983
+ }
3984
+
3985
+ function normalizePublicDirectory(value) {
3986
+ if (value === undefined) return join(root, "public")
3987
+ if (typeof value !== "string" || !value) throw new Error("kudzu.config publicDir must be a non-empty directory path")
3988
+ const directory = resolve(root, value)
3989
+ if (directory === outputDirectory || directory === workDirectory) throw new Error("kudzu.config publicDir cannot be dist or .kudzu")
3990
+ return directory
3991
+ }
3992
+
3993
+ async function resolveDocumentMetadata(value, context, label) {
3994
+ if (value === undefined) return {}
3995
+ const metadata = typeof value === "function" ? await value(context) : value
3996
+ if (!isPlainRecord(metadata)) throw new Error(`${label} must be a plain object or a function returning one`)
3997
+ return metadata
3910
3998
  }
3911
3999
 
3912
4000
  export function normalizeNavigation(value) {
@@ -33,36 +33,44 @@ export function listExpression(read: () => unknown, module: string, handler: str
33
33
  export function listItem(): unknown
34
34
  export function listConditional(kind: "and" | "ternary", read: () => unknown, truthy: () => unknown, falsy: () => unknown, module: string, handler: string): unknown
35
35
 
36
+ export type PageMetadata = {
37
+ title?: string
38
+ description?: string
39
+ lang?: string
40
+ locale?: string
41
+ siteName?: string
42
+ type?: string
43
+ url?: string
44
+ image?: string
45
+ imageAlt?: string
46
+ twitterCard?: string
47
+ twitterImage?: string
48
+ themeColor?: string
49
+ icon?: string
50
+ appleTouchIcon?: string
51
+ manifest?: string
52
+ styles?: boolean | string[]
53
+ base?: string
54
+ runtimeAsset?: string
55
+ effectAsset?: string
56
+ nativeAsset?: string
57
+ paramAsset?: string
58
+ runtimeParams?: string[]
59
+ navigationAsset?: string
60
+ applicationId?: string
61
+ layoutId?: string
62
+ routeId?: string
63
+ }
64
+
65
+ export type MetadataContext<Props = Record<string, unknown>> = {
66
+ route: string
67
+ params: Record<string, string>
68
+ props: Props
69
+ }
70
+
36
71
  export function renderPage<Props = Record<string, never>>(
37
72
  component: (props: Props) => unknown | Promise<unknown>,
38
- metadata?: {
39
- title?: string
40
- description?: string
41
- lang?: string
42
- locale?: string
43
- siteName?: string
44
- type?: string
45
- url?: string
46
- image?: string
47
- imageAlt?: string
48
- twitterCard?: string
49
- twitterImage?: string
50
- themeColor?: string
51
- icon?: string
52
- appleTouchIcon?: string
53
- manifest?: string
54
- styles?: boolean | string[]
55
- base?: string
56
- runtimeAsset?: string
57
- effectAsset?: string
58
- nativeAsset?: string
59
- paramAsset?: string
60
- runtimeParams?: string[]
61
- navigationAsset?: string
62
- applicationId?: string
63
- layoutId?: string
64
- routeId?: string
65
- },
73
+ metadata?: PageMetadata,
66
74
  props?: Props,
67
75
  layout?: (props: { children: unknown }) => unknown | Promise<unknown>
68
76
  ): Promise<{
@@ -568,6 +568,8 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
568
568
  }
569
569
  if (node?.[listConditionalMarker]) {
570
570
  const descriptor = { kind: node.kind, module: node.module, handler: node.handler }
571
+ const owner = renderContext.listRoot ?? renderContext.listRowRoot
572
+ if (owner) owner.conditions = true
571
573
  const previousBranch = renderContext.listConditionalBranch
572
574
  renderContext.listConditionalBranch = true
573
575
  let truthy
@@ -765,13 +767,14 @@ async function renderList(node, namespace, selectValue) {
765
767
  renderContext.listRowConditions = []
766
768
  renderContext.listRowLists = []
767
769
  renderContext.listFields = new Set([node.keyField])
768
- renderContext.listRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, rowIndexes: { s: 0, c: 0, l: 0 } }
769
- renderContext.listRowRoot = renderContext.listRoot
770
+ const templateRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, rowIndexes: { s: 0, c: 0, l: 0 } }
771
+ renderContext.listRoot = templateRoot
772
+ renderContext.listRowRoot = templateRoot
770
773
  const template = await renderNode(node.render({}), namespace, selectValue)
771
774
  if (template.includes("data-k-native-") || template.includes("data-k-effects=") || template.includes("data-k-list=")) descriptor.mount = true
772
775
  if (template.includes("data-k-list=")) descriptor.nested = true
773
776
  if (template.includes("data-k-effects=")) descriptor.effects = true
774
- if (template.includes("data-k-list-condition")) descriptor.conditions = true
777
+ if (templateRoot.conditions) descriptor.conditions = true
775
778
  if (template.includes("data-k-list-text-end")) descriptor.textRanges = true
776
779
  if (template.includes("data-k-list-attrs")) descriptor.attributes = true
777
780
  if (template.includes("data-k-list-events")) descriptor.events = true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.6.21",
3
+ "version": "0.6.23",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",