@uniweb/build 0.65.0 → 0.66.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.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/schemas": "^0.8.0",
|
|
61
|
-
"@uniweb/projections": "^0.7.5",
|
|
62
|
-
"@uniweb/theming": "^0.1.15",
|
|
63
60
|
"@uniweb/content-writer": "^0.3.4",
|
|
64
61
|
"@uniweb/content-reader": "^1.2.5",
|
|
62
|
+
"@uniweb/schemas": "^0.8.0",
|
|
63
|
+
"@uniweb/projections": "^0.7.6",
|
|
64
|
+
"@uniweb/theming": "^0.1.15",
|
|
65
65
|
"@uniweb/semantic-parser": "^1.4.1"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.27.
|
|
68
|
+
"@uniweb/runtime": "^0.27.8"
|
|
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.27.8",
|
|
78
|
+
"@uniweb/core": "^0.32.0"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
package/src/i18n/records.js
CHANGED
|
@@ -361,10 +361,12 @@ function derivesExcerpt(record, whole, dataSchema, config) {
|
|
|
361
361
|
/**
|
|
362
362
|
* ⛔ A RECORD'S SYSTEM FIELDS ARE NEVER PROSE — never extracted, never translated, by
|
|
363
363
|
* either path. A `$`-prefixed key at any depth is the system's (`$name`, the handle a
|
|
364
|
-
* parametric page's URL names; `$uuid
|
|
365
|
-
*
|
|
366
|
-
* translation units, and a translation whose source
|
|
367
|
-
* so a record's page stopped matching its URL.
|
|
364
|
+
* parametric page's URL names; `$uuid`; `$branch`, the folder a compiled list holds a
|
|
365
|
+
* record under), and a top-level `slug` is how a record's reader hands the file's name
|
|
366
|
+
* on. Until 2026-09-14 they became translation units, and a translation whose source
|
|
367
|
+
* happened to equal one rewrote it, so a record's page stopped matching its URL.
|
|
368
|
+
* ⛔ A top-level `path` was one too until 2026-09-27, when the placement it named moved to
|
|
369
|
+
* `$branch` [Diego]: a field an author names `path` is theirs, and translated as its schema says.
|
|
368
370
|
*
|
|
369
371
|
* @param {string|number} key - a field name (an array index is never one)
|
|
370
372
|
* @param {boolean} topLevel - whether the key sits at the record's own level
|
|
@@ -372,7 +374,7 @@ function derivesExcerpt(record, whole, dataSchema, config) {
|
|
|
372
374
|
*/
|
|
373
375
|
function isRecordSystemField(key, topLevel) {
|
|
374
376
|
if (typeof key !== 'string') return false
|
|
375
|
-
return key.startsWith('$') || (topLevel &&
|
|
377
|
+
return key.startsWith('$') || (topLevel && key === 'slug')
|
|
376
378
|
}
|
|
377
379
|
|
|
378
380
|
/**
|
package/src/prerender.js
CHANGED
|
@@ -14,7 +14,6 @@ import { pathToFileURL } from 'node:url'
|
|
|
14
14
|
import {
|
|
15
15
|
resolveDefaultLocale,
|
|
16
16
|
resolveFetchConfigs,
|
|
17
|
-
joinPathCapture,
|
|
18
17
|
pageRouteQuery,
|
|
19
18
|
routeSelection,
|
|
20
19
|
routeParamValues,
|
|
@@ -410,13 +409,14 @@ export function expandDynamicPages(pages, fetched, onProgress = () => {}, stats
|
|
|
410
409
|
}
|
|
411
410
|
for (const paramValue of values) {
|
|
412
411
|
|
|
413
|
-
// Create concrete route: /blog/:slug → /blog/my-post
|
|
414
|
-
// record
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
412
|
+
// Create concrete route: /blog/:slug → /blog/my-post, and the same under
|
|
413
|
+
// `[...path]`: a record carries no branch, so its own URL is its handle alone
|
|
414
|
+
// (ruled 2026-09-27 [Diego]), the URL its `$route` names (`fillRoutePattern`).
|
|
415
|
+
// ⛔ Until then this prefixed the record's `path`. ⛔ A FILE PATH, so decoded: the
|
|
416
|
+
// server decodes the request before looking the file up.
|
|
417
|
+
const capture = catchAll ? String(paramValue) : null
|
|
418
418
|
const concreteRoute = catchAll
|
|
419
|
-
? page.route.replace(new RegExp(`:${catchAll}\\*$`), capture)
|
|
419
|
+
? page.route.replace(new RegExp(`:${catchAll}\\*$`), () => capture)
|
|
420
420
|
: page.route.replace(`:${paramName}`, paramValue)
|
|
421
421
|
|
|
422
422
|
// ⛔ TWO RECORDS, ONE ROUTE — normal the moment the route field is not
|
package/src/site/data-fetcher.js
CHANGED
|
@@ -88,7 +88,7 @@ export function applyWhere(items, where) {
|
|
|
88
88
|
* `@uniweb/core`'s `evaluateQuery`, the one order of work the runtime's default
|
|
89
89
|
* fetcher uses too:
|
|
90
90
|
*
|
|
91
|
-
* 1. the query's set — `scope` over each record
|
|
91
|
+
* 1. the query's set — `scope` over the branch each record is held with (`$branch`), `where`,
|
|
92
92
|
* `sort`, `limit`;
|
|
93
93
|
* 2. the fetch's `narrow` of it — `where` (and `match`), `sort`, `limit`.
|
|
94
94
|
*
|
|
@@ -72,8 +72,9 @@ export function createFileTransport({ siteRoot, distDir = null, publicDir = 'pub
|
|
|
72
72
|
const body = await readFile(file, 'utf8')
|
|
73
73
|
return response(body, path.endsWith('.json') ? 'application/json' : 'text/plain')
|
|
74
74
|
}
|
|
75
|
-
// ⚖️ Not an error here. A page can ask for a per-record file
|
|
76
|
-
// and the step reports the outcome so the caller
|
|
75
|
+
// ⚖️ Not an error here. A page can ask for a per-record file that was never written — a
|
|
76
|
+
// record this run withholds as a draft, say — and the step reports the outcome so the caller
|
|
77
|
+
// can see it rather than crash a build.
|
|
77
78
|
return missing(path)
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -58,7 +58,7 @@ import { createHash } from 'node:crypto'
|
|
|
58
58
|
import yaml from 'js-yaml'
|
|
59
59
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
60
60
|
import { parseBibtex } from '@citestyle/bibtex'
|
|
61
|
-
import { DATA_DIR, withoutRouteVariables, mapQueryPaths } from '@uniweb/core'
|
|
61
|
+
import { DATA_DIR, withoutRouteVariables, mapQueryPaths, BRANCH_KEY } from '@uniweb/core'
|
|
62
62
|
import { applyWhere, applySort, refuseUnder, refuseOutsideLanguage, refuseQueryRoute, refuseLimit } from './data-fetcher.js'
|
|
63
63
|
import { resolveAssetPath, walkContentAssets, isLocalAssetPath } from './assets.js'
|
|
64
64
|
import { readEntityPool, groupPoolBySchema, poolDirsForSchema } from './entity-pool.js'
|
|
@@ -760,8 +760,8 @@ export function flatFormRefusal(where, ref, schema, misplaced) {
|
|
|
760
760
|
* Every source file in a collection, as paths relative to the collection root —
|
|
761
761
|
* `hello.md`, `2024/spring.md`, `2024/q1/notes.yml`.
|
|
762
762
|
*
|
|
763
|
-
* Nesting is how an author
|
|
764
|
-
*
|
|
763
|
+
* Nesting is how an author organizes the files; the folder branch a record sits in, which a
|
|
764
|
+
* query's `scope:` reads, is `records/folder.yml`'s to say (`records-config.js`). Before this walk the
|
|
765
765
|
* scan was a flat `readdir`, so a record in a subdirectory was not ignored with
|
|
766
766
|
* a warning — it was invisible, and the site simply rendered without it.
|
|
767
767
|
*
|
|
@@ -800,7 +800,7 @@ function warnDuplicateSlugs(items, queryName) {
|
|
|
800
800
|
for (const item of items) {
|
|
801
801
|
if (!item || item.$name === undefined) continue
|
|
802
802
|
const name = String(item.$name)
|
|
803
|
-
const where = item
|
|
803
|
+
const where = item[BRANCH_KEY] ? `${item[BRANCH_KEY]}/` : ''
|
|
804
804
|
if (seen.has(name)) {
|
|
805
805
|
console.warn(
|
|
806
806
|
`[query-processor] Query "${queryName}" has more than one record named ` +
|
|
@@ -927,7 +927,7 @@ function pickBrief(record, keys) {
|
|
|
927
927
|
return out
|
|
928
928
|
}
|
|
929
929
|
for (const [key, value] of Object.entries(record)) {
|
|
930
|
-
if (!key.startsWith('$') && !['slug', '
|
|
930
|
+
if (!key.startsWith('$') && !['slug', 'excerpt', 'image', 'draft'].includes(key)) out[key] = value
|
|
931
931
|
}
|
|
932
932
|
return out
|
|
933
933
|
}
|
|
@@ -966,10 +966,17 @@ async function collectItems(siteDir, config, recordsRoot, basePath, locale = nul
|
|
|
966
966
|
// BibTeX → CSL-JSON bibliography items).
|
|
967
967
|
let items = await readPooledRecords(pooled, config, siteDir, recordsRoot, basePath)
|
|
968
968
|
|
|
969
|
-
// ⭐ `
|
|
969
|
+
// ⭐ `$branch` IS THE FOLDER `folder.yml` PLACED THE RECORD IN — `''` at the top —
|
|
970
970
|
// and it is the whole reason folders exist: `scope: archive` is how a query asks
|
|
971
971
|
// for a slice. Structure is query scope, not navigation.
|
|
972
972
|
//
|
|
973
|
+
// ⛔ THE COMPILED FILE'S OWN KEY, NOT THE RECORD'S (ruled 2026-09-27 [Diego]): the
|
|
974
|
+
// browser evaluates `scope` over this file, so the file holds each record's branch, and
|
|
975
|
+
// no answer carries it — `@uniweb/core`'s `evaluateQuery` reads it and drops it, as a
|
|
976
|
+
// records service answers no branch. ⛔ Until then it was `path`: it reached every
|
|
977
|
+
// component, wrote over an authored field named `path`, and made a `[...path]` record's
|
|
978
|
+
// link `<branch>/<name>`.
|
|
979
|
+
//
|
|
973
980
|
// ⛔ THIS WAS HARDCODED TO `''` FOR A WHILE, AND THE COMMENT SAID "until the
|
|
974
981
|
// folder producer lands". It landed, and this was not revisited — so every
|
|
975
982
|
// folder slice matched NOTHING on the delivery lane, silently, which is the one
|
|
@@ -979,9 +986,9 @@ async function collectItems(siteDir, config, recordsRoot, basePath, locale = nul
|
|
|
979
986
|
// ⚠️ It stays a SCALAR. `@uniweb/core`'s `withinScope` matches strings only, so
|
|
980
987
|
// an array would match nothing — one placement per record is the ruling.
|
|
981
988
|
items = items.map((result, i) => {
|
|
982
|
-
const
|
|
983
|
-
if (Array.isArray(result)) return result.map((item) => item && { ...item,
|
|
984
|
-
return result && { ...result,
|
|
989
|
+
const branch = pooled[i] ? (config.placements?.get(pooled[i].id)?.path ?? '') : ''
|
|
990
|
+
if (Array.isArray(result)) return result.map((item) => item && { ...item, [BRANCH_KEY]: branch })
|
|
991
|
+
return result && { ...result, [BRANCH_KEY]: branch }
|
|
985
992
|
})
|
|
986
993
|
|
|
987
994
|
// Flatten one level: a .bib file contributes its entries individually. Every other
|
|
@@ -1053,7 +1060,8 @@ async function collectItems(siteDir, config, recordsRoot, basePath, locale = nul
|
|
|
1053
1060
|
// since 2026-09-11, could select anything from it.
|
|
1054
1061
|
|
|
1055
1062
|
// ⭐ EACH RECORD'S BRIEF — what its list holds: the brief's fields at the top, beside the record's
|
|
1056
|
-
// own keys
|
|
1063
|
+
// own keys, what the build derives (`excerpt`, `image`), and the branch the list holds for `scope`
|
|
1064
|
+
// (`$branch`, which no answer carries). A record of a schema with no
|
|
1057
1065
|
// brief, or of one with a single section, is its brief as it is.
|
|
1058
1066
|
const briefOf = briefBuilder(config.dataSchema)
|
|
1059
1067
|
if (briefOf) {
|
|
@@ -1127,7 +1135,7 @@ export async function processQueries(siteDir, queriesConfig, recordsDir, basePat
|
|
|
1127
1135
|
}
|
|
1128
1136
|
|
|
1129
1137
|
// ⭐ EVERY FILE IN `records/` IS A RECORD (ruled 2026-09-21 [Diego]), so every one
|
|
1130
|
-
// is compiled; `records/folder.yml` only says which folder each sits in (`
|
|
1138
|
+
// is compiled; `records/folder.yml` only says which folder each sits in (`$branch`),
|
|
1131
1139
|
// which is what a query's `scope` reads. ⛔ Until 2026-09-21 `records.yml`, at the
|
|
1132
1140
|
// site root, listed the records, and an unlisted file was left out of
|
|
1133
1141
|
// `/data/<name>.json` here.
|
|
@@ -1347,7 +1355,9 @@ export async function writeQueryFiles(siteDir, byQuery, queriesConfig = null) {
|
|
|
1347
1355
|
const handle = item.$name
|
|
1348
1356
|
if (handle === undefined || handle === null || handle === '') continue
|
|
1349
1357
|
const filename = `${handle}.json`
|
|
1350
|
-
|
|
1358
|
+
// The record's own file answers it whole, so it holds no branch (the list does, for `scope`).
|
|
1359
|
+
const { [BRANCH_KEY]: _branch, ...own } = item
|
|
1360
|
+
await writeFile(join(recordsDir, filename), JSON.stringify(item[WHOLE_RECORD] ?? own, null, 2))
|
|
1351
1361
|
written.add(filename)
|
|
1352
1362
|
}
|
|
1353
1363
|
const pruned = await pruneOrphanedRecords(dataDir, name, written)
|
|
@@ -153,7 +153,7 @@ export async function readRecordsConfig(siteRoot, { dir } = {}) {
|
|
|
153
153
|
*
|
|
154
154
|
* ⛔ ONE PLACEMENT PER RECORD. Two entries matching one file is a hard error, not
|
|
155
155
|
* a second placement. The wire could carry many-to-many — `folder.js` nests, and
|
|
156
|
-
* a placement is banked by the record it references — but a record's
|
|
156
|
+
* a placement is banked by the record it references — but a record's branch is one string:
|
|
157
157
|
* `@uniweb/core`'s `withinScope` matches nothing that is not a string, so a record
|
|
158
158
|
* with two paths would fall outside every `scope:`, silently. The records service
|
|
159
159
|
* evaluates `scope` natively, so widening it is a cross-lane change to agree first,
|