@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 +14 -6
- package/framework/build.mjs +110 -22
- package/framework/core.d.ts +36 -28
- package/framework/core.mjs +6 -3
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
|
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 => <
|
|
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
|
|
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
|
|
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
|
|
package/framework/build.mjs
CHANGED
|
@@ -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
|
|
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
|
-
...
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
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) {
|
package/framework/core.d.ts
CHANGED
|
@@ -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<{
|
package/framework/core.mjs
CHANGED
|
@@ -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
|
-
|
|
769
|
-
renderContext.
|
|
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 (
|
|
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
|