@uniweb/build 0.66.0 → 0.66.1
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 +6 -6
- package/src/site/query-processor.js +23 -3
- package/src/site/records-config.js +126 -45
- package/src/uwx/folder.js +12 -4
- package/src/uwx/records-project.js +20 -5
- package/src/uwx/records.js +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.1",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
61
60
|
"@uniweb/content-reader": "^1.2.5",
|
|
62
61
|
"@uniweb/schemas": "^0.8.0",
|
|
63
62
|
"@uniweb/projections": "^0.7.6",
|
|
64
|
-
"@uniweb/
|
|
65
|
-
"@uniweb/semantic-parser": "^1.4.1"
|
|
63
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
64
|
+
"@uniweb/semantic-parser": "^1.4.1",
|
|
65
|
+
"@uniweb/theming": "^0.1.15"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
68
|
"@uniweb/runtime": "^0.27.8"
|
|
@@ -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/core": "^0.32.0",
|
|
78
|
+
"@uniweb/runtime": "^0.27.8"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
|
@@ -932,6 +932,16 @@ function pickBrief(record, keys) {
|
|
|
932
932
|
return out
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
+
// What a record's folder entry says about it, as the keys a record is answered with — `$tags`
|
|
936
|
+
// (in the order written) and `$label` — or null when it says nothing.
|
|
937
|
+
function entryKeys(placed) {
|
|
938
|
+
if (!placed) return null
|
|
939
|
+
const out = {}
|
|
940
|
+
if (Array.isArray(placed.tags) && placed.tags.length) out.$tags = [...placed.tags]
|
|
941
|
+
if (typeof placed.label === 'string' && placed.label) out.$label = placed.label
|
|
942
|
+
return Object.keys(out).length ? out : null
|
|
943
|
+
}
|
|
944
|
+
|
|
935
945
|
// A query's records in folder-tree order (`folderTreeOrder`), before anything reads them — so
|
|
936
946
|
// `where`, `sort` (stable) and `limit` start from the order a records service answers in.
|
|
937
947
|
function inFolderOrder(pooled, rank) {
|
|
@@ -985,10 +995,20 @@ async function collectItems(siteDir, config, recordsRoot, basePath, locale = nul
|
|
|
985
995
|
//
|
|
986
996
|
// ⚠️ It stays a SCALAR. `@uniweb/core`'s `withinScope` matches strings only, so
|
|
987
997
|
// an array would match nothing — one placement per record is the ruling.
|
|
998
|
+
//
|
|
999
|
+
// ⭐ AND WHAT THE FOLDER SAYS ABOUT THE RECORD — its entry's `tags` and `label` — rides
|
|
1000
|
+
// beside it as `$tags` and `$label`, in its list and in its own file, as a records service
|
|
1001
|
+
// answers them (ruled 2026-09-27 [Diego]). Absent when the entry says none.
|
|
988
1002
|
items = items.map((result, i) => {
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
1003
|
+
const placed = pooled[i] ? config.placements?.get(pooled[i].id) : null
|
|
1004
|
+
const said = entryKeys(placed)
|
|
1005
|
+
const at = (item) => {
|
|
1006
|
+
if (!item) return item
|
|
1007
|
+
const out = { ...item, ...said, [BRANCH_KEY]: placed?.path ?? '' }
|
|
1008
|
+
if (said && out[WHOLE_RECORD]) out[WHOLE_RECORD] = { ...out[WHOLE_RECORD], ...said }
|
|
1009
|
+
return out
|
|
1010
|
+
}
|
|
1011
|
+
return Array.isArray(result) ? result.map(at) : at(result)
|
|
992
1012
|
})
|
|
993
1013
|
|
|
994
1014
|
// Flatten one level: a .bib file contributes its entries individually. Every other
|
|
@@ -164,9 +164,25 @@ export async function readRecordsConfig(siteRoot, { dir } = {}) {
|
|
|
164
164
|
* @param {object} [opts]
|
|
165
165
|
* @param {string} [opts.dir] - the records directory as written, for messages
|
|
166
166
|
* @returns {{ nodes: Array, placements: Map, errors: string[], warnings: string[] }}
|
|
167
|
-
* `nodes` is the folder tree — the declared
|
|
168
|
-
* record no
|
|
169
|
-
* `{ entity, path, slug }`, `path` being `''` at the top of the folder.
|
|
167
|
+
* `nodes` is the folder tree — the declared entries in file order, then every
|
|
168
|
+
* record no entry names; `placements` maps EVERY record's id to its
|
|
169
|
+
* `{ entity, path, slug, tags?, label? }`, `path` being `''` at the top of the folder.
|
|
170
|
+
*
|
|
171
|
+
* ⭐ WHAT THE FOLDER SAYS ABOUT A RECORD — its entry's `tags` and `label` (ruled
|
|
172
|
+
* 2026-09-27 [Diego]). A record's entry is a path, or `{ path, tags?, label? }`:
|
|
173
|
+
*
|
|
174
|
+
* - folder: team
|
|
175
|
+
* records:
|
|
176
|
+
* - team/grace.md
|
|
177
|
+
* - path: team/ada.md
|
|
178
|
+
* tags: [staff, featured]
|
|
179
|
+
* label: Ada Lovelace
|
|
180
|
+
*
|
|
181
|
+
* They are the ENTRY's, not the record's — a backend keeps them on the folder entry and
|
|
182
|
+
* answers them beside the record as `$tags` and `$label`, and so does the static lane.
|
|
183
|
+
* Tags keep the order written. At the top level an entry is allowed only when it says
|
|
184
|
+
* one of them: a bare path there would claim to place a record the directory already
|
|
185
|
+
* placed.
|
|
170
186
|
*/
|
|
171
187
|
export function resolveFolder(entries, pool, { dir = RECORDS_DIR } = {}) {
|
|
172
188
|
// The file as the author sees it, for every message below.
|
|
@@ -187,7 +203,7 @@ export function resolveFolder(entries, pool, { dir = RECORDS_DIR } = {}) {
|
|
|
187
203
|
byRelPath.set(e.poolPath, e)
|
|
188
204
|
}
|
|
189
205
|
|
|
190
|
-
const place = (entity, pathSegs, where) => {
|
|
206
|
+
const place = (entity, pathSegs, where, said = null) => {
|
|
191
207
|
const key = entity.id
|
|
192
208
|
const prior = claimedBy.get(key)
|
|
193
209
|
if (prior) {
|
|
@@ -200,60 +216,94 @@ export function resolveFolder(entries, pool, { dir = RECORDS_DIR } = {}) {
|
|
|
200
216
|
claimedBy.set(key, where)
|
|
201
217
|
const slug = slugForEntity(entity)
|
|
202
218
|
const path = pathSegs.join('/')
|
|
203
|
-
placements.set(key, { entity, path, slug })
|
|
204
|
-
return { kind: 'ref', name: slug, $entityId: key }
|
|
219
|
+
placements.set(key, { entity, path, slug, ...said })
|
|
220
|
+
return { kind: 'ref', name: slug, $entityId: key, ...said }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// The records one path names — a file, or a pattern's matches — placed where the entry sits.
|
|
224
|
+
const placePath = (raw, pathSegs, where, said = null) => {
|
|
225
|
+
const pattern = typeof raw === 'string' ? raw.trim() : ''
|
|
226
|
+
if (!pattern) {
|
|
227
|
+
errors.push(`${file}: ${where} ${said ? 'has an empty `path:`' : 'is an empty string'}.`)
|
|
228
|
+
return []
|
|
229
|
+
}
|
|
230
|
+
if (!isPattern(pattern)) {
|
|
231
|
+
const hit = byRelPath.get(pattern)
|
|
232
|
+
if (!hit) {
|
|
233
|
+
errors.push(
|
|
234
|
+
`${file}: ${where} names "${pattern}", which is not in ${dir}/. ` +
|
|
235
|
+
`A path is relative to ${dir}/, extension included.`
|
|
236
|
+
)
|
|
237
|
+
return []
|
|
238
|
+
}
|
|
239
|
+
const leaf = place(hit, pathSegs, where, said)
|
|
240
|
+
return leaf ? [leaf] : []
|
|
241
|
+
}
|
|
242
|
+
// ⛔ A PATTERN MATCHING NOTHING IS AN ERROR. `artcle/*.md` is the old
|
|
243
|
+
// empty-branch defect respelled, and it produced a real, reachable, empty
|
|
244
|
+
// path with no warning.
|
|
245
|
+
const matches = [...byRelPath.entries()]
|
|
246
|
+
.filter(([rel]) => matchEntityPattern(pattern, rel))
|
|
247
|
+
.map(([, e]) => e)
|
|
248
|
+
if (matches.length === 0) {
|
|
249
|
+
errors.push(
|
|
250
|
+
`${file}: ${where} pattern "${pattern}" matches no record in ${dir}/. ` +
|
|
251
|
+
`Check the schema folder name and the extension.`
|
|
252
|
+
)
|
|
253
|
+
return []
|
|
254
|
+
}
|
|
255
|
+
// Matches sort alphanumerically by filename, numeric-aware — so `1-`, `2-`,
|
|
256
|
+
// `10-` order as written rather than as strings, and `2025-…` precedes
|
|
257
|
+
// `2026-…`. Ordering only: the number never leaves the name.
|
|
258
|
+
matches.sort((a, b) => compareByNumericPrefix(a.slug, b.slug))
|
|
259
|
+
return matches.map((e) => place(e, pathSegs, where, said)).filter(Boolean)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// An entry's `tags` and `label` — text, as YAML may type a tag or a label a number
|
|
263
|
+
// (`2024`). Tags keep the order written; an empty one is dropped. Null when one is
|
|
264
|
+
// not text (said).
|
|
265
|
+
const saidOf = (entry, where) => {
|
|
266
|
+
const said = {}
|
|
267
|
+
if (entry.tags !== undefined && entry.tags !== null) {
|
|
268
|
+
const tags = []
|
|
269
|
+
for (const tag of Array.isArray(entry.tags) ? entry.tags : [entry.tags]) {
|
|
270
|
+
if (typeof tag !== 'string' && typeof tag !== 'number') {
|
|
271
|
+
errors.push(`${file}: ${where} has a tag that is not text: ${JSON.stringify(tag)}. Write each tag as a word.`)
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
const text = String(tag).trim()
|
|
275
|
+
if (text) tags.push(text)
|
|
276
|
+
}
|
|
277
|
+
if (tags.length) said.tags = tags
|
|
278
|
+
}
|
|
279
|
+
if (entry.label !== undefined && entry.label !== null) {
|
|
280
|
+
if (typeof entry.label !== 'string' && typeof entry.label !== 'number') {
|
|
281
|
+
errors.push(`${file}: ${where} has a \`label:\` that is not text. A label is the entry's display text.`)
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
const label = String(entry.label).trim()
|
|
285
|
+
if (label) said.label = label
|
|
286
|
+
}
|
|
287
|
+
return said
|
|
205
288
|
}
|
|
206
289
|
|
|
207
290
|
const resolveEntry = (entry, pathSegs, index, trail) => {
|
|
208
291
|
const where = `entry ${trail}[${index}]`
|
|
209
292
|
|
|
210
293
|
if (typeof entry === 'string') {
|
|
211
|
-
const pattern = entry.trim()
|
|
212
294
|
// ⛔ A PATH AT THE TOP LEVEL SELECTED RECORDS until 2026-09-21 — it was how a
|
|
213
295
|
// file became one. Every file in the directory is a record now, so the line
|
|
214
296
|
// would claim to choose while choosing nothing: refused, with the reason.
|
|
215
297
|
if (pathSegs.length === 0) {
|
|
216
298
|
errors.push(
|
|
217
|
-
`${file}: ${where} ("${
|
|
218
|
-
`${dir}/ is a record already — ${file} only sorts records into folders
|
|
299
|
+
`${file}: ${where} ("${entry.trim()}") lists records at the top level. Every file in ` +
|
|
300
|
+
`${dir}/ is a record already — ${file} only sorts records into folders, and says ` +
|
|
301
|
+
`what a folder knows about a record (\`tags:\`, \`label:\`). ` +
|
|
219
302
|
`Remove the entry, or put it under a \`folder:\`.`
|
|
220
303
|
)
|
|
221
304
|
return []
|
|
222
305
|
}
|
|
223
|
-
|
|
224
|
-
errors.push(`${file}: ${where} is an empty string.`)
|
|
225
|
-
return []
|
|
226
|
-
}
|
|
227
|
-
if (!isPattern(pattern)) {
|
|
228
|
-
const hit = byRelPath.get(pattern)
|
|
229
|
-
if (!hit) {
|
|
230
|
-
errors.push(
|
|
231
|
-
`${file}: ${where} names "${pattern}", which is not in ${dir}/. ` +
|
|
232
|
-
`A path is relative to ${dir}/, extension included.`
|
|
233
|
-
)
|
|
234
|
-
return []
|
|
235
|
-
}
|
|
236
|
-
const leaf = place(hit, pathSegs, where)
|
|
237
|
-
return leaf ? [leaf] : []
|
|
238
|
-
}
|
|
239
|
-
// ⛔ A PATTERN MATCHING NOTHING IS AN ERROR. `artcle/*.md` is the old
|
|
240
|
-
// empty-branch defect respelled, and it produced a real, reachable, empty
|
|
241
|
-
// path with no warning.
|
|
242
|
-
const matches = [...byRelPath.entries()]
|
|
243
|
-
.filter(([rel]) => matchEntityPattern(pattern, rel))
|
|
244
|
-
.map(([, e]) => e)
|
|
245
|
-
if (matches.length === 0) {
|
|
246
|
-
errors.push(
|
|
247
|
-
`${file}: ${where} pattern "${pattern}" matches no record in ${dir}/. ` +
|
|
248
|
-
`Check the schema folder name and the extension.`
|
|
249
|
-
)
|
|
250
|
-
return []
|
|
251
|
-
}
|
|
252
|
-
// Matches sort alphanumerically by filename, numeric-aware — so `1-`, `2-`,
|
|
253
|
-
// `10-` order as written rather than as strings, and `2025-…` precedes
|
|
254
|
-
// `2026-…`. Ordering only: the number never leaves the name.
|
|
255
|
-
matches.sort((a, b) => compareByNumericPrefix(a.slug, b.slug))
|
|
256
|
-
return matches.map((e) => place(e, pathSegs, where)).filter(Boolean)
|
|
306
|
+
return placePath(entry, pathSegs, where)
|
|
257
307
|
}
|
|
258
308
|
|
|
259
309
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
@@ -263,6 +313,30 @@ export function resolveFolder(entries, pool, { dir = RECORDS_DIR } = {}) {
|
|
|
263
313
|
return []
|
|
264
314
|
}
|
|
265
315
|
|
|
316
|
+
// ⭐ A RECORD'S ENTRY WITH WHAT THE FOLDER SAYS ABOUT IT — `{ path, tags?, label? }`.
|
|
317
|
+
if (entry.path !== undefined) {
|
|
318
|
+
const extra = Object.keys(entry).filter((k) => !['path', 'tags', 'label'].includes(k))
|
|
319
|
+
if (extra.length) {
|
|
320
|
+
errors.push(
|
|
321
|
+
`${file}: ${where} is a record's entry (\`path:\`), which takes \`tags:\` and \`label:\` — ` +
|
|
322
|
+
`not ${extra.map((k) => `\`${k}:\``).join(', ')}.` +
|
|
323
|
+
(extra.includes('folder') || extra.includes('records') ? ' A folder is its own entry, `folder:`.' : '')
|
|
324
|
+
)
|
|
325
|
+
return []
|
|
326
|
+
}
|
|
327
|
+
const said = saidOf(entry, where)
|
|
328
|
+
if (!said) return []
|
|
329
|
+
if (pathSegs.length === 0 && !said.tags && !said.label) {
|
|
330
|
+
errors.push(
|
|
331
|
+
`${file}: ${where} ("${String(entry.path).trim()}") lists a record at the top level and says ` +
|
|
332
|
+
`nothing about it. Every file in ${dir}/ sits at the top already — give it \`tags:\` or a ` +
|
|
333
|
+
`\`label:\`, or remove the entry.`
|
|
334
|
+
)
|
|
335
|
+
return []
|
|
336
|
+
}
|
|
337
|
+
return placePath(entry.path, pathSegs, where, said)
|
|
338
|
+
}
|
|
339
|
+
|
|
266
340
|
if (entry.folder !== undefined) {
|
|
267
341
|
// ⚠️ COERCED, because YAML types a bare `2024` as a NUMBER — and a folder
|
|
268
342
|
// named for a year is the single most likely one anybody writes. Both the
|
|
@@ -298,6 +372,13 @@ export function resolveFolder(entries, pool, { dir = RECORDS_DIR } = {}) {
|
|
|
298
372
|
folderAt.set(at, where)
|
|
299
373
|
const branch = { kind: 'branch', name: segment }
|
|
300
374
|
if (entry.label !== undefined && entry.label !== null) branch.label = String(entry.label)
|
|
375
|
+
// A folder takes `tags:` as a record's entry does — kept, sent and pulled back, though
|
|
376
|
+
// no question answers a folder.
|
|
377
|
+
if (entry.tags !== undefined && entry.tags !== null) {
|
|
378
|
+
const said = saidOf({ tags: entry.tags }, where)
|
|
379
|
+
if (!said) return []
|
|
380
|
+
if (said.tags) branch.tags = said.tags
|
|
381
|
+
}
|
|
301
382
|
const kids = Array.isArray(entry.records) ? entry.records : []
|
|
302
383
|
if (kids.length === 0) {
|
|
303
384
|
warnings.push(
|
|
@@ -328,8 +409,8 @@ export function resolveFolder(entries, pool, { dir = RECORDS_DIR } = {}) {
|
|
|
328
409
|
errors.push(
|
|
329
410
|
`${file}: ${where} has no recognized kind. ` +
|
|
330
411
|
(pathSegs.length
|
|
331
|
-
? `Inside a folder, a path names records
|
|
332
|
-
: `An entry is a \`folder:\` — its \`records:\` are paths under ${dir}
|
|
412
|
+
? `Inside a folder, a path names records — or \`path:\` with its \`tags:\` and \`label:\`; anything else says \`folder:\`.`
|
|
413
|
+
: `An entry is a \`folder:\` — its \`records:\` are paths under ${dir}/ — or a record's \`path:\` with its \`tags:\` or \`label:\`.`)
|
|
333
414
|
)
|
|
334
415
|
return []
|
|
335
416
|
}
|
package/src/uwx/folder.js
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
// - `contents` is the self-nesting tree (an array), nesting via `$children` — the
|
|
8
8
|
// same mechanism site-content pages/sections use. Each node holds REFERENCES,
|
|
9
9
|
// never content:
|
|
10
|
-
// - a LEAF references one record entity: `{ kind: 'ref', name, entry }` —
|
|
10
|
+
// - a LEAF references one record entity: `{ kind: 'ref', name, entry, label?, tags? }` —
|
|
11
11
|
// `entry: { schema, entity: <uuid> }` once the record was minted (back-filled
|
|
12
12
|
// into its file), or `entry: { $ref: "<$id>" }` while brand-new (resolved
|
|
13
|
-
// within this payload)
|
|
14
|
-
//
|
|
13
|
+
// within this payload); `label` (a localized map) and `tags` are what
|
|
14
|
+
// `records/folder.yml` says about the record, since 2026-09-27.
|
|
15
|
+
// - a BRANCH is a sub-folder: `{ kind: 'branch', name, label?, tags?, $children }`.
|
|
15
16
|
//
|
|
16
17
|
// ⭐ A LEAF'S `name` IS ITS RECORD'S HANDLE — the slug, the records service's `$name` —
|
|
17
18
|
// and a branch's `name` is the segment a query's `scope:` names. `label` is a branch's
|
|
@@ -99,6 +100,7 @@ function contentsFromNodes(nodes, byEntityId, missing, sourceLocale) {
|
|
|
99
100
|
// map, like every localized scalar this producer sends — keyed by the
|
|
100
101
|
// site's source locale.
|
|
101
102
|
if (node.label !== undefined) branch.label = { [sourceLocale]: String(node.label) }
|
|
103
|
+
if (Array.isArray(node.tags) && node.tags.length) branch.tags = [...node.tags]
|
|
102
104
|
branch.$children = contentsFromNodes(node.$children, byEntityId, missing, sourceLocale)
|
|
103
105
|
out.push(branch)
|
|
104
106
|
continue
|
|
@@ -108,7 +110,13 @@ function contentsFromNodes(nodes, byEntityId, missing, sourceLocale) {
|
|
|
108
110
|
missing.push(node.$entityId)
|
|
109
111
|
continue
|
|
110
112
|
}
|
|
111
|
-
|
|
113
|
+
const leaf = refLeaf(entity)
|
|
114
|
+
// ⭐ What the folder says about the record (`records/folder.yml`, ruled 2026-09-27
|
|
115
|
+
// [Diego]): its display text, localized as a branch's is, and its tags, in the order
|
|
116
|
+
// written. A records service answers them beside the record as `$label` and `$tags`.
|
|
117
|
+
if (node.label !== undefined) leaf.label = { [sourceLocale]: String(node.label) }
|
|
118
|
+
if (Array.isArray(node.tags) && node.tags.length) leaf.tags = [...node.tags]
|
|
119
|
+
out.push(leaf)
|
|
112
120
|
}
|
|
113
121
|
return out
|
|
114
122
|
}
|
|
@@ -493,7 +493,9 @@ export function declarationsToQueriesYml({ document, siteRoot, scope, ...rest })
|
|
|
493
493
|
* there — so `folder.yml` carries the branches and the records placed in them, and
|
|
494
494
|
* a folder with no branches is a site with no `folder.yml`: a local one is REMOVED
|
|
495
495
|
* (it could only describe sub-folders the backend's folder no longer has). No state
|
|
496
|
-
* of the file removes a record, so this cannot empty anything.
|
|
496
|
+
* of the file removes a record, so this cannot empty anything. ⭐ The one exception,
|
|
497
|
+
* since 2026-09-27: a record whose entry has `tags` or a `label` is written wherever it
|
|
498
|
+
* sits, as `{ path, tags?, label? }`, since only `folder.yml` can say them.
|
|
497
499
|
*
|
|
498
500
|
* ⛔ A PULL THAT CARRIED NO FOLDER, or one with a placed record that was not written
|
|
499
501
|
* locally, leaves the file alone: it has nothing true to write.
|
|
@@ -524,22 +526,34 @@ export function folderToFolderYml({ folderDoc, siteRoot, poolPathByUuid, sourceL
|
|
|
524
526
|
// does not caption its rows. On the wire the label is a localized map;
|
|
525
527
|
// folder.yml carries the source-locale string (a bare string passes).
|
|
526
528
|
if (node.label !== undefined) entry.label = unwrapLocalized(node.label, sourceLocale)
|
|
529
|
+
if (Array.isArray(node.tags) && node.tags.length) entry.tags = node.tags.map(String)
|
|
527
530
|
entry.records = walk(node.$children, true)
|
|
528
531
|
out.push(entry)
|
|
529
532
|
continue
|
|
530
533
|
}
|
|
531
534
|
const uuid = node.entry?.entity ?? node.entry
|
|
532
535
|
const rel = typeof uuid === 'string' ? poolPathByUuid.get(uuid) : null
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
+
// ⭐ What the folder says about the record — its entry's `tags` and `label` — comes
|
|
537
|
+
// back as the entry `{ path, tags?, label? }` (ruled 2026-09-27 [Diego]).
|
|
538
|
+
const said = {}
|
|
539
|
+
if (Array.isArray(node.tags) && node.tags.length) said.tags = node.tags.map(String)
|
|
540
|
+
if (node.label !== undefined && node.label !== null) {
|
|
541
|
+
const label = unwrapLocalized(node.label, sourceLocale)
|
|
542
|
+
if (typeof label === 'string' && label) said.label = label
|
|
543
|
+
}
|
|
544
|
+
const says = Object.keys(said).length > 0
|
|
545
|
+
// A record at the top of the folder needs no line unless the folder says something
|
|
546
|
+
// about it — but one that did not land is still said: the next push sends the folder
|
|
547
|
+
// the directory holds, which would not have it.
|
|
536
548
|
if (!inBranch) {
|
|
537
549
|
if (!rel) {
|
|
538
550
|
warnings.push(
|
|
539
551
|
`the folder holds a record ("${node.name ?? '?'}") that was not written locally — ` +
|
|
540
552
|
`a push from here would remove it from the folder.`
|
|
541
553
|
)
|
|
554
|
+
continue
|
|
542
555
|
}
|
|
556
|
+
if (says && !out.some((e) => e?.path === rel)) out.push({ path: rel, ...said })
|
|
543
557
|
continue
|
|
544
558
|
}
|
|
545
559
|
if (!rel) {
|
|
@@ -554,7 +568,8 @@ export function folderToFolderYml({ folderDoc, siteRoot, poolPathByUuid, sourceL
|
|
|
554
568
|
continue
|
|
555
569
|
}
|
|
556
570
|
// A list file holds several records and is one path — listed once.
|
|
557
|
-
if (
|
|
571
|
+
if (out.some((e) => e === rel || e?.path === rel)) continue
|
|
572
|
+
out.push(says ? { path: rel, ...said } : rel)
|
|
558
573
|
}
|
|
559
574
|
return out
|
|
560
575
|
}
|
package/src/uwx/records.js
CHANGED
|
@@ -729,8 +729,13 @@ function placeProducedRecords(nodes, producedBy) {
|
|
|
729
729
|
out.push({ ...node, $children: placeProducedRecords(node.$children, producedBy) })
|
|
730
730
|
continue
|
|
731
731
|
}
|
|
732
|
+
// What the folder says about the record — its entry's `tags` and `label` — goes with
|
|
733
|
+
// every record the file produced.
|
|
734
|
+
const said = {}
|
|
735
|
+
if (node?.tags) said.tags = node.tags
|
|
736
|
+
if (node?.label !== undefined) said.label = node.label
|
|
732
737
|
for (const r of producedBy.get(node?.$entityId) || []) {
|
|
733
|
-
out.push({ kind: 'ref', name: r.slug, $entityId: r.id })
|
|
738
|
+
out.push({ kind: 'ref', name: r.slug, $entityId: r.id, ...said })
|
|
734
739
|
}
|
|
735
740
|
}
|
|
736
741
|
return out
|