@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.
- package/package.json +8 -8
- package/src/content/index.js +0 -1
- package/src/dev/api-mount.js +30 -20
- package/src/foundation/derive-supports.js +3 -2
- package/src/resolve-data-schema.js +2 -1
- package/src/runtime-schema.js +30 -12
- package/src/schema.js +93 -1
- package/src/site/content-collector.js +21 -6
- package/src/site/data-fetcher.js +19 -0
- package/src/site/deploy-config-writer.js +4 -2
- package/src/site/index.js +1 -2
- package/src/site/plugin.js +19 -6
- package/src/site/queries-config.js +2 -1
- package/src/site/query-processor.js +6 -42
- package/src/uwx/emit-surface.json +1 -13
- package/src/uwx/index.js +12 -5
- package/src/uwx/project-writer.js +31 -21
- package/src/uwx/services-request.js +471 -198
- package/src/uwx/shown-records.js +52 -0
- package/src/uwx/site-diff.js +47 -0
- package/src/uwx/site-project.js +40 -55
- package/src/uwx/site.js +61 -162
- package/src/uwx/sync-package.js +30 -21
- package/src/uwx/yaml-edit.js +288 -0
- package/src/validate-data.js +18 -11
- package/src/uwx/yaml-upsert.js +0 -79
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
* await writeQueryFiles(siteDir, byQuery)
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
-
import { readFile, readdir,
|
|
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
|
-
//
|
|
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
|
|
1383
|
-
//
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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.
|
|
220
|
-
//
|
|
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
|
|
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
|
-
|
|
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.
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
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
|
-
|
|
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 (
|
|
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
|
-
*
|
|
286
|
-
* `
|
|
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-
|
|
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-
|
|
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
|