@uniweb/build 0.78.0 → 0.80.0
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/package.json +7 -7
- package/src/dev/api-mount.js +11 -9
- package/src/foundation/default-layout.js +52 -0
- package/src/foundation/derive-supports.js +3 -2
- package/src/generate-entry.js +11 -0
- package/src/resolve-data-schema.js +2 -1
- package/src/runtime-schema.js +30 -12
- package/src/schema.js +101 -4
- package/src/site/content-collector.js +14 -9
- package/src/site/deploy-config-writer.js +4 -2
- package/src/site/plugin.js +5 -5
- package/src/site/queries-config.js +55 -2
- package/src/site/query-processor.js +42 -34
- package/src/uwx/data-key-types.js +4 -2
- package/src/uwx/index.js +7 -1
- package/src/uwx/project-writer.js +31 -21
- package/src/uwx/services-request.js +148 -27
- package/src/uwx/site-diff.js +6 -2
- package/src/uwx/site-project.js +2 -3
- package/src/uwx/site.js +10 -8
- package/src/uwx/yaml-edit.js +288 -0
- package/src/validate-data.js +18 -11
- package/src/uwx/yaml-upsert.js +0 -79
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,15 +57,15 @@
|
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
|
-
"@uniweb/projections": "^0.7.14",
|
|
61
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
62
60
|
"@uniweb/semantic-parser": "^1.5.1",
|
|
61
|
+
"@uniweb/projections": "^0.7.15",
|
|
62
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
63
|
+
"@uniweb/schemas": "^0.13.0",
|
|
63
64
|
"@uniweb/content-reader": "^1.2.8",
|
|
64
|
-
"@uniweb/schemas": "^0.12.0",
|
|
65
65
|
"@uniweb/theming": "^0.2.0"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.29.
|
|
68
|
+
"@uniweb/runtime": "^0.29.4"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"@tailwindcss/vite": "^4.0.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
76
76
|
"vite-plugin-svgr": "^4.0.0",
|
|
77
|
-
"@uniweb/
|
|
78
|
-
"@uniweb/
|
|
77
|
+
"@uniweb/runtime": "^0.29.4",
|
|
78
|
+
"@uniweb/core": "^0.38.0"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
package/src/dev/api-mount.js
CHANGED
|
@@ -5,7 +5,7 @@ import yaml from 'js-yaml'
|
|
|
5
5
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Where `uniweb dev` answers the site's `
|
|
8
|
+
* Where `uniweb dev` answers the site's `backend` service when `$devBackend` names a handler.
|
|
9
9
|
* The dev server is the host here, so the address is its own.
|
|
10
10
|
*/
|
|
11
11
|
export const DEV_API_ADDRESS = '/_api'
|
|
@@ -16,22 +16,24 @@ export const DEV_API_ADDRESS = '/_api'
|
|
|
16
16
|
* A site that talks to a backend needs one running to be developed against, and
|
|
17
17
|
* making that a live deployment is slow, costs money, and puts a shared database
|
|
18
18
|
* behind a developer's experiments. So a site may name a **local handler**, and in
|
|
19
|
-
* `uniweb dev` the dev server answers the site's `
|
|
19
|
+
* `uniweb dev` the dev server answers the site's `backend` service with it:
|
|
20
20
|
*
|
|
21
21
|
* ```yaml
|
|
22
22
|
* # site.yml
|
|
23
23
|
* services:
|
|
24
|
-
*
|
|
25
|
-
* $
|
|
24
|
+
* backend: true # ask your host for it (in production)
|
|
25
|
+
* $devBackend: ./mock/api.js # what answers it in `uniweb dev`, at /_api
|
|
26
26
|
* ```
|
|
27
27
|
*
|
|
28
28
|
* ⭐ THE DEV SERVER SUPPLIES THE ADDRESS [Diego, 2026-10-06]. In `uniweb dev` it is
|
|
29
29
|
* the host, so where it answers is its own to choose — `DEV_API_ADDRESS` — and the
|
|
30
|
-
* plugin puts that address in the dev payload's `config.
|
|
30
|
+
* plugin puts that address in the dev payload's `config.backend`, the site tier
|
|
31
31
|
* `resolveService` reads when no host speaks. ⛔ *Until then the site wrote it, as a
|
|
32
32
|
* top-level `api: /_api` "the same in development and in production" — an address
|
|
33
33
|
* that, under `services:`, would read as "the site brings its own backend" and ask the
|
|
34
|
-
* host to turn its own off.*
|
|
34
|
+
* host to turn its own off.* ⛔ *And until 2026-10-07 the service was `api` and this
|
|
35
|
+
* key `$devApi`; both are refused by those names now — `services.api` by
|
|
36
|
+
* `refuseUnreadableServices`, `$devApi` by `refuseRetiredServiceKeys`.*
|
|
35
37
|
*
|
|
36
38
|
* ```js
|
|
37
39
|
* // mock/api.js — default-export a fetch handler
|
|
@@ -50,9 +52,9 @@ export const DEV_API_ADDRESS = '/_api'
|
|
|
50
52
|
*
|
|
51
53
|
* ## ⛔ Development only, and it cannot leak
|
|
52
54
|
*
|
|
53
|
-
* `$
|
|
55
|
+
* `$devBackend` is read by the dev plugin and by nothing else: no build reads it, no
|
|
54
56
|
* `info` key carries it, and only the dev server's payload names its address. What
|
|
55
|
-
* answers the site's `
|
|
57
|
+
* answers the site's `backend` service locally is a fact about one machine.
|
|
56
58
|
*
|
|
57
59
|
* ⚠️ **Same-origin on purpose.** Mounting inside the dev server means cookies and
|
|
58
60
|
* `credentials: 'same-origin'` behave as they do in production, where a site's app
|
|
@@ -87,7 +89,7 @@ export function mountDevApi(server, { root }) {
|
|
|
87
89
|
return null
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
const spec = site.$
|
|
92
|
+
const spec = site.$devBackend
|
|
91
93
|
if (!spec) return null
|
|
92
94
|
const mount = DEV_API_ADDRESS
|
|
93
95
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The layout a page renders with when it names none — decided once, by the build, and written
|
|
3
|
+
* into both artifacts: the entry's `capabilities.defaultLayout`, which the runtime reads
|
|
4
|
+
* (`Website.getDefaultLayoutName`), and `_self.defaultLayout` in the schema, which an editor
|
|
5
|
+
* reads to offer a page the areas it will draw. One rule, run once, so the two cannot disagree.
|
|
6
|
+
*
|
|
7
|
+
* ⭐ THE RULE [2026-10-07; Diego: "I think the default layout maybe called `default`"]:
|
|
8
|
+
*
|
|
9
|
+
* 1. `main.js`'s `defaultLayout`, written as that layout's own name — `DocsLayout` for
|
|
10
|
+
* `docs` — so a reader finds it in `_layouts` by key. ⛔ One that names none of the
|
|
11
|
+
* foundation's layouts stops the build: at render it fell to the built-in layout with
|
|
12
|
+
* nothing saying why.
|
|
13
|
+
* 2. Else the layout whose name is `default`, by the one naming rule (`layoutNameKey`):
|
|
14
|
+
* `Default`, `DefaultLayout`, `default-layout`.
|
|
15
|
+
* 3. Else none — the runtime's built-in layout, which draws `header`, the body and `footer`.
|
|
16
|
+
*
|
|
17
|
+
* ⛔ NOT "the only layout". Under that rule, adding a second layout would change the layout of
|
|
18
|
+
* every page that names none — a non-local effect nobody asked for. A foundation that means its
|
|
19
|
+
* one layout as the default says so, as every template does.
|
|
20
|
+
*
|
|
21
|
+
* ⛔ *Until 2026-10-07 the build copied the declared value and derived nothing, so a layout named
|
|
22
|
+
* `Default` was the default only when `main.js` said so — and a reader of the schema finding none
|
|
23
|
+
* had to guess what the runtime would draw.*
|
|
24
|
+
*
|
|
25
|
+
* @module @uniweb/build/foundation/default-layout
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { findLayoutEntry } from '@uniweb/core/layout-name'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {*} declared - `main.js`'s `defaultLayout`, as `loadFoundationConfig` reads it
|
|
32
|
+
* @param {string[]} layoutNames - the foundation's layouts (`discoverLayoutsInPath`)
|
|
33
|
+
* @returns {string|null} the layout's own name, or null for the built-in layout
|
|
34
|
+
* @throws {Error} when `declared` names none of the layouts
|
|
35
|
+
*/
|
|
36
|
+
export function resolveDefaultLayout(declared, layoutNames = []) {
|
|
37
|
+
const byName = Object.fromEntries((layoutNames || []).map((name) => [name, name]))
|
|
38
|
+
|
|
39
|
+
if (declared !== undefined && declared !== null && declared !== '') {
|
|
40
|
+
const name = typeof declared === 'string' ? findLayoutEntry(byName, declared) : undefined
|
|
41
|
+
if (name) return name
|
|
42
|
+
const known = Object.keys(byName)
|
|
43
|
+
throw new Error(
|
|
44
|
+
`main.js: \`defaultLayout\` names ${JSON.stringify(declared)}, which is not one of this foundation's layouts` +
|
|
45
|
+
(known.length > 0 ? ` (${known.join(', ')})` : ' — it has none in src/layouts/') +
|
|
46
|
+
'. Name one of them, or remove the key: a page that names no layout then renders the built-in ' +
|
|
47
|
+
'layout, or one named `Default`.'
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return findLayoutEntry(byName, 'default') ?? null
|
|
52
|
+
}
|
|
@@ -75,9 +75,10 @@
|
|
|
75
75
|
*
|
|
76
76
|
* ⚠️ `@uniweb/api` deliberately needs no entry: it calls
|
|
77
77
|
* `resolveService(website, SERVICE_NAME)` against a module-level `const`, which
|
|
78
|
-
* `readModuleConsts` resolves. Without that resolution `
|
|
78
|
+
* `readModuleConsts` resolves. Without that resolution `backend` would be both
|
|
79
79
|
* missed *and* counted as blindness, marking every foundation that uses it
|
|
80
|
-
* unknowable.
|
|
80
|
+
* unknowable. ⛔ *The constant was `api` until 2026-10-07, so a foundation built
|
|
81
|
+
* against an earlier `@uniweb/api` derives `api` — a service no host offers now.*
|
|
81
82
|
*
|
|
82
83
|
* ## Why the AST rather than a regex
|
|
83
84
|
*
|
package/src/generate-entry.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
LAYOUTS_PATH
|
|
33
33
|
} from './schema.js'
|
|
34
34
|
import { extractAllRuntimeSchemas, extractAllLayoutRuntimeSchemas, extractFoundationRuntime } from './runtime-schema.js'
|
|
35
|
+
import { resolveDefaultLayout } from './foundation/default-layout.js'
|
|
35
36
|
import { SCHEMA_EXTENSIONS } from './resolve-data-schema.js'
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -124,6 +125,7 @@ function generateEntrySource(components, options = {}) {
|
|
|
124
125
|
cssPath = null,
|
|
125
126
|
foundationExports = null,
|
|
126
127
|
foundationRuntime = { code: [], data: {} },
|
|
128
|
+
defaultLayout = null,
|
|
127
129
|
meta = {},
|
|
128
130
|
layouts = {},
|
|
129
131
|
layoutMeta = {},
|
|
@@ -188,6 +190,9 @@ function generateEntrySource(components, options = {}) {
|
|
|
188
190
|
for (const key of foundationRuntime.code) capParts.push(`${key}: _foundationModule.default?.${key}`)
|
|
189
191
|
for (const [key, value] of Object.entries(foundationRuntime.data)) capParts.push(`${key}: ${JSON.stringify(value)}`)
|
|
190
192
|
}
|
|
193
|
+
// The resolved default, with or without a main.js — a layout named `Default` is one
|
|
194
|
+
// (`resolveDefaultLayout`), and `_self.defaultLayout` in the schema carries the same name.
|
|
195
|
+
if (defaultLayout) capParts.push(`defaultLayout: ${JSON.stringify(defaultLayout)}`)
|
|
191
196
|
if (layoutNames.length > 0) {
|
|
192
197
|
capParts.push(`layouts: { ${layoutNames.join(', ')} }`)
|
|
193
198
|
}
|
|
@@ -310,6 +315,11 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
310
315
|
const foundationExports = detectFoundationExports(srcDir)
|
|
311
316
|
const foundationConfig = await loadFoundationConfig(srcDir)
|
|
312
317
|
const foundationRuntime = foundationExports ? extractFoundationRuntime(foundationConfig) : { code: [], data: {} }
|
|
318
|
+
// Which layout a page that names none renders with — the build's answer, written here and in
|
|
319
|
+
// the schema's `_self.defaultLayout` (`buildSchema`) from one rule, so the runtime and an editor
|
|
320
|
+
// agree. It replaces the declared value `extractFoundationRuntime` copied.
|
|
321
|
+
const defaultLayout = resolveDefaultLayout(foundationConfig.defaultLayout, layoutNames)
|
|
322
|
+
delete foundationRuntime.data.defaultLayout
|
|
313
323
|
|
|
314
324
|
// Per-component runtime metadata. ⭐ No data schema is resolved for it: the runtime reads a
|
|
315
325
|
// declared key's ref and nothing of its shape (2026-10-05), so the entry no longer depends on
|
|
@@ -329,6 +339,7 @@ export async function generateEntryPoint(srcDir, outputPath = null, options = {}
|
|
|
329
339
|
cssPath,
|
|
330
340
|
foundationExports,
|
|
331
341
|
foundationRuntime,
|
|
342
|
+
defaultLayout,
|
|
332
343
|
meta,
|
|
333
344
|
layouts,
|
|
334
345
|
layoutMeta,
|
|
@@ -161,7 +161,8 @@ export function collectSchemaRefs(components, foundationData = null) {
|
|
|
161
161
|
const readData = (data) => {
|
|
162
162
|
if (!data || typeof data !== 'object' || data === false) return
|
|
163
163
|
for (const binding of Object.values(data)) {
|
|
164
|
-
// `
|
|
164
|
+
// A ref, short or long (`{ schema, single, whole }`) — the flags change nothing about which
|
|
165
|
+
// schema the key is typed by.
|
|
165
166
|
const { ref } = dataRefOf(binding)
|
|
166
167
|
if (ref) refs.add(ref)
|
|
167
168
|
}
|
package/src/runtime-schema.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* reads to render, and nothing else:
|
|
6
6
|
*
|
|
7
7
|
* - background: 'self' when component handles its own background
|
|
8
|
-
* - data: { <key>: <schema ref> | null } — every `content.data`
|
|
9
|
-
* declares, in order, with its schema ref (null for
|
|
8
|
+
* - data: { <key>: <schema ref> | { schema, single?, whole? } | null } — every `content.data`
|
|
9
|
+
* key the component declares, in order, with its schema ref and the flags set on it (null for
|
|
10
|
+
* an inline shape)
|
|
10
11
|
* - vars: { <name>: { default? } } — the CSS variables the component declares
|
|
11
12
|
* - defaults: param default values
|
|
12
13
|
* - context: static capabilities for cross-block coordination
|
|
@@ -34,17 +35,34 @@
|
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
import { lowerData } from '@uniweb/schemas/content'
|
|
38
|
+
import { schemaDeclarationOf } from '@uniweb/schemas/foundation'
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
|
-
* A `data:` entry
|
|
40
|
-
* shape, which names no schema and is filled only under its own key.
|
|
41
|
+
* A `data:` entry as the runtime reads it: its schema ref, with the flags set on it — or null for an
|
|
42
|
+
* inline shape, which names no schema and is filled only under its own key.
|
|
41
43
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
+
* ⭐ Two flags, independent (ruled 2026-10-07 [Diego]): `single` — one record, not a list — and
|
|
45
|
+
* `whole` — each record as stored, not its brief.
|
|
46
|
+
* Lean on purpose: the bare ref when neither is set, which is most keys, and only a flag that is set.
|
|
47
|
+
* ⛔ Until 2026-10-07 this wrote the ref alone, so `{ schema, whole: true }` lost its flag here and
|
|
48
|
+
* only the `'…/*'` spelling, now retired, reached the runtime.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} key - the key, for a refusal
|
|
51
|
+
* @param {*} value - its value, as authored
|
|
52
|
+
* @returns {string|{ schema: string, single?: true, whole?: true }|null}
|
|
53
|
+
* @throws {Error} for a retired or malformed declaration (`schemaDeclarationOf`)
|
|
44
54
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
function leanDataValue(key, value) {
|
|
56
|
+
let declaration
|
|
57
|
+
try {
|
|
58
|
+
declaration = schemaDeclarationOf(value)
|
|
59
|
+
} catch (err) {
|
|
60
|
+
throw new Error(`[uniweb] Invalid 'data.${key}': ${err.message}`)
|
|
61
|
+
}
|
|
62
|
+
if (!declaration || !declaration.schema) return null
|
|
63
|
+
const { schema, single, whole } = declaration
|
|
64
|
+
if (!single && !whole) return schema
|
|
65
|
+
return { schema, ...(single ? { single: true } : {}), ...(whole ? { whole: true } : {}) }
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
/**
|
|
@@ -148,7 +166,7 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
148
166
|
)
|
|
149
167
|
}
|
|
150
168
|
runtime.data = runtime.data || {}
|
|
151
|
-
runtime.data[key] =
|
|
169
|
+
runtime.data[key] = leanDataValue(key, value)
|
|
152
170
|
}
|
|
153
171
|
} else if (data !== undefined) {
|
|
154
172
|
throw new Error(
|
|
@@ -337,12 +355,12 @@ function leanVars(vars) {
|
|
|
337
355
|
return Object.keys(out).length > 0 ? out : null
|
|
338
356
|
}
|
|
339
357
|
|
|
340
|
-
/** A foundation's `data:` as a section type's lean `data` is: each key → its
|
|
358
|
+
/** A foundation's `data:` as a section type's lean `data` is: each key → its ref and flags, or null. */
|
|
341
359
|
function leanDataKeys(data) {
|
|
342
360
|
const lowered = lowerData(data)
|
|
343
361
|
if (!lowered || typeof lowered !== 'object' || Array.isArray(lowered)) return null
|
|
344
362
|
const out = {}
|
|
345
|
-
for (const [key, value] of Object.entries(lowered)) out[key] =
|
|
363
|
+
for (const [key, value] of Object.entries(lowered)) out[key] = leanDataValue(key, value)
|
|
346
364
|
return Object.keys(out).length > 0 ? out : null
|
|
347
365
|
}
|
|
348
366
|
|
package/src/schema.js
CHANGED
|
@@ -18,7 +18,8 @@ import { join, dirname, extname, basename } from 'node:path'
|
|
|
18
18
|
import { pathToFileURL } from 'node:url'
|
|
19
19
|
import { describeChildren, describeVisuals, lowerChildren } from '@uniweb/schemas/component'
|
|
20
20
|
import { describeContent, lowerData } from '@uniweb/schemas/content'
|
|
21
|
-
import { normalizeData } from '@uniweb/schemas/foundation'
|
|
21
|
+
import { normalizeData, schemaDeclarationOf } from '@uniweb/schemas/foundation'
|
|
22
|
+
import { rootListSection } from '@uniweb/schemas/conform'
|
|
22
23
|
import { SECTION_PARAMS, SECTION_KEYS } from '@uniweb/schemas/section'
|
|
23
24
|
import { collectSchemaRefs, buildDataSchemaMap, ownSchemaRefs } from './resolve-data-schema.js'
|
|
24
25
|
import {
|
|
@@ -26,6 +27,8 @@ import {
|
|
|
26
27
|
deriveRecordsSupport,
|
|
27
28
|
unnameableIn,
|
|
28
29
|
} from './foundation/derive-supports.js'
|
|
30
|
+
import { RENAMED_SERVICES } from './uwx/services-request.js'
|
|
31
|
+
import { resolveDefaultLayout } from './foundation/default-layout.js'
|
|
29
32
|
import { foundationNameOf, splitFoundationName } from './foundation-name.js'
|
|
30
33
|
import { resolveFoundationSrcDir } from './utils/foundation-source-root.js'
|
|
31
34
|
|
|
@@ -635,12 +638,67 @@ function buildComponentEntry(name, relativePath, meta) {
|
|
|
635
638
|
else delete entry.children
|
|
636
639
|
|
|
637
640
|
if (meta.data !== undefined) entry.data = lowerData(meta.data)
|
|
641
|
+
refuseDataDeclarations(`${name} (meta.js)`, entry.data)
|
|
638
642
|
|
|
639
643
|
const problems = [...content.problems, ...describeChildren(meta).problems]
|
|
640
644
|
if (problems.length > 0) loweringProblems.set(entry, problems)
|
|
641
645
|
return entry
|
|
642
646
|
}
|
|
643
647
|
|
|
648
|
+
/**
|
|
649
|
+
* Refuse a `data:` key typed by a schema that is not written as one: the retired `/*` suffix, a
|
|
650
|
+
* property of the long form other than `schema`, `single` and `whole`, or a flag that is not a
|
|
651
|
+
* boolean (`schemaDeclarationOf`; ruled 2026-10-07 [Diego]). A refusal, not a warning: the runtime would
|
|
652
|
+
* read a mistyped flag as absent, and deliver a list where the author asked for one record.
|
|
653
|
+
*
|
|
654
|
+
* @param {string} owner - who declares it, for the message
|
|
655
|
+
* @param {*} data - a `data:` map, lowered
|
|
656
|
+
*/
|
|
657
|
+
function refuseDataDeclarations(owner, data) {
|
|
658
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return
|
|
659
|
+
for (const [key, value] of Object.entries(data)) {
|
|
660
|
+
try {
|
|
661
|
+
schemaDeclarationOf(value)
|
|
662
|
+
} catch (err) {
|
|
663
|
+
throw new Error(`[uniweb] ${owner}: data.${key}: ${err.message}`)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Refuse `single` or `whole` on a key whose schema is a LIST — `@std/nav`, `@std/form`: a schema
|
|
670
|
+
* whose root is one list section describes the key's whole value, so the key holds that list, and
|
|
671
|
+
* neither one record of it nor a record as stored means anything. Checked once the schemas resolve.
|
|
672
|
+
*
|
|
673
|
+
* @param {Array<[string, *]>} owners - `[who, data:]` pairs
|
|
674
|
+
* @param {Object} dataSchemas - normalized schemas by ref
|
|
675
|
+
* @param {string} srcDir - to resolve a ref no binding brought in (a layout's)
|
|
676
|
+
*/
|
|
677
|
+
async function refuseFlagsOnListSchemas(owners, dataSchemas, srcDir) {
|
|
678
|
+
for (const [owner, data] of owners) {
|
|
679
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) continue
|
|
680
|
+
for (const [key, value] of Object.entries(data)) {
|
|
681
|
+
const declaration = schemaDeclarationOf(value)
|
|
682
|
+
if (!declaration || (!declaration.single && !declaration.whole)) continue
|
|
683
|
+
let schema = dataSchemas[declaration.schema]
|
|
684
|
+
if (!schema) {
|
|
685
|
+
try {
|
|
686
|
+
schema = (await buildDataSchemaMap([declaration.schema], { srcDir }))[declaration.schema]
|
|
687
|
+
} catch {
|
|
688
|
+
schema = null // an unresolvable ref is reported where refs resolve
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (schema && rootListSection(schema)) {
|
|
692
|
+
const flags = ['single', 'whole'].filter((flag) => declaration[flag]).map((flag) => `\`${flag}\``).join(' and ')
|
|
693
|
+
throw new Error(
|
|
694
|
+
`[uniweb] ${owner}: data.${key}: ${declaration.schema} is a list — its key holds that list, so ${flags} ` +
|
|
695
|
+
`does not apply. Write data.${key}: '${declaration.schema}'.`
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
644
702
|
/** `content: {}` or `content: []` — the component's way of saying it takes no content. */
|
|
645
703
|
function declaresNoContent(content) {
|
|
646
704
|
if (Array.isArray(content)) return content.length === 0
|
|
@@ -954,6 +1012,29 @@ export async function discoverComponents(srcDir, sectionPaths = DEFAULT_SECTION_
|
|
|
954
1012
|
return sections
|
|
955
1013
|
}
|
|
956
1014
|
|
|
1015
|
+
/**
|
|
1016
|
+
* Stop on a service `uniweb.supports` names by its old name (`RENAMED_SERVICES`), as
|
|
1017
|
+
* `site.yml::services` does. A foundation claiming `api` claims a service no host offers
|
|
1018
|
+
* now — and the build derives `backend` from the code itself, through kit's and
|
|
1019
|
+
* `@uniweb/api`'s literals, so a renamed claim could only ever disagree with it.
|
|
1020
|
+
*
|
|
1021
|
+
* ⛔ Before the dev-rebuild bail, like `reportSupports`' warning: dev is where the
|
|
1022
|
+
* developer is working.
|
|
1023
|
+
*
|
|
1024
|
+
* @param {string} srcDir
|
|
1025
|
+
* @param {string[]|undefined} authored - normalized `uniweb.supports`
|
|
1026
|
+
* @throws {Error} naming each old name and the new one
|
|
1027
|
+
*/
|
|
1028
|
+
function refuseRenamedSupports(srcDir, authored) {
|
|
1029
|
+
const renamed = (authored || []).filter((s) => RENAMED_SERVICES[s])
|
|
1030
|
+
if (!renamed.length) return
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
`${srcDir}/package.json: \`uniweb.supports\` names ${renamed.map((s) => `\`${s}\``).join(', ')}, which is now ` +
|
|
1033
|
+
`${renamed.map((s) => `\`${RENAMED_SERVICES[s]}\``).join(', ')} — the site's own backend is the \`backend\` ` +
|
|
1034
|
+
'service. Rename it in package.json.',
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
957
1038
|
/**
|
|
958
1039
|
* Say what the derivation added, at the one moment the developer is looking.
|
|
959
1040
|
*
|
|
@@ -1138,6 +1219,8 @@ export async function buildSchema(srcDir, sectionPaths, derivedSupports = null)
|
|
|
1138
1219
|
// Load configuration from foundation.js
|
|
1139
1220
|
const foundationConfig = await loadFoundationConfig(srcDir)
|
|
1140
1221
|
|
|
1222
|
+
refuseDataDeclarations('the foundation (main.js)', lowerData(foundationConfig.data))
|
|
1223
|
+
|
|
1141
1224
|
// Discover section types
|
|
1142
1225
|
const components = await discoverComponents(srcDir, sectionPaths)
|
|
1143
1226
|
reportPlacementDeclarations(components)
|
|
@@ -1154,6 +1237,18 @@ export async function buildSchema(srcDir, sectionPaths, derivedSupports = null)
|
|
|
1154
1237
|
|
|
1155
1238
|
// Discover layouts from src/layouts/
|
|
1156
1239
|
const layouts = await discoverLayoutsInPath(srcDir)
|
|
1240
|
+
// The layout a page that names none renders with — the entry gets the same answer
|
|
1241
|
+
// (`generate-entry.js`), so an editor reading `_self.defaultLayout` offers what the runtime draws.
|
|
1242
|
+
const defaultLayout = resolveDefaultLayout(foundationConfig.defaultLayout, Object.keys(layouts))
|
|
1243
|
+
await refuseFlagsOnListSchemas(
|
|
1244
|
+
[
|
|
1245
|
+
...Object.entries(components).map(([name, entry]) => [`${name} (meta.js)`, entry?.data]),
|
|
1246
|
+
...Object.entries(layouts).map(([name, entry]) => [`layout ${name} (meta.js)`, entry?.data]),
|
|
1247
|
+
['the foundation (main.js)', lowerData(foundationConfig.data)],
|
|
1248
|
+
],
|
|
1249
|
+
dataSchemas,
|
|
1250
|
+
srcDir,
|
|
1251
|
+
)
|
|
1157
1252
|
reportUnregistrableData(components, layouts, foundationConfig)
|
|
1158
1253
|
|
|
1159
1254
|
// Determine extension role
|
|
@@ -1169,8 +1264,9 @@ export async function buildSchema(srcDir, sectionPaths, derivedSupports = null)
|
|
|
1169
1264
|
}
|
|
1170
1265
|
}
|
|
1171
1266
|
|
|
1172
|
-
// Build _self, stripping the raw extension boolean in favor of normalized role
|
|
1173
|
-
|
|
1267
|
+
// Build _self, stripping the raw extension boolean in favor of normalized role, and the
|
|
1268
|
+
// declared `defaultLayout` in favor of the resolved one (`resolveDefaultLayout`)
|
|
1269
|
+
const { extension: _ext, defaultLayout: _declaredLayout, ...configWithoutExtension } = foundationConfig
|
|
1174
1270
|
|
|
1175
1271
|
// `supports` is the one identity field the build knows better than the file.
|
|
1176
1272
|
// `identity.supports` is already normalized (absent stays absent, `[]` stays
|
|
@@ -1188,6 +1284,7 @@ export async function buildSchema(srcDir, sectionPaths, derivedSupports = null)
|
|
|
1188
1284
|
// `supports` differs from the one a real build produces is a trap rather than a
|
|
1189
1285
|
// convenience — the artifact would disagree with itself depending on which
|
|
1190
1286
|
// command wrote it. One rule: the derivation runs on a real build.
|
|
1287
|
+
refuseRenamedSupports(srcDir, identity.supports)
|
|
1191
1288
|
const derived = derivedSupports && {
|
|
1192
1289
|
...derivedSupports,
|
|
1193
1290
|
services: deriveRecordsSupport(components, foundationConfig.data)
|
|
@@ -1211,7 +1308,7 @@ export async function buildSchema(srcDir, sectionPaths, derivedSupports = null)
|
|
|
1211
1308
|
...supports,
|
|
1212
1309
|
...(name && { name }),
|
|
1213
1310
|
...(foundationConfig.description && { description: foundationConfig.description }),
|
|
1214
|
-
...(
|
|
1311
|
+
...(defaultLayout && { defaultLayout }),
|
|
1215
1312
|
...(isExtension && { role: 'extension' }),
|
|
1216
1313
|
// The build's fact, last: which form the entries below hold.
|
|
1217
1314
|
schemaFormat: SCHEMA_FORMAT,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
25
|
-
import { resolveQueriesConfig, toConfigQueries } from './queries-config.js'
|
|
25
|
+
import { resolveQueriesConfig, toConfigQueries, dataKeyTyping } from './queries-config.js'
|
|
26
26
|
import { isSectionKey } from '@uniweb/schemas/section'
|
|
27
27
|
import { parseNumericPrefix, compareByNumericPrefix } from '../utils/numeric-prefix.js'
|
|
28
28
|
import { isMarkdownFile, isIgnoredFolder } from '../utils/content-files.js'
|
|
@@ -41,7 +41,7 @@ import { resolveDefaultLocale, resolvePublishableLocales, validateLanguageConfig
|
|
|
41
41
|
import { authoredRedirectTarget } from '@uniweb/core/resolve-route'
|
|
42
42
|
import { parseFrontmatter } from '../utils/frontmatter.js'
|
|
43
43
|
import { parseGrid } from '@uniweb/schemas/grid'
|
|
44
|
-
import { runtimeServicesConfig, refuseRetiredServiceKeys } from '../uwx/services-request.js'
|
|
44
|
+
import { runtimeServicesConfig, refuseRetiredServiceKeys, refuseUnreadableServices } from '../uwx/services-request.js'
|
|
45
45
|
|
|
46
46
|
// Try to import content-reader, fall back to simplified parser
|
|
47
47
|
let markdownToProseMirror
|
|
@@ -2503,6 +2503,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2503
2503
|
// Read site config and raw theme config
|
|
2504
2504
|
const siteConfig = await readYamlFile(join(sitePath, configFile))
|
|
2505
2505
|
refuseRetiredServiceKeys(siteConfig, configFile)
|
|
2506
|
+
refuseUnreadableServices(siteConfig, configFile)
|
|
2506
2507
|
|
|
2507
2508
|
// Queries are declared in TWO files — `site.yml::queries` and `queries.yml`,
|
|
2508
2509
|
// the latter winning per key — and resolving them is one question with one
|
|
@@ -2510,13 +2511,17 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2510
2511
|
// merged both, so a declaration in the second file was invisible here: never
|
|
2511
2512
|
// compiled, `data: <name>` delivering nothing, while sync pushed it fine.
|
|
2512
2513
|
//
|
|
2513
|
-
// ⭐ `config.queries` —
|
|
2514
|
-
//
|
|
2515
|
-
//
|
|
2516
|
-
//
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2514
|
+
// ⭐ `config.queries` — what a page's queries are, which a section's keys are filled by. A
|
|
2515
|
+
// backend-served payload carries its own projection of each (`schema`, `where`, `sort`,
|
|
2516
|
+
// `limit`, `scope`) since 2026-09-05. ⛔ This said the backend's projector never emitted it,
|
|
2517
|
+
// which was true only before then.
|
|
2518
|
+
//
|
|
2519
|
+
// ⭐ A query a data key types carries the type, as a push names it (`dataKeyTyping`), so a key
|
|
2520
|
+
// typed `@/post` fills from `articles:` on a static site as it does on a hosted one.
|
|
2521
|
+
const { declarations } = await resolveQueriesConfig(sitePath, { siteYml: siteConfig })
|
|
2522
|
+
const nameDefaulted = Object.values(declarations).some((d) => d?.schemaExplicit === false)
|
|
2523
|
+
const typing = nameDefaulted ? await dataKeyTyping(sitePath, declarations, { siteYml: siteConfig }) : null
|
|
2524
|
+
const byQuery = toConfigQueries(declarations, typing)
|
|
2520
2525
|
if (byQuery) siteConfig.queries = byQuery
|
|
2521
2526
|
|
|
2522
2527
|
// Record the RESOLVED base (--base > UNIWEB_BASE > site.yml::base) on the
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* must not destroy a developer's comments on the targets:/saveDeploys:
|
|
8
8
|
* regions of the file.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The loader (deploy-config.js) stays on js-yaml for read-only ingestion.
|
|
11
|
+
* ⚠️ This read "the only place in @uniweb/build that depends on `yaml`" until
|
|
12
|
+
* 2026-10-07; the pull's config writers locate entries with it too
|
|
13
|
+
* (`uwx/yaml-edit.js::editYamlText`), and print with js-yaml.
|
|
12
14
|
*
|
|
13
15
|
* `saveDeploys: false` (or `--no-save`) makes this a no-op; otherwise it
|
|
14
16
|
* touches ONLY deploys.<targetName> — plus targets.<targetName> when the file has no
|
package/src/site/plugin.js
CHANGED
|
@@ -655,16 +655,16 @@ export function siteContentPlugin(options = {}) {
|
|
|
655
655
|
}
|
|
656
656
|
|
|
657
657
|
let siteContent = null
|
|
658
|
-
// Where `uniweb dev` answers the site's `
|
|
658
|
+
// Where `uniweb dev` answers the site's `backend` service with `$devBackend`'s handler — set
|
|
659
659
|
// in `configureServer`, so never in a build.
|
|
660
660
|
let devApiAddress = null
|
|
661
661
|
/**
|
|
662
|
-
* The dev payload names `$
|
|
662
|
+
* The dev payload names `$devBackend`'s address as the site's `backend` service: the site
|
|
663
663
|
* tier, which `resolveService` reads when no host speaks. A build never gets here
|
|
664
664
|
* with one.
|
|
665
665
|
*/
|
|
666
666
|
const withDevApi = (content) => {
|
|
667
|
-
if (devApiAddress && content?.config) content.config.
|
|
667
|
+
if (devApiAddress && content?.config) content.config.backend = devApiAddress
|
|
668
668
|
return content
|
|
669
669
|
}
|
|
670
670
|
// What each section type declares its data blocks are — read once per collection of the site.
|
|
@@ -1009,10 +1009,10 @@ export function siteContentPlugin(options = {}) {
|
|
|
1009
1009
|
configureServer(devServer) {
|
|
1010
1010
|
server = devServer
|
|
1011
1011
|
|
|
1012
|
-
// A site's own backend, answered locally in development. `site.yml::$
|
|
1012
|
+
// A site's own backend, answered locally in development. `site.yml::$devBackend`
|
|
1013
1013
|
// names a module that default-exports a fetch handler; the dev server mounts it
|
|
1014
1014
|
// at an address of its own and names that address in the payload it serves
|
|
1015
|
-
// (`withDevApi`), so the site's `
|
|
1015
|
+
// (`withDevApi`), so the site's `backend` service resolves to it.
|
|
1016
1016
|
// ⚠️ Synchronous on purpose — see mountDevApi. An await here and the
|
|
1017
1017
|
// middleware lands after Vite's SPA fallback, which answers the API with
|
|
1018
1018
|
// index.html and says nothing about why.
|