@uniweb/build 0.61.0 → 0.62.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 +6 -6
- package/src/i18n/freeform.js +3 -1
- package/src/i18n/records.js +161 -80
- package/src/resolve-data-schema.js +11 -1
- package/src/runtime-schema.js +14 -16
- package/src/site/queries-config.js +68 -13
- package/src/site/query-processor.js +310 -67
- package/src/uwx/backfill.js +115 -52
- package/src/uwx/data-schema.js +16 -11
- package/src/uwx/project-writer.js +4 -2
- package/src/uwx/record-layout.js +61 -0
- package/src/uwx/records-project.js +23 -18
- package/src/uwx/records.js +370 -201
- package/src/uwx/registry-package.js +3 -3
- package/src/uwx/sync-package.js +66 -4
- package/src/uwx/uuid.js +7 -0
- package/src/validate-data.js +57 -14
package/src/uwx/backfill.js
CHANGED
|
@@ -32,7 +32,8 @@ import yaml from 'js-yaml'
|
|
|
32
32
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
33
33
|
import { proseMirrorToMarkdown } from '@uniweb/content-writer'
|
|
34
34
|
import { parseFrontmatter } from './entity-source.js'
|
|
35
|
-
import { isProseMirrorField
|
|
35
|
+
import { isProseMirrorField } from './data-schema.js'
|
|
36
|
+
import { recordFileLayout, contentBodyTarget } from './record-layout.js'
|
|
36
37
|
import { unwrapLocalizedContent } from './locale-sync.js'
|
|
37
38
|
import { parseBibtex, exportBibtex } from '@citestyle/bibtex'
|
|
38
39
|
|
|
@@ -213,28 +214,32 @@ export function unwrapLocalizedList(value, sourceLocale) {
|
|
|
213
214
|
return unwrapLocalized(value, sourceLocale)
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
// (the sections-tree has no schema-level `brief:` back-reference). Returned with
|
|
218
|
-
// its name attached (the map key) so callers can key the document by it.
|
|
219
|
-
function briefSectionOf(declaration) {
|
|
220
|
-
const entry = Object.entries(declaration?.sections || {}).find(([, s]) => s && s.brief === true)
|
|
221
|
-
return entry ? { name: entry[0], ...entry[1] } : null
|
|
222
|
-
}
|
|
217
|
+
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
223
218
|
|
|
224
219
|
/**
|
|
225
220
|
* Render an entity `document` a pull brought back to its source-file authoring shape
|
|
226
221
|
* — the file becomes a projection of the backend's state, which is what a pull is
|
|
227
|
-
* (`writeRecordFile`, `records-project.js`). A push never uses it (header ⛔).
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
222
|
+
* (`writeRecordFile`, `records-project.js`). A push never uses it (header ⛔).
|
|
223
|
+
*
|
|
224
|
+
* The file takes its schema's layout (`./record-layout.js`), the one the push reads:
|
|
225
|
+
* FLAT for a schema with one top-level section holding one record — that section's
|
|
226
|
+
* fields at the top — and BY SECTION otherwise, every section the document holds under
|
|
227
|
+
* its own name: an object for a single section, a list of records for a `many` one, a
|
|
228
|
+
* self-nesting record's `$children` written back as `children:`. Values are decoded by
|
|
229
|
+
* their declaration at every depth (localized unwrapped to the source locale, a
|
|
230
|
+
* ProseMirror doc to markdown, dates and scalars verbatim). Every record's own `$uuid`
|
|
231
|
+
* is dropped — the backend matches a single section's record by singularity — and
|
|
232
|
+
* `$schema`/`$id`/`$meta` are omitted; the entity's `$uuid` leads. A disabled entity
|
|
233
|
+
* (`$disabled: true`) writes `draft: true`, and an enabled one writes no `draft:`.
|
|
233
234
|
* For markdown, the content body field becomes the body; for YAML/JSON it stays a field.
|
|
234
235
|
*
|
|
236
|
+
* ⛔ Until 2026-09-24 this wrote the BRIEF section alone, flat. Every other section was
|
|
237
|
+
* dropped from the file — a pulled `@std/article` markdown record came back with an
|
|
238
|
+
* empty body, its `article_body` gone — and nothing said so.
|
|
239
|
+
*
|
|
235
240
|
* @param {object} params
|
|
236
|
-
* @param {object} params.document - pulled `{ $uuid, $schema, <
|
|
237
|
-
* @param {object} params.declaration - the
|
|
241
|
+
* @param {object} params.document - a pulled `{ $uuid, $schema, <section>: … }`
|
|
242
|
+
* @param {object} params.declaration - the data schema's declaration (`sections`)
|
|
238
243
|
* @param {string} params.format - 'yaml' | 'json' | 'md'
|
|
239
244
|
* @param {string} [params.sourceLocale]
|
|
240
245
|
* @param {object} [params.collector] - translation collector; target locales of
|
|
@@ -244,58 +249,116 @@ function briefSectionOf(declaration) {
|
|
|
244
249
|
* @param {string} [params.freeformRelPath] - the free-form path for this record's
|
|
245
250
|
* content body (buildFreeformRecordPath); lets a target-locale full-doc
|
|
246
251
|
* body be written under locales/freeform/{locale}/ instead of being dropped.
|
|
252
|
+
* @param {(model: string, uuid: string) => string|null} [params.refName] - the name a
|
|
253
|
+
* referenced record goes by in the project, for a reference's uuid; a reference
|
|
254
|
+
* it has no name for is written as the uuid.
|
|
247
255
|
* @returns {string} the source-file text
|
|
248
256
|
*/
|
|
249
|
-
export function renderEntityDocument({ document, declaration, format, sourceLocale = 'en', collector, freeformRelPath }) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
export function renderEntityDocument({ document, declaration, format, sourceLocale = 'en', collector, freeformRelPath, refName }) {
|
|
258
|
+
const layout = recordFileLayout(declaration)
|
|
259
|
+
if (layout.sections.length === 0) {
|
|
260
|
+
throw new Error('uwx/render: the data schema declares no sections')
|
|
261
|
+
}
|
|
262
|
+
// ⛔ An entity holding NONE of its schema's sections — what a partly applied push
|
|
263
|
+
// left, before the backend's push became one transaction — is not written: rendered,
|
|
264
|
+
// it would replace the author's record with an empty one. The caller skips it.
|
|
265
|
+
if (!layout.sections.some(([name]) => document?.[name] != null)) {
|
|
266
|
+
throw new Error("uwx/render: the entity holds none of its data schema's sections — nothing to write")
|
|
254
267
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const contentKey = Object.entries(fields).find(([, f]) => isContentBodyField(f))?.[0]
|
|
268
|
+
// Markdown: the content body field, wherever its single section is, becomes the body.
|
|
269
|
+
const { target } = contentBodyTarget(declaration)
|
|
270
|
+
const dec = { sourceLocale, collector, freeformRelPath, refName, bodyAt: format === 'md' ? target : null, body: '' }
|
|
259
271
|
|
|
260
272
|
const record = {}
|
|
261
|
-
if (document
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Capture target-locale translations of localized SCALAR fields (not the
|
|
279
|
-
// content body) into the collector when one is supplied.
|
|
280
|
-
if (field.localized && key !== contentKey) collector?.add(raw)
|
|
281
|
-
const value = field.localized ? unwrapLocalized(raw, sourceLocale) : raw
|
|
282
|
-
if (format === 'md' && key === contentKey) {
|
|
283
|
-
body = typeof value === 'string' ? value : String(value ?? '')
|
|
284
|
-
continue
|
|
273
|
+
if (document?.$uuid) record.$uuid = document.$uuid
|
|
274
|
+
if (layout.flat) {
|
|
275
|
+
const [name, def] = layout.sections[0]
|
|
276
|
+
Object.assign(record, decodeRecord(def.fields, document?.[name], dec, name))
|
|
277
|
+
} else {
|
|
278
|
+
for (const [name, def] of layout.sections) {
|
|
279
|
+
const value = document?.[name]
|
|
280
|
+
if (value == null) continue
|
|
281
|
+
if (def.multiple === true) {
|
|
282
|
+
const list = decodeList(def, value, dec)
|
|
283
|
+
if (list.length) record[name] = list
|
|
284
|
+
} else {
|
|
285
|
+
const data = decodeRecord(def.fields, value, dec, name)
|
|
286
|
+
if (Object.keys(data).length) record[name] = data
|
|
287
|
+
}
|
|
285
288
|
}
|
|
286
|
-
record[key] = value
|
|
287
289
|
}
|
|
288
290
|
// ⭐ A DISABLED ENTITY IS A DRAFT ON THE FILE SIDE. It is kept in the folder and never
|
|
289
291
|
// publicly delivered, which is exactly what `draft: true` means. An enabled entity
|
|
290
292
|
// carries no key and writes no `draft:`, so a record re-enabled on the backend
|
|
291
293
|
// comes back without the line.
|
|
292
|
-
if (document
|
|
294
|
+
if (document?.$disabled === true) record.draft = true
|
|
293
295
|
|
|
294
296
|
if (format === 'json') return JSON.stringify(record, null, 2) + '\n'
|
|
295
|
-
if (format === 'md') return `---\n${yaml.dump(record)}---\n${body}`
|
|
297
|
+
if (format === 'md') return `---\n${yaml.dump(record)}---\n${dec.body}`
|
|
296
298
|
return yaml.dump(record) // yaml / yaml
|
|
297
299
|
}
|
|
298
300
|
|
|
301
|
+
// One record of a section, its declared fields in declared order. `sectionName` is the
|
|
302
|
+
// top-level section the record belongs to, so the content body can be found; null below.
|
|
303
|
+
function decodeRecord(fields, value, dec, sectionName) {
|
|
304
|
+
const out = {}
|
|
305
|
+
if (!isPlainObject(value)) return out
|
|
306
|
+
for (const [key, field] of Object.entries(fields || {})) {
|
|
307
|
+
const raw = value[key]
|
|
308
|
+
if (raw === undefined) continue
|
|
309
|
+
if (field?.type === 'section') {
|
|
310
|
+
out[key] = field.multiple === true ? decodeList(field, raw, dec) : decodeRecord(field.fields, raw, dec, null)
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
const isBody = Boolean(dec.bodyAt) && sectionName === dec.bodyAt.section && key === dec.bodyAt.key
|
|
314
|
+
const decoded = decodeLeaf(raw, field, dec, isBody)
|
|
315
|
+
if (isBody) dec.body = typeof decoded === 'string' ? decoded : String(decoded ?? '')
|
|
316
|
+
else out[key] = decoded
|
|
317
|
+
}
|
|
318
|
+
return out
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The records of a `many` section; a self-nesting one's `$children` nest back under
|
|
322
|
+
// `children:`, the reserved key the push reads them from.
|
|
323
|
+
function decodeList(def, value, dec) {
|
|
324
|
+
if (!Array.isArray(value)) return []
|
|
325
|
+
return value.map((item) => {
|
|
326
|
+
const out = decodeRecord(def.fields, item, dec, null)
|
|
327
|
+
if (def.self_nesting === true && Array.isArray(item?.$children) && item.$children.length) {
|
|
328
|
+
out.children = decodeList(def, item.$children, dec)
|
|
329
|
+
}
|
|
330
|
+
return out
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// A leaf's authored value. A localized SCALAR's target locales are captured into the
|
|
335
|
+
// collector (the content body's are captured by `unwrapLocalizedContent`, with its
|
|
336
|
+
// free-form path); a list of localized values is unwrapped element by element.
|
|
337
|
+
function decodeLeaf(raw, field, dec, isBody) {
|
|
338
|
+
if (field?.type === 'entity_ref') return decodeReference(raw, field, dec)
|
|
339
|
+
if (isProseMirrorField(field)) {
|
|
340
|
+
const sourceDoc = field.localized
|
|
341
|
+
? unwrapLocalizedContent(raw, dec.sourceLocale, dec.collector, isBody ? dec.freeformRelPath : undefined)
|
|
342
|
+
: raw
|
|
343
|
+
return sourceDoc ? proseMirrorToMarkdown(sourceDoc) : ''
|
|
344
|
+
}
|
|
345
|
+
if (!field?.localized) return raw
|
|
346
|
+
if (Array.isArray(raw)) {
|
|
347
|
+
if (!isBody) for (const item of raw) dec.collector?.add(item)
|
|
348
|
+
return unwrapLocalizedList(raw, dec.sourceLocale)
|
|
349
|
+
}
|
|
350
|
+
if (!isBody) dec.collector?.add(raw)
|
|
351
|
+
return unwrapLocalized(raw, dec.sourceLocale)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// A reference comes back as the backend's uuid; the file names its record by handle — the
|
|
355
|
+
// name the site's folder gives it (`refName`), which is what a push resolves back to that
|
|
356
|
+
// uuid. A record the folder does not hold keeps its uuid, and a push sends it as it is.
|
|
357
|
+
function decodeReference(raw, field, dec) {
|
|
358
|
+
const one = (v) => (typeof v === 'string' && dec.refName ? dec.refName(field.model, v) ?? v : v)
|
|
359
|
+
return Array.isArray(raw) ? raw.map(one) : one(raw)
|
|
360
|
+
}
|
|
361
|
+
|
|
299
362
|
/**
|
|
300
363
|
* Back-fill the sync response into the source files. Correlation is by **`index`**
|
|
301
364
|
* — `finalized[i].index` is the 0-based position of the entity in the submitted
|
package/src/uwx/data-schema.js
CHANGED
|
@@ -91,7 +91,7 @@ export function toDataSchemaDeclaration(normalized, { name, resolveName, resolve
|
|
|
91
91
|
|
|
92
92
|
const { sections, brief } = normalized.sections
|
|
93
93
|
? lowerSectionsForm(normalized.sections, resolve, optResolve)
|
|
94
|
-
: lowerFieldsForm(normalized.fields || {},
|
|
94
|
+
: lowerFieldsForm(normalized.fields || {}, resolve, optResolve)
|
|
95
95
|
|
|
96
96
|
// The model-level sort axis is inline: `sort_date: true` on the brief's named
|
|
97
97
|
// date field (replaces the old schema-level `sort_date_field` back-reference).
|
|
@@ -139,13 +139,22 @@ export function toDataSchemaDeclaration(normalized, { name, resolveName, resolve
|
|
|
139
139
|
|
|
140
140
|
// --- shapes ------------------------------------------------------------------
|
|
141
141
|
|
|
142
|
-
// The `fields:` shorthand: a flat field set is ONE single
|
|
143
|
-
//
|
|
144
|
-
// `sections:`; this expands to it.
|
|
145
|
-
|
|
142
|
+
// The `fields:` shorthand: a flat field set is ONE single section, named `brief` and
|
|
143
|
+
// marked `brief: true` — exactly `sections: { brief: { brief: true, fields } }`. Our
|
|
144
|
+
// producer-side sugar — the registry's root is always `sections:`; this expands to it.
|
|
145
|
+
//
|
|
146
|
+
// ⭐ THE NAME IS FIXED, NOT DERIVED (2026-09-22 [Diego]: "We assume it is single section,
|
|
147
|
+
// `brief: true` and named `brief`"). It was the schema's short name until then —
|
|
148
|
+
// `@std/person` → `person` — a name nobody wrote, stored on every entity and in every
|
|
149
|
+
// `item_ref` path, that a schema growing into the `sections:` form had to reuse exactly
|
|
150
|
+
// with nothing to say so. A `sections:` schema still names its own sections; `brief` is
|
|
151
|
+
// only the recommended name for its card.
|
|
152
|
+
export const SHORTHAND_SECTION = 'brief'
|
|
153
|
+
|
|
154
|
+
function lowerFieldsForm(fields, resolve, optResolve) {
|
|
146
155
|
return {
|
|
147
|
-
sections: { [
|
|
148
|
-
brief:
|
|
156
|
+
sections: { [SHORTHAND_SECTION]: lowerSection({ kind: 'single', brief: true, fields }, resolve, optResolve) },
|
|
157
|
+
brief: SHORTHAND_SECTION,
|
|
149
158
|
}
|
|
150
159
|
}
|
|
151
160
|
|
|
@@ -460,10 +469,6 @@ function sectionAttrsFromField(field) {
|
|
|
460
469
|
return out
|
|
461
470
|
}
|
|
462
471
|
|
|
463
|
-
function shortName(name) {
|
|
464
|
-
return String(name).split('/').pop()
|
|
465
|
-
}
|
|
466
|
-
|
|
467
472
|
// `@/x` resolves into the schema's own org; other scopes pass through.
|
|
468
473
|
function defaultResolver(ownName) {
|
|
469
474
|
const slash = ownName.indexOf('/')
|
|
@@ -380,9 +380,11 @@ export function writeThemeFile(siteRoot, theme) {
|
|
|
380
380
|
* @param {object} [opts.collector] - translation collector for localized scalar fields
|
|
381
381
|
* @param {string} [opts.freeformRelPath] - free-form path for this record's content
|
|
382
382
|
* body (so a target-locale full-doc body is captured for locales/freeform/)
|
|
383
|
+
* @param {(model: string, uuid: string) => string|null} [opts.refName] - the name a
|
|
384
|
+
* referenced record goes by, so a reference is written as that name
|
|
383
385
|
* @returns {'updated'|'unchanged'}
|
|
384
386
|
*/
|
|
385
|
-
export function writeRecordFile({ filePath, document, declaration, format, sourceLocale = 'en', collector, freeformRelPath }) {
|
|
386
|
-
const text = renderEntityDocument({ document, declaration, format, sourceLocale, collector, freeformRelPath })
|
|
387
|
+
export function writeRecordFile({ filePath, document, declaration, format, sourceLocale = 'en', collector, freeformRelPath, refName }) {
|
|
388
|
+
const text = renderEntityDocument({ document, declaration, format, sourceLocale, collector, freeformRelPath, refName })
|
|
387
389
|
return writeIfChanged(filePath, text)
|
|
388
390
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// How one record of a data schema is written in its file — the layout the push reads
|
|
2
|
+
// and the pull writes, read off the schema's declaration (`toDataSchemaDeclaration`).
|
|
3
|
+
//
|
|
4
|
+
// ⭐ FLAT ONLY WHEN THE SCHEMA HAS ONE TOP-LEVEL SECTION HOLDING ONE RECORD — the
|
|
5
|
+
// `fields:` shorthand, or a `sections:` schema whose only section is a single one. The
|
|
6
|
+
// file's keys are then that section's fields, and nothing is lost either way.
|
|
7
|
+
//
|
|
8
|
+
// ⭐ EVERY OTHER SCHEMA IS WRITTEN BY SECTION: each top-level section under its own
|
|
9
|
+
// name — an object for a single section, a list of records for a `many` one, an object
|
|
10
|
+
// of child sections for a binder. That is the stored entity's own shape
|
|
11
|
+
// (docs/reference/entity-content.md), so a push sends it and a pull writes it back
|
|
12
|
+
// without a mapping in between. A schema whose only section is `many` (`@std/nav`) is
|
|
13
|
+
// written this way too — `items: [...]` — since a file whose top level is a list holds
|
|
14
|
+
// several records, not one.
|
|
15
|
+
//
|
|
16
|
+
// ⛔ THE FLAT FORM FOR A MULTI-SECTION SCHEMA IS RETIRED (2026-09-22 [Diego]: "we can
|
|
17
|
+
// discontinue the shorthands for multi-section Models"). It matched a file's keys to
|
|
18
|
+
// fields across sections by name, and sections are namespaces: a field name two
|
|
19
|
+
// sections share got one value in both, a `many` section had no place at all, and a
|
|
20
|
+
// pull — writing back only what it could place — returned a record with nothing but its
|
|
21
|
+
// brief. Measured before the change: a pulled `@std/article` markdown record came back
|
|
22
|
+
// with an empty body.
|
|
23
|
+
|
|
24
|
+
import { isContentBodyField } from './data-schema.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} declaration - `{ name, sections }`
|
|
28
|
+
* @returns {{ flat: boolean, sections: Array<[string, object]>, brief: string|null }}
|
|
29
|
+
* `sections` — the top-level sections in declared order; `brief` — the section the
|
|
30
|
+
* declaration marks `brief: true`, or null when it marks none.
|
|
31
|
+
*/
|
|
32
|
+
export function recordFileLayout(declaration) {
|
|
33
|
+
const sections = Object.entries(declaration?.sections || {}).filter(([, s]) => s && typeof s === 'object')
|
|
34
|
+
const flat = sections.length === 1 && sections[0][1].multiple !== true
|
|
35
|
+
const brief = sections.find(([, s]) => s.brief === true)?.[0] ?? null
|
|
36
|
+
return { flat, sections, brief }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The schema's CONTENT body field — the one a `.md` record's markdown body fills: a
|
|
41
|
+
* markup `text` field (`format: markdown|html`) or a `format: prosemirror` json field,
|
|
42
|
+
* declared directly on a top-level single section — the brief or another, like
|
|
43
|
+
* `@std/article`'s `article_body.content`. The first in declared order.
|
|
44
|
+
*
|
|
45
|
+
* ⛔ Not the brief's alone. Until 2026-09-24 the pull and the per-locale body override
|
|
46
|
+
* looked only in the brief, while the push looked in every single section, so a body
|
|
47
|
+
* declared outside the brief was pushed and then neither translated nor pulled back.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} declaration
|
|
50
|
+
* @returns {{ target: { section: string, key: string, field: object }|null, count: number }}
|
|
51
|
+
*/
|
|
52
|
+
export function contentBodyTarget(declaration) {
|
|
53
|
+
const matches = []
|
|
54
|
+
for (const [section, def] of recordFileLayout(declaration).sections) {
|
|
55
|
+
if (def.multiple === true) continue
|
|
56
|
+
for (const [key, field] of Object.entries(def.fields || {})) {
|
|
57
|
+
if (isContentBodyField(field)) matches.push({ section, key, field })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { target: matches[0] || null, count: matches.length }
|
|
61
|
+
}
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
// fact about it.
|
|
19
19
|
// - an existing local file carrying the same `$uuid` is re-rendered in place;
|
|
20
20
|
// otherwise a new single-record file is placed at `<slug>.<ext>`, its format
|
|
21
|
-
// matched to the schema folder's existing files, else markdown when the
|
|
22
|
-
//
|
|
21
|
+
// matched to the schema folder's existing files, else markdown when the schema
|
|
22
|
+
// has a content body field, else YAML.
|
|
23
23
|
//
|
|
24
|
-
// Field rendering reuses renderEntityDocument (via writeRecordFile)
|
|
25
|
-
//
|
|
24
|
+
// Field rendering reuses renderEntityDocument (via writeRecordFile), which writes the
|
|
25
|
+
// record in its schema's layout — flat, or by section — with localized unwrap, date
|
|
26
|
+
// handling and content-body→body inverted. Asset paths
|
|
26
27
|
// are restored before it runs (`restoreAssetRefs`), as the content lane does.
|
|
27
28
|
//
|
|
28
29
|
// Deferred: array-form & BibTeX multi-record files (a pulled record is placed as
|
|
@@ -43,7 +44,7 @@ import { writeRecordFile, writeQueriesConfig, writeRecordsConfig } from './proje
|
|
|
43
44
|
import { defaultSchema, deferredFromSchema, foundationDataSchemas } from './queries-config.js'
|
|
44
45
|
import { poolDirsForSchema, resolveRecordsDir } from '../site/entity-pool.js'
|
|
45
46
|
import { folderYmlPath } from '../site/records-config.js'
|
|
46
|
-
import {
|
|
47
|
+
import { contentBodyTarget } from './record-layout.js'
|
|
47
48
|
import { unresolveSelfScope, refuseOrgOption } from './self-scope.js'
|
|
48
49
|
import { parseCatalogRef } from '../site/foundation-ref.js'
|
|
49
50
|
import { unwrapLocalized } from './backfill.js'
|
|
@@ -96,8 +97,8 @@ export function findRecordFileByUuid(poolDir, uuid) {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
// The format to give a NEW record file in a collection: match the collection's
|
|
99
|
-
// existing single-record files, else markdown when the
|
|
100
|
-
//
|
|
100
|
+
// existing single-record files, else markdown when the schema has a content body field
|
|
101
|
+
// (so the body has a home), else YAML.
|
|
101
102
|
function defaultFormat(poolDir, declaration) {
|
|
102
103
|
if (existsSync(poolDir)) {
|
|
103
104
|
for (const entry of readdirSync(poolDir)) {
|
|
@@ -106,14 +107,9 @@ function defaultFormat(poolDir, declaration) {
|
|
|
106
107
|
if (format) return format
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Whether the declaration's brief section declares a content body field — a markup
|
|
113
|
-
// `text` field or a `format: prosemirror` json field (the md-body target).
|
|
114
|
-
function briefHasContentBody(declaration) {
|
|
115
|
-
const brief = Object.values(declaration?.sections || {}).find((s) => s && s.brief === true)
|
|
116
|
-
return Object.values(brief?.fields || {}).some((f) => isContentBodyField(f))
|
|
110
|
+
// ⛔ This asked the brief alone until 2026-09-24, so a pulled `@std/article` — its
|
|
111
|
+
// body in `article_body` — was placed as YAML, the body a field.
|
|
112
|
+
return contentBodyTarget(declaration).target ? 'md' : 'yaml'
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
// Build `uuid → { collection, slug }` from the folder document's ref leaves. The
|
|
@@ -137,7 +133,8 @@ function indexFolder(folderDoc) {
|
|
|
137
133
|
} else if (node?.kind === 'ref' && node.entry) {
|
|
138
134
|
// `entry` is `{ schema, entity: <uuid> }`; tolerate a bare uuid defensively.
|
|
139
135
|
const uuid = typeof node.entry === 'object' ? node.entry.entity : node.entry
|
|
140
|
-
|
|
136
|
+
const schema = typeof node.entry === 'object' ? node.entry.schema : undefined
|
|
137
|
+
if (uuid) byUuid.set(uuid, { folderPath, slug: node.name, schema })
|
|
141
138
|
}
|
|
142
139
|
}
|
|
143
140
|
}
|
|
@@ -223,7 +220,7 @@ const DECL_WIRE_CONSUMED = new Set([
|
|
|
223
220
|
])
|
|
224
221
|
|
|
225
222
|
// Is this wire `deferred` exactly what the schema's brief would have derived? Compared
|
|
226
|
-
// as an ORDER-INSENSITIVE set: the deriver walks
|
|
223
|
+
// as an ORDER-INSENSITIVE set: the deriver walks the schema's sections, and a round trip
|
|
227
224
|
// through YAML and the store is not obliged to preserve that order. Comparing as a list
|
|
228
225
|
// would classify a reordered-but-identical value as authored, and persist it.
|
|
229
226
|
function isDerivedDeferred(d, dataSchemas) {
|
|
@@ -485,6 +482,14 @@ export function recordsToProject({ folderDoc, recordDocs = [], siteRoot, opts =
|
|
|
485
482
|
}
|
|
486
483
|
|
|
487
484
|
const folderIndex = indexFolder(folderDoc)
|
|
485
|
+
// ⭐ A REFERENCE IS WRITTEN AS THE NAME OF THE RECORD IT POINTS AT — `speaker: ada` —
|
|
486
|
+
// which is what a push resolves back to this backend's uuid. The folder names every
|
|
487
|
+
// record of the site; a reference to a record it does not hold (or holds under another
|
|
488
|
+
// Model) is written as the uuid.
|
|
489
|
+
const refName = (model, uuid) => {
|
|
490
|
+
const hit = folderIndex.get(uuid)
|
|
491
|
+
return hit?.slug && (!hit.schema || hit.schema === model) ? hit.slug : null
|
|
492
|
+
}
|
|
488
493
|
// Where records land — `site.yml::paths.records`, else `records/`: the one resolver
|
|
489
494
|
// the build and the push read too, so a pulled record lands where the next build
|
|
490
495
|
// looks. ⛔ This wrote into the default directory until 2026-09-21, whatever the
|
|
@@ -570,7 +575,7 @@ export function recordsToProject({ folderDoc, recordDocs = [], siteRoot, opts =
|
|
|
570
575
|
const toWrite = own && own !== theirs ? { ...document, $uuid: own } : document
|
|
571
576
|
let status
|
|
572
577
|
try {
|
|
573
|
-
status = writeRecordFile({ filePath, document: toWrite, declaration, format, sourceLocale, collector, freeformRelPath })
|
|
578
|
+
status = writeRecordFile({ filePath, document: toWrite, declaration, format, sourceLocale, collector, freeformRelPath, refName })
|
|
574
579
|
} catch (err) {
|
|
575
580
|
// ⛔ A record that could not be written was not placed, so it is a SKIP, not a
|
|
576
581
|
// warning: a caller that counts what it placed (the CLI's pull) must see it.
|