@uniweb/build 0.77.0 → 0.79.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.
@@ -51,7 +51,7 @@
51
51
  * await writeQueryFiles(siteDir, byQuery)
52
52
  */
53
53
 
54
- import { readFile, readdir, stat, writeFile, mkdir, copyFile, rm } from 'node:fs/promises'
54
+ import { readFile, readdir, writeFile, mkdir, copyFile, rm } from 'node:fs/promises'
55
55
  import { join, basename, extname, dirname, relative, resolve, sep, isAbsolute } from 'node:path'
56
56
  import { existsSync } from 'node:fs'
57
57
  import { createHash } from 'node:crypto'
@@ -59,7 +59,7 @@ import yaml from 'js-yaml'
59
59
  import { YAML_OPTIONS } from '../utils/yaml-schema.js'
60
60
  import { parseBibtex } from '@citestyle/bibtex'
61
61
  import { DATA_DIR, withoutRouteVariables, mapQueryPaths, BRANCH_KEY } from '@uniweb/core'
62
- import { applyWhere, applySort, refuseUnder, refuseOutsideLanguage, refuseQueryRoute, refuseLimit } from './data-fetcher.js'
62
+ import { applyWhere, applySort, refuseUnder, refuseOutsideLanguage, refuseQueryRoute, refuseLimit, refuseFilter } from './data-fetcher.js'
63
63
  import { resolveAssetPath, walkContentAssets, isLocalAssetPath } from './assets.js'
64
64
  import { readEntityPool, groupPoolBySchema, poolDirsForSchema } from './entity-pool.js'
65
65
  import { readRecordsConfig, resolveFolder, folderTreeOrder } from './records-config.js'
@@ -126,7 +126,6 @@ function parseQueryConfig(name, config) {
126
126
  scope: null,
127
127
  sort: null,
128
128
  where: null,
129
- filter: null,
130
129
  limit: 0,
131
130
  excerpt: { maxLength: 160 },
132
131
  }
@@ -136,6 +135,7 @@ function parseQueryConfig(name, config) {
136
135
  refuseOutsideLanguage(config.where, `queries.${name}`)
137
136
  refuseQueryRoute(config, `queries.${name}`)
138
137
  refuseLimit(config.limit, `queries.${name}`)
138
+ refuseFilter(config, `queries.${name}`)
139
139
  return {
140
140
  name,
141
141
  // The query's schema selects its records — `records/{schema}/` declares the
@@ -146,11 +146,8 @@ function parseQueryConfig(name, config) {
146
146
  // here until 2026-09-11: a named query's `scope` was ignored on this lane.
147
147
  scope: typeof config.scope === 'string' ? config.scope : null,
148
148
  sort: config.sort || null,
149
- // `where:` is the CANONICAL predicate; `filter:` is the deprecated string DSL
150
- // it replaced. Both are carried and both are applied below, in the same order
151
- // `data-fetcher.js::applyPostProcessing` uses — see the note there.
149
+ // The predicate. ⛔ `filter:`, the string DSL it replaced, is refused above.
152
150
  where: config.where || null,
153
- filter: config.filter || null,
154
151
  limit: config.limit || 0,
155
152
  excerpt: {
156
153
  maxLength: config.excerpt?.maxLength || 160,
@@ -1379,8 +1376,8 @@ export async function writeQueryFiles(siteDir, byQuery, queriesConfig = null) {
1379
1376
 
1380
1377
  // ⭐ TWO PAYLOADS FOR EVERY QUERY (ruled 2026-09-27 [Diego]):
1381
1378
  // 1. Each record WHOLE, as stored, at /data/<name>/<$name>.json — a key per section, the
1382
- // brief's included — what a whole question answers: a parametric page's record for a
1383
- // component that declares `'@x/y/*'`, and kit's `useWholeRecord`.
1379
+ // brief's included — what a whole question answers: a record for a component that
1380
+ // declares its key `whole: true`, and kit's `useWholeRecord`.
1384
1381
  // 2. The list at /data/<name>.json — each record's BRIEF, the brief's fields at the top —
1385
1382
  // what `query: <name>` delivers everywhere else. ⛔ `deferred:`, which left fields of the
1386
1383
  // brief out of it, was retired the same day [Diego].
@@ -1419,36 +1416,3 @@ export async function writeQueryFiles(siteDir, byQuery, queriesConfig = null) {
1419
1416
  }
1420
1417
  }
1421
1418
  }
1422
-
1423
- /**
1424
- * Get last modified time for a collection
1425
- *
1426
- * @param {string} siteDir - Site root directory
1427
- * @param {Object} config - Collection config
1428
- * @returns {Promise<Date|null>} Most recent modification time
1429
- */
1430
- export async function getQueryLastModified(siteDir, config) {
1431
- const parsed = parseQueryConfig('temp', config)
1432
- const poolDir = join(siteDir, parsed.path)
1433
-
1434
- if (!existsSync(poolDir)) {
1435
- return null
1436
- }
1437
-
1438
- const files = await readdir(poolDir)
1439
- const itemFiles = files.filter(f =>
1440
- !f.startsWith('_') &&
1441
- (f.endsWith('.md') || f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.json') || f.endsWith('.bib'))
1442
- )
1443
-
1444
- let lastModified = null
1445
-
1446
- for (const file of itemFiles) {
1447
- const fileStat = await stat(join(poolDir, file))
1448
- if (!lastModified || fileStat.mtime > lastModified) {
1449
- lastModified = fileStat.mtime
1450
- }
1451
- }
1452
-
1453
- return lastModified
1454
- }
@@ -45,9 +45,6 @@
45
45
  "agents": [
46
46
  "site.yml::agents"
47
47
  ],
48
- "assistant": [
49
- "site.yml::assistant"
50
- ],
51
48
  "base": [
52
49
  "site.yml::base"
53
50
  ],
@@ -89,19 +86,10 @@
89
86
  "publish_languages": [
90
87
  "site.yml::publishLanguages"
91
88
  ],
92
- "search": [
93
- "site.yml::search"
94
- ],
95
89
  "seo": [
96
90
  "site.yml::seo"
97
91
  ],
98
- "submit": [
99
- "site.yml::submit"
100
- ],
101
- "theme": null,
102
- "tracking": [
103
- "site.yml::tracking"
104
- ]
92
+ "theme": null
105
93
  }
106
94
  },
107
95
  "services": {
package/src/uwx/index.js CHANGED
@@ -12,6 +12,8 @@
12
12
 
13
13
  export { mintUuidV7 } from './uuid.js'
14
14
  export { createZip, readZip } from './zip.js'
15
+ // The queries whose records a site's pages show live — what `records` is for.
16
+ export { recordQueriesShown } from './shown-records.js'
15
17
  export {
16
18
  sha256Hex,
17
19
  toJsonBuffer,
@@ -52,7 +54,8 @@ export {
52
54
  queriesYmlPath,
53
55
  QUERIES_YML_RELPATH,
54
56
  } from './queries-config.js'
55
- export { upsertYamlScalar, removeYamlScalar } from './yaml-upsert.js'
57
+ // Edits to a YAML file an author wrote — the one editor; files are written through project-writer.js.
58
+ export { editYamlText, setYamlKey, replaceInYamlList } from './yaml-edit.js'
56
59
  export { buildFolderEntity,
57
60
  collectFolderItemUuids,
58
61
  stampFolderItemUuids
@@ -116,11 +119,14 @@ export {
116
119
  // A site's services request — `site.yml::services`, applied over the site's own rows.
117
120
  // Spec: kb/framework/reference/site-services-request.md
118
121
  export {
122
+ RUNTIME_KEYS,
119
123
  readServicesRequest,
120
- takeServices,
121
- mergeServiceRows,
122
- reconcileServices,
123
- recordAfter,
124
+ unreadableServices,
125
+ refuseUnreadableServices,
126
+ statedServices,
127
+ heldServices,
128
+ servicesFromDocument,
129
+ sameServiceRow,
124
130
  } from './services-request.js'
125
131
  export {
126
132
  diffSiteUnits,
@@ -130,6 +136,7 @@ export {
130
136
  walkSiteUnits,
131
137
  collectUnitUuids,
132
138
  collectQueryUuids,
139
+ siteItemsByKey,
133
140
  stampUnitUuids,
134
141
  } from './site-diff.js'
135
142
  export {
@@ -22,6 +22,7 @@ import { canonicalJson, sameMarkdownDocument } from './same-content.js'
22
22
  import { renderEntityDocument } from './backfill.js'
23
23
  import { queriesYmlPath } from './queries-config.js'
24
24
  import { folderYmlPath } from '../site/records-config.js'
25
+ import { editYamlText, YAML_DUMP_OPTS } from './yaml-edit.js'
25
26
  import { DECLARATION_KEYS } from '../site/fetch-shapes.js'
26
27
 
27
28
  // Frontmatter keys that belong to the CCA framework / the developer's local
@@ -45,9 +46,6 @@ export const DEFAULT_RESERVED_FRONTMATTER = new Set([
45
46
  // editor toggles it, so a pull must carry the store's value into the file (2026-09-28).
46
47
  ])
47
48
 
48
- // js-yaml dump options shared by every config write, so output is byte-stable.
49
- export const YAML_DUMP_OPTS = { lineWidth: -1, quotingType: "'", forceQuotes: false, noRefs: true }
50
-
51
49
  /**
52
50
  * Write `text` to `filePath` only when it differs from what's on disk, using a
53
51
  * tmp-file + rename so a reader never sees a half-written file. Creates the
@@ -216,10 +214,8 @@ export function writeSectionFile({ filePath, content, params, reserved = DEFAULT
216
214
  // Shallow-merge `changes` into a YAML config file and write idempotently. A key
217
215
  // whose value is null/undefined is deleted; an object value is shallow-merged one
218
216
  // level deep (so partial `theme` / `build` updates don't drop sibling keys) unless
219
- // the key is in `replace`; any other value replaces. NOTE: this re-dumps the file,
220
- // so author comments/order are not preserved — acceptable for machine-owned
221
- // config, but comment-preserving merges for hand-authored config files are a
222
- // quality bar to revisit.
217
+ // the key is in `replace`; any other value replaces. The file is edited, not
218
+ // re-dumped: its comments and layout stay (`writeYamlIfChanged`).
223
219
  function mergeYamlConfig(filePath, changes, { replace = [] } = {}) {
224
220
  const prior = loadYamlFile(filePath)
225
221
  const existing = isPlainObject(prior.value) ? { ...prior.value } : {}
@@ -240,23 +236,37 @@ function mergeYamlConfig(filePath, changes, { replace = [] } = {}) {
240
236
  return writeYamlIfChanged(filePath, existing, prior)
241
237
  }
242
238
 
243
- // A YAML file, parsed — and whether there was one: a missing or unreadable file is never "unchanged".
239
+ // A YAML file, parsed, with its text — and whether there was one: a missing file is never "unchanged".
240
+ // ⛔ One that EXISTS and does not parse is refused, never read as missing: read as missing, the writer
241
+ // replaced the author's whole file with only the keys it was writing (until 2026-10-07).
244
242
  function loadYamlFile(filePath) {
243
+ let text
245
244
  try {
246
- return { value: yaml.load(readFileSync(filePath, 'utf8'), YAML_OPTIONS) ?? {}, read: true }
245
+ text = readFileSync(filePath, 'utf8')
247
246
  } catch {
248
- return { value: {}, read: false }
247
+ return { value: {}, read: false, text: null }
248
+ }
249
+ try {
250
+ return { value: yaml.load(text, YAML_OPTIONS) ?? {}, read: true, text }
251
+ } catch (err) {
252
+ throw new Error(`${filePath} is not YAML that can be read, so it was not written: ${err.message.split('\n')[0]}`)
249
253
  }
250
254
  }
251
255
 
252
- // ⭐ A FILE WHOSE MEANING WOULD NOT CHANGE IS LEFT AS ITS AUTHOR WROTE IT. Every writer here re-dumps the
253
- // whole file — its comments dropped, its strings re-quoted, its lists reflowed — so a pull that restated
254
- // a file rewrote it all the same. ⛔ Until 2026-09-26 a pull into the copy that pushed it re-dumped
255
- // `site.yml`, `theme.yml` and every `page.yml` (measured on the `international` template: every comment
256
- // in its `site.yml` gone, and nothing had changed).
256
+ // ⭐ A FILE WHOSE MEANING WOULD NOT CHANGE IS LEFT AS ITS AUTHOR WROTE IT. ⛔ Until 2026-09-26 a pull into
257
+ // the copy that pushed it re-dumped `site.yml`, `theme.yml` and every `page.yml` (measured on the
258
+ // `international` template: every comment in its `site.yml` gone, and nothing had changed).
259
+ //
260
+ // ⭐ AND ONE THAT CHANGES IS EDITED, NOT RE-DUMPED (2026-10-07): only the entries that differ are
261
+ // rewritten, and every comment, blank line and list the author wrote stays (`editYamlText`). ⛔ Until then
262
+ // every writer here dumped the whole file — a pull that changed one key of `site.yml` left the Starter
263
+ // template's 93 lines as 10, and a `pull --merge` conflicted on the whole commented header. The whole dump
264
+ // remains for a file with nothing to keep (none yet, unreadable) and one the editor declines (an alias or a
265
+ // `<<` merge key, or a result that would read differently).
257
266
  function writeYamlIfChanged(filePath, value, prior) {
258
267
  if (prior.read && canonicalJson(value) === canonicalJson(prior.value)) return 'unchanged'
259
- return writeIfChanged(filePath, yaml.dump(value, YAML_DUMP_OPTS))
268
+ const edited = prior.read ? editYamlText(prior.text, prior.value, value, YAML_DUMP_OPTS) : null
269
+ return writeIfChanged(filePath, edited ?? yaml.dump(value, YAML_DUMP_OPTS))
260
270
  }
261
271
 
262
272
  const isPlainObject = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v)
@@ -280,10 +290,10 @@ export function writeSiteConfig(siteRoot, config) {
280
290
  const SITE_CONFIG_WHOLE = Object.freeze([...DECLARATION_KEYS, 'services'])
281
291
 
282
292
  /**
283
- * Write a YAML object to a file (full dump, idempotent) — for machine-owned
293
+ * Write a YAML object to a file (idempotent) — for machine-owned
284
294
  * config the projector authors wholesale, e.g. a projected `page.yml`/`folder.yml`.
285
- * (Comment/unknown-key preservation is a later refinement; `site.yml`/
286
- * `collections.yml` use the merging writers instead.)
295
+ * An existing file is edited, not re-dumped — its comments stay (`writeYamlIfChanged`);
296
+ * `site.yml` and `queries.yml` use the merging writers.
287
297
  * @returns {'updated'|'unchanged'}
288
298
  */
289
299
  export function writeYamlFile(filePath, obj) {
@@ -296,7 +306,7 @@ export function writeYamlFile(filePath, obj) {
296
306
  * absent from `projected` is removed (the projector owns the managed set
297
307
  * wholesale); unknown keys keep their value and relative order. Idempotent.
298
308
  *
299
- * Same key-preserving (comment-dropping) bar as `writeSiteConfig` — for the
309
+ * Same key- and comment-preserving bar as `writeSiteConfig` — for the
300
310
  * hand-authored `page.yml`/`folder.yml`, whose author-added keys must survive a
301
311
  * pull rather than being clobbered by a full re-dump.
302
312
  *
@@ -323,7 +333,7 @@ export function writeMergedYaml(filePath, projected, managedKeys) {
323
333
  /**
324
334
  * Merge `queries` into `queries.yml` (shallow). Preserves queries not in the
325
335
  * incoming set; each incoming one is replaced wholesale. Same key-preserving
326
- * (comment-dropping) bar as `writeSiteConfig`.
336
+ * (and comment-preserving) bar as `writeSiteConfig`.
327
337
  *
328
338
  * ⛔ `queries.yml` IS THE MAP — there is no root key, so `queries` is passed bare.
329
339
  * The predecessor wrote `{ collections: {...} }` into `collections.yml`; handing