@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.65.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.7"
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/core": "^0.31.0",
78
- "@uniweb/runtime": "^0.27.7"
77
+ "@uniweb/runtime": "^0.27.8",
78
+ "@uniweb/core": "^0.32.0"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "vite": {
@@ -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`), and a record's top-level `slug` and `path`
365
- * are its handle and its placement in `folder.yml`. Until 2026-09-14 they became
366
- * translation units, and a translation whose source happened to equal one rewrote it,
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 && (key === 'slug' || key === 'path'))
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. Under `[...path]` the
414
- // record's URL is its placement (the folder `folder.yml` put it in, carried
415
- // as `path`) plus its handle — the split rule in reverse. ⛔ A FILE PATH, so
416
- // decoded: the server decodes the request before looking the file up.
417
- const capture = catchAll ? joinPathCapture({ dir: item.path, slug: paramValue }) : null
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
@@ -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's placement (`path`), `where`,
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 a `deferred:` query never wrote,
76
- // and the step reports the outcome so the caller can see it rather than crash a build.
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 gives a collection an internal structure, and it is
764
- * what the `path` field and the `under` predicate address. Before this walk the
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.path ? `${item.path}/` : ''
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', 'path', 'excerpt', 'image', 'draft'].includes(key)) out[key] = value
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
- // ⭐ `path` IS THE FOLDER `folder.yml` PLACED THE RECORD IN — `''` at the top —
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 path = pooled[i] ? (config.placements?.get(pooled[i].id)?.path ?? '') : ''
983
- if (Array.isArray(result)) return result.map((item) => item && { ...item, path })
984
- return result && { ...result, path }
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 and what the build derives (`excerpt`, `image`, `path`). A record of a schema with no
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 (`path`),
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
- await writeFile(join(recordsDir, filename), JSON.stringify(item[WHOLE_RECORD] ?? item, null, 2))
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 `path` is one string:
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,