@uniweb/build 0.79.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 +4 -4
- package/src/foundation/default-layout.js +52 -0
- package/src/generate-entry.js +11 -0
- package/src/schema.js +8 -3
- package/src/site/content-collector.js +12 -8
- package/src/site/queries-config.js +55 -2
- package/src/site/query-processor.js +40 -32
- package/src/uwx/data-key-types.js +4 -2
- package/src/uwx/index.js +3 -0
- package/src/uwx/site-diff.js +6 -2
- package/src/uwx/site-project.js +1 -1
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/semantic-parser": "^1.5.1",
|
|
60
61
|
"@uniweb/projections": "^0.7.15",
|
|
61
62
|
"@uniweb/content-writer": "^0.3.4",
|
|
62
|
-
"@uniweb/semantic-parser": "^1.5.1",
|
|
63
63
|
"@uniweb/schemas": "^0.13.0",
|
|
64
64
|
"@uniweb/content-reader": "^1.2.8",
|
|
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,7 +74,7 @@
|
|
|
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/runtime": "^0.29.
|
|
77
|
+
"@uniweb/runtime": "^0.29.4",
|
|
78
78
|
"@uniweb/core": "^0.38.0"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
@@ -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
|
+
}
|
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,
|
package/src/schema.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
unnameableIn,
|
|
29
29
|
} from './foundation/derive-supports.js'
|
|
30
30
|
import { RENAMED_SERVICES } from './uwx/services-request.js'
|
|
31
|
+
import { resolveDefaultLayout } from './foundation/default-layout.js'
|
|
31
32
|
import { foundationNameOf, splitFoundationName } from './foundation-name.js'
|
|
32
33
|
import { resolveFoundationSrcDir } from './utils/foundation-source-root.js'
|
|
33
34
|
|
|
@@ -1236,6 +1237,9 @@ export async function buildSchema(srcDir, sectionPaths, derivedSupports = null)
|
|
|
1236
1237
|
|
|
1237
1238
|
// Discover layouts from src/layouts/
|
|
1238
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))
|
|
1239
1243
|
await refuseFlagsOnListSchemas(
|
|
1240
1244
|
[
|
|
1241
1245
|
...Object.entries(components).map(([name, entry]) => [`${name} (meta.js)`, entry?.data]),
|
|
@@ -1260,8 +1264,9 @@ export async function buildSchema(srcDir, sectionPaths, derivedSupports = null)
|
|
|
1260
1264
|
}
|
|
1261
1265
|
}
|
|
1262
1266
|
|
|
1263
|
-
// Build _self, stripping the raw extension boolean in favor of normalized role
|
|
1264
|
-
|
|
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
|
|
1265
1270
|
|
|
1266
1271
|
// `supports` is the one identity field the build knows better than the file.
|
|
1267
1272
|
// `identity.supports` is already normalized (absent stays absent, `[]` stays
|
|
@@ -1303,7 +1308,7 @@ export async function buildSchema(srcDir, sectionPaths, derivedSupports = null)
|
|
|
1303
1308
|
...supports,
|
|
1304
1309
|
...(name && { name }),
|
|
1305
1310
|
...(foundationConfig.description && { description: foundationConfig.description }),
|
|
1306
|
-
...(
|
|
1311
|
+
...(defaultLayout && { defaultLayout }),
|
|
1307
1312
|
...(isExtension && { role: 'extension' }),
|
|
1308
1313
|
// The build's fact, last: which form the entries below hold.
|
|
1309
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'
|
|
@@ -2511,13 +2511,17 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2511
2511
|
// merged both, so a declaration in the second file was invisible here: never
|
|
2512
2512
|
// compiled, `data: <name>` delivering nothing, while sync pushed it fine.
|
|
2513
2513
|
//
|
|
2514
|
-
// ⭐ `config.queries` —
|
|
2515
|
-
//
|
|
2516
|
-
//
|
|
2517
|
-
//
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
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)
|
|
2521
2525
|
if (byQuery) siteConfig.queries = byQuery
|
|
2522
2526
|
|
|
2523
2527
|
// Record the RESOLVED base (--base > UNIWEB_BASE > site.yml::base) on the
|
|
@@ -41,6 +41,7 @@ import { refuseQueryRoute, refuseLimit, refuseFilter } from './data-fetcher.js'
|
|
|
41
41
|
import { readFile } from 'node:fs/promises'
|
|
42
42
|
import yaml from 'js-yaml'
|
|
43
43
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
44
|
+
import { dataKeyTypes } from '../uwx/data-key-types.js'
|
|
44
45
|
|
|
45
46
|
// Read its own YAML rather than importing the site build's helper. That import
|
|
46
47
|
// pointed the wrong way — a config resolver reaching into the collector that
|
|
@@ -320,21 +321,73 @@ function refuseMethod(method, where) {
|
|
|
320
321
|
* held a key a declaration may want for something else [Diego, 2026-09-27]. Until then every
|
|
321
322
|
* query on the payload read `{ name: 'articles', … }` under `articles`.
|
|
322
323
|
*
|
|
324
|
+
* ⭐ A query that declares no `schema:` and whose records a data key types carries the TYPE —
|
|
325
|
+
* `articles` → `@/post` (`dataKeyTyping`) — which is what a push names it on the wire
|
|
326
|
+
* (`uwx/site.js::queriesNested`). The schema a page's query carries is what a section's key is
|
|
327
|
+
* filled by at render (automatic `as`), so the two lanes fill the same keys. ⛔ Until 2026-10-08 the
|
|
328
|
+
* static payload carried the name (`@/articles`), and a key typed `@/post` filled on a hosted site
|
|
329
|
+
* and stayed empty on a static one.
|
|
330
|
+
*
|
|
323
331
|
* Returns undefined for a site with no queries, so `config.queries`
|
|
324
332
|
* stays absent rather than becoming an empty object — an empty object reads as
|
|
325
333
|
* "declared, and empty" to anything checking for presence.
|
|
334
|
+
*
|
|
335
|
+
* @param {object} declarations - `resolveQueriesConfig(…).declarations`
|
|
336
|
+
* @param {Map<string, string>|null} [typing] - `dataKeyTyping`: name-defaulted ref → its type
|
|
326
337
|
*/
|
|
327
|
-
export function toConfigQueries(declarations) {
|
|
338
|
+
export function toConfigQueries(declarations, typing = null) {
|
|
328
339
|
const names = Object.keys(declarations || {})
|
|
329
340
|
if (names.length === 0) return undefined
|
|
330
341
|
const out = {}
|
|
331
342
|
for (const key of names) {
|
|
332
343
|
const { schemaExplicit, name, ...rest } = declarations[key]
|
|
333
|
-
|
|
344
|
+
const typed = schemaExplicit === false ? typing?.get(rest.schema) : null
|
|
345
|
+
out[key] = typed ? { ...rest, schema: typed } : rest
|
|
334
346
|
}
|
|
335
347
|
return out
|
|
336
348
|
}
|
|
337
349
|
|
|
350
|
+
/**
|
|
351
|
+
* ⭐ THE TYPE A DATA KEY GIVES A NAME — `@/articles` → `@/post` (ruled 2026-09-25 [Diego];
|
|
352
|
+
* `uwx/data-key-types.js`), decided for the static build as a push decides it
|
|
353
|
+
* (`uwx/records.js::typedFor`), so both lanes give a query one schema.
|
|
354
|
+
*
|
|
355
|
+
* A name-defaulted ref, `@/<name>`, stands for the type of the data key of that name when:
|
|
356
|
+
*
|
|
357
|
+
* - no data schema has the name — `@/<name>` resolves to nothing;
|
|
358
|
+
* - the foundation's section types declare the key `<name>` with one type, and it resolves;
|
|
359
|
+
* - no query asks for `@/<name>` explicitly — that one is the author's, and fails loudly.
|
|
360
|
+
*
|
|
361
|
+
* Then `records/<name>/` holds records of the type, and a query named `<name>` that declares no
|
|
362
|
+
* schema is a query of it. ⚠️ Where the records live on disk and how their translations are keyed
|
|
363
|
+
* stay keyed by the name.
|
|
364
|
+
*
|
|
365
|
+
* @param {string} siteRoot
|
|
366
|
+
* @param {object} declarations - `resolveQueriesConfig(…).declarations`, for which refs are explicit
|
|
367
|
+
* @param {object} [opts]
|
|
368
|
+
* @param {object} [opts.siteYml] - an already-read site.yml
|
|
369
|
+
* @returns {Promise<Map<string, string>>} name-defaulted ref → the type; empty when none
|
|
370
|
+
*/
|
|
371
|
+
export async function dataKeyTyping(siteRoot, declarations, opts = {}) {
|
|
372
|
+
const typing = new Map()
|
|
373
|
+
const siteYml = opts.siteYml || (await readYamlFile(join(siteRoot, 'site.yml')))
|
|
374
|
+
const keyTypes = dataKeyTypes(await foundationSections(siteRoot, siteYml))
|
|
375
|
+
if (keyTypes.size === 0) return typing
|
|
376
|
+
const explicit = new Set(
|
|
377
|
+
Object.values(declarations || {})
|
|
378
|
+
.filter((d) => d && d.schemaExplicit === true)
|
|
379
|
+
.map((d) => d.schema)
|
|
380
|
+
)
|
|
381
|
+
const named = [...keyTypes.keys()].map(defaultSchema)
|
|
382
|
+
const { schemas } = await resolveRecordSchemas(siteRoot, [...named, ...keyTypes.values()], { siteYml })
|
|
383
|
+
for (const [key, type] of keyTypes) {
|
|
384
|
+
const ref = defaultSchema(key)
|
|
385
|
+
if (schemas[ref] || explicit.has(ref) || !schemas[type]) continue
|
|
386
|
+
typing.set(ref, type)
|
|
387
|
+
}
|
|
388
|
+
return typing
|
|
389
|
+
}
|
|
390
|
+
|
|
338
391
|
|
|
339
392
|
/**
|
|
340
393
|
* The data schemas a site's records are shaped by, keyed by ref — each resolved from
|
|
@@ -65,8 +65,8 @@ import { readEntityPool, groupPoolBySchema, poolDirsForSchema } from './entity-p
|
|
|
65
65
|
import { readRecordsConfig, resolveFolder, folderTreeOrder } from './records-config.js'
|
|
66
66
|
import { isDraftRecord } from './record-draft.js'
|
|
67
67
|
import { refuseNotOneRecord } from './record-file.js'
|
|
68
|
-
import { resolveRecordSchemas, resolveQueriesConfig,
|
|
69
|
-
import {
|
|
68
|
+
import { resolveRecordSchemas, resolveQueriesConfig, dataKeyTyping } from './queries-config.js'
|
|
69
|
+
import { keyOfDefaultRef } from '../uwx/data-key-types.js'
|
|
70
70
|
import { parseFrontmatter } from '../utils/frontmatter.js'
|
|
71
71
|
import { toDeliveredRecord, toStoredRecord, storedPath, briefFieldMap, contentBodyField, misplacedFields, mapReferences, recordLayout } from '@uniweb/schemas/conform'
|
|
72
72
|
import { collectNestedRefs } from '@uniweb/schemas/format'
|
|
@@ -1197,10 +1197,24 @@ export async function processQueries(siteDir, queriesConfig, recordsDir, basePat
|
|
|
1197
1197
|
const poolBySchema = groupPoolBySchema(pool.entities)
|
|
1198
1198
|
const recordsRoot = resolve(siteDir, pool.dir)
|
|
1199
1199
|
|
|
1200
|
+
// ⭐ A FOLDER NAMED FOR A DATA KEY HOLDS RECORDS OF THE KEY'S TYPE (ruled 2026-09-25 [Diego];
|
|
1201
|
+
// `dataKeyTyping`): `records/articles/`, when no data schema has the name and the foundation types
|
|
1202
|
+
// `articles` as `@/post`, holds `@/post` records — so they join that type's records, as a push
|
|
1203
|
+
// sends them, and any query of `@/post` reads them, as a host answers one. ⛔ Until 2026-10-08 they
|
|
1204
|
+
// stayed under `@/articles`: only a query named `articles` read them, and it carried `@/articles`
|
|
1205
|
+
// on the payload where a push named `@/post`.
|
|
1206
|
+
const typing = await recordTyping(siteDir, queriesConfig, poolBySchema)
|
|
1207
|
+
for (const [ref, type] of typing) {
|
|
1208
|
+
const held = poolBySchema.get(ref)
|
|
1209
|
+
if (!held) continue
|
|
1210
|
+
poolBySchema.delete(ref)
|
|
1211
|
+
poolBySchema.set(type, [...(poolBySchema.get(type) || []), ...held])
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1200
1214
|
// ⭐ THE DATA SCHEMA EACH QUERY'S RECORDS ARE DELIVERED IN (`deliverRecord`) — resolved
|
|
1201
1215
|
// from the foundation's source once for every query. A query with none compiles its
|
|
1202
1216
|
// records as their files hold them.
|
|
1203
|
-
const dataSchemas = options.dataSchemas ?? (await querySchemas(siteDir, queriesConfig))
|
|
1217
|
+
const dataSchemas = options.dataSchemas ?? (await querySchemas(siteDir, queriesConfig, typing))
|
|
1204
1218
|
const references = referenceHydrator({ poolBySchema, dataSchemas, siteDir, recordsRoot, basePath, includeDrafts })
|
|
1205
1219
|
|
|
1206
1220
|
const results = {}
|
|
@@ -1211,6 +1225,9 @@ export async function processQueries(siteDir, queriesConfig, recordsDir, basePat
|
|
|
1211
1225
|
// be an empty `/data/<name>.json` standing in for a live source.
|
|
1212
1226
|
if (config && typeof config === 'object' && config.url !== undefined) continue
|
|
1213
1227
|
const parsed = parseQueryConfig(name, config)
|
|
1228
|
+
// A query that still names its folder (`@/articles`) is a query of the folder's type.
|
|
1229
|
+
const folderRef = parsed.schema
|
|
1230
|
+
if (parsed.schema && typing.has(parsed.schema)) parsed.schema = typing.get(parsed.schema)
|
|
1214
1231
|
parsed.poolEntities = parsed.schema ? poolBySchema.get(parsed.schema) || [] : []
|
|
1215
1232
|
parsed.placements = folder.placements
|
|
1216
1233
|
parsed.folderRank = folderRank
|
|
@@ -1218,7 +1235,7 @@ export async function processQueries(siteDir, queriesConfig, recordsDir, basePat
|
|
|
1218
1235
|
parsed.dataSchema = (parsed.schema && dataSchemas[parsed.schema]) || null
|
|
1219
1236
|
parsed.references = references
|
|
1220
1237
|
if (parsed.poolEntities.length === 0) {
|
|
1221
|
-
const dirs =
|
|
1238
|
+
const dirs = folderRef ? poolDirsForSchema(folderRef) : null
|
|
1222
1239
|
console.warn(
|
|
1223
1240
|
`[query-processor] Query "${name}" matches no records — ` +
|
|
1224
1241
|
(dirs
|
|
@@ -1240,12 +1257,12 @@ export async function processQueries(siteDir, queriesConfig, recordsDir, basePat
|
|
|
1240
1257
|
* its records are compiled as their files hold them, which is not the shape a
|
|
1241
1258
|
* component receives from a host.
|
|
1242
1259
|
*/
|
|
1243
|
-
async function querySchemas(siteDir, queriesConfig) {
|
|
1244
|
-
const refs = []
|
|
1260
|
+
async function querySchemas(siteDir, queriesConfig, typing = new Map()) {
|
|
1261
|
+
const refs = [...typing.values()]
|
|
1245
1262
|
for (const config of Object.values(queriesConfig)) {
|
|
1246
1263
|
if (config && typeof config === 'object' && config.url !== undefined) continue
|
|
1247
1264
|
const ref = typeof config === 'string' ? config : config?.schema
|
|
1248
|
-
if (typeof ref === 'string') refs.push(ref)
|
|
1265
|
+
if (typeof ref === 'string' && !typing.has(ref)) refs.push(ref)
|
|
1249
1266
|
}
|
|
1250
1267
|
const { schemas, failures } = await resolveRecordSchemas(siteDir, refs)
|
|
1251
1268
|
for (const { ref, message } of failures) {
|
|
@@ -1254,28 +1271,25 @@ async function querySchemas(siteDir, queriesConfig) {
|
|
|
1254
1271
|
`their files hold them rather than as a component receives them: ${message}`
|
|
1255
1272
|
)
|
|
1256
1273
|
}
|
|
1257
|
-
await typeByDataKey(siteDir, queriesConfig, schemas)
|
|
1258
1274
|
return schemas
|
|
1259
1275
|
}
|
|
1260
1276
|
|
|
1261
1277
|
/**
|
|
1262
|
-
* ⭐
|
|
1263
|
-
* `
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1266
|
-
*
|
|
1267
|
-
* query asked for explicitly: that one is the author's, and does not resolve. ⛔ Until 2026-09-27
|
|
1268
|
-
* this lane compiled them as if they had no data schema, while a push typed them.
|
|
1278
|
+
* ⭐ WHICH FOLDERS HOLD RECORDS OF A DATA KEY'S TYPE (ruled 2026-09-25 [Diego]): `@/articles` →
|
|
1279
|
+
* `@/post` — `dataKeyTyping`, the rule a push applies, asked only when the site has a folder or a
|
|
1280
|
+
* query it could apply to: a name-defaulted ref, `@/<name>`. ⛔ Until 2026-10-08 this lane typed a
|
|
1281
|
+
* query's RECORDS by aliasing its `@/<name>` to the type's schema, while its query and its folder
|
|
1282
|
+
* kept the name.
|
|
1269
1283
|
*
|
|
1270
1284
|
* @param {string} siteDir
|
|
1271
|
-
* @param {Object} queriesConfig
|
|
1272
|
-
* @param {
|
|
1285
|
+
* @param {Object} queriesConfig - what the build compiles (`config.queries`)
|
|
1286
|
+
* @param {Map<string, Array>} poolBySchema - the records, by the ref their folder names
|
|
1287
|
+
* @returns {Promise<Map<string, string>>} name-defaulted ref → the type
|
|
1273
1288
|
*/
|
|
1274
|
-
async function
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
)
|
|
1278
|
-
if (untyped.length === 0) return
|
|
1289
|
+
async function recordTyping(siteDir, queriesConfig, poolBySchema) {
|
|
1290
|
+
const candidate = (ref) => typeof ref === 'string' && keyOfDefaultRef(ref) !== null
|
|
1291
|
+
const queried = Object.values(queriesConfig).map((c) => (typeof c === 'string' ? c : c?.schema))
|
|
1292
|
+
if (![...poolBySchema.keys(), ...queried].some(candidate)) return new Map()
|
|
1279
1293
|
// `config.queries` carries no trace of whether its author wrote the schema; the declarations do.
|
|
1280
1294
|
let declarations = {}
|
|
1281
1295
|
try {
|
|
@@ -1283,17 +1297,11 @@ async function typeByDataKey(siteDir, queriesConfig, schemas) {
|
|
|
1283
1297
|
} catch {
|
|
1284
1298
|
// a declaration the build refuses is reported where the build reads it
|
|
1285
1299
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
for (const [name, c] of pending) {
|
|
1291
|
-
const type = keyTypes.get(name)
|
|
1292
|
-
if (type) wanted.set(c.schema, type)
|
|
1300
|
+
try {
|
|
1301
|
+
return await dataKeyTyping(siteDir, declarations)
|
|
1302
|
+
} catch {
|
|
1303
|
+
return new Map() // a foundation that does not read is reported by the build that reads it
|
|
1293
1304
|
}
|
|
1294
|
-
if (wanted.size === 0) return
|
|
1295
|
-
const { schemas: types } = await resolveRecordSchemas(siteDir, wanted.values())
|
|
1296
|
-
for (const [ref, type] of wanted) if (types[type]) schemas[ref] = types[type]
|
|
1297
1305
|
}
|
|
1298
1306
|
|
|
1299
1307
|
/**
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// records folder (`records/team/`). When no data schema has that name, but the foundation's
|
|
8
8
|
// section types declare the data key of that name with a type — `data: { team: '@/member' }` in a
|
|
9
9
|
// `meta.js` — the records ARE of that type: a push sends them as its entities, and the query names
|
|
10
|
-
// it on the wire
|
|
11
|
-
//
|
|
10
|
+
// it on the wire; the static build names it on the payload and reads them as the type's records
|
|
11
|
+
// (`site/queries-config.js::dataKeyTyping`, since 2026-10-08 — until then the payload named the query
|
|
12
|
+
// `@/team`). ⛔ Until 2026-09-25 they were read as having no data schema at all and went out as
|
|
13
|
+
// static files, although the one section that reads them declared what they are.
|
|
12
14
|
//
|
|
13
15
|
// ⚠️ THIS DECIDES ONLY WHICH DATA SCHEMA THE RECORDS BELONG TO. Where they live on disk, how the
|
|
14
16
|
// site builds them and how their translations are keyed stay keyed by the name (`team`), as the
|
package/src/uwx/index.js
CHANGED
|
@@ -88,6 +88,9 @@ export {
|
|
|
88
88
|
sectionRecordToFile,
|
|
89
89
|
pageSectionsToFiles,
|
|
90
90
|
siteContentDocumentToProject,
|
|
91
|
+
// The file the author keeps a section in (`1-hero.md` holds `hero`) — what a section
|
|
92
|
+
// unit's path names on disk, for a push recording what it delivered.
|
|
93
|
+
existingSectionFile,
|
|
91
94
|
} from './site-project.js'
|
|
92
95
|
export {
|
|
93
96
|
createTranslationCollector,
|
package/src/uwx/site-diff.js
CHANGED
|
@@ -96,8 +96,12 @@ export function collectSiteUnits(doc, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.
|
|
|
96
96
|
export function walkSiteUnits(doc, cb, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale) {
|
|
97
97
|
const walkSections = (sections, dir) => {
|
|
98
98
|
for (const record of sections || []) {
|
|
99
|
-
// The projector's own file rule
|
|
100
|
-
//
|
|
99
|
+
// The projector's own file rule — including an app-created section named from its
|
|
100
|
+
// `$uuid`. ⚠️ The path NAMES the unit by its id; it is not always the author's file:
|
|
101
|
+
// a pull writes into an existing `1-about.md` or `@about.md` in place
|
|
102
|
+
// (`existingSectionFile`), so a reader mapping a unit to disk looks it up there.
|
|
103
|
+
// (This said "a unit's path here is the file it is written to" until 2026-10-07,
|
|
104
|
+
// and a push recorded no file for a numbered section.)
|
|
101
105
|
const base = sectionFileBase(record)
|
|
102
106
|
if (base) cb(`${dir}/${base}.md`, record, 'section')
|
|
103
107
|
walkSections(record.$children, dir)
|
package/src/uwx/site-project.js
CHANGED
|
@@ -587,7 +587,7 @@ function listedNames(list) {
|
|
|
587
587
|
//
|
|
588
588
|
// ⛔ A top-level section never takes an `@` file: that is a child's name, and the build renders an
|
|
589
589
|
// `@` file no `nest:` claims only as an orphan appended at the end — listed by name, it is not rendered.
|
|
590
|
-
function existingSectionFile(pageDir, fileBase, stableId, { child = false } = {}) {
|
|
590
|
+
export function existingSectionFile(pageDir, fileBase, stableId, { child = false } = {}) {
|
|
591
591
|
if (!existsSync(pageDir)) return null
|
|
592
592
|
const entries = readdirSync(pageDir).filter((e) => isMarkdownFile(e) && (child || !e.startsWith('@')))
|
|
593
593
|
const found =
|