@uniweb/build 0.64.0 → 0.64.2
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/i18n/extract.js +19 -0
- package/src/i18n/merge.js +15 -1
- package/src/i18n/records.js +5 -1
- package/src/import-map-plugin.js +13 -0
- package/src/runtime-schema.js +3 -1
- package/src/site/config.js +59 -5
- package/src/site/foundation-ref.js +21 -2
- package/src/site/index.js +2 -0
- package/src/site/preview-media.js +90 -0
- package/src/site/preview.js +84 -0
- package/src/site/queries-config.js +8 -1
- package/src/site/query-processor.js +6 -4
- package/src/site/registered-foundation.js +48 -0
- package/src/uwx/backfill.js +17 -6
- package/src/uwx/data-schema.js +34 -0
- package/src/uwx/emit-surface.json +6 -0
- package/src/uwx/index.js +9 -0
- package/src/uwx/open-graph.js +69 -0
- package/src/uwx/project-writer.js +6 -3
- package/src/uwx/records-project.js +50 -9
- package/src/uwx/records.js +48 -2
- package/src/uwx/site-project.js +13 -2
- package/src/uwx/site.js +36 -13
- package/src/uwx/sync-package.js +24 -1
- package/src/validate-data.js +15 -5
package/src/uwx/backfill.js
CHANGED
|
@@ -32,7 +32,7 @@ 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 } from './data-schema.js'
|
|
35
|
+
import { isProseMirrorField, isOpenMapSection, openMapFromRows } from './data-schema.js'
|
|
36
36
|
import { recordFileLayout, contentBodyTarget } from './record-layout.js'
|
|
37
37
|
import { unwrapLocalizedContent } from './locale-sync.js'
|
|
38
38
|
import { parseBibtex, exportBibtex } from '@citestyle/bibtex'
|
|
@@ -307,7 +307,7 @@ function decodeRecord(fields, value, dec, sectionName) {
|
|
|
307
307
|
const raw = value[key]
|
|
308
308
|
if (raw === undefined) continue
|
|
309
309
|
if (field?.type === 'section') {
|
|
310
|
-
out[key] = field.multiple === true ?
|
|
310
|
+
out[key] = field.multiple === true ? decodeSectionList(field, raw, dec) : decodeRecord(field.fields, raw, dec, null)
|
|
311
311
|
continue
|
|
312
312
|
}
|
|
313
313
|
const isBody = Boolean(dec.bodyAt) && sectionName === dec.bodyAt.section && key === dec.bodyAt.key
|
|
@@ -318,6 +318,14 @@ function decodeRecord(fields, value, dec, sectionName) {
|
|
|
318
318
|
return out
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// A `many` field's records — an OPEN MAP's written back as the author's map, keyed by each row's
|
|
322
|
+
// `name` (`isOpenMapSection`), which is what the push reads a map from. Rows that cannot be a map
|
|
323
|
+
// stay a list, which the push reads too.
|
|
324
|
+
function decodeSectionList(def, value, dec) {
|
|
325
|
+
const list = decodeList(def, value, dec)
|
|
326
|
+
return (isOpenMapSection(def) && openMapFromRows(list)) || list
|
|
327
|
+
}
|
|
328
|
+
|
|
321
329
|
// The records of a `many` section; a self-nesting one's `$children` nest back under
|
|
322
330
|
// `children:`, the reserved key the push reads them from.
|
|
323
331
|
function decodeList(def, value, dec) {
|
|
@@ -332,8 +340,11 @@ function decodeList(def, value, dec) {
|
|
|
332
340
|
}
|
|
333
341
|
|
|
334
342
|
// A leaf's authored value. A localized SCALAR's target locales are captured into the
|
|
335
|
-
// collector
|
|
336
|
-
//
|
|
343
|
+
// collector — a markdown body's too, keyed by its whole text as the records extractor keys it
|
|
344
|
+
// (a ProseMirror body's are captured by `unwrapLocalizedContent`, with its free-form path); a
|
|
345
|
+
// list of localized values is unwrapped element by element. ⛔ Until 2026-09-26 a body was
|
|
346
|
+
// taken to be ProseMirror, so a markdown one's translations were captured nowhere and a
|
|
347
|
+
// clone rendered it in the source language.
|
|
337
348
|
function decodeLeaf(raw, field, dec, isBody) {
|
|
338
349
|
if (field?.type === 'entity_ref') return decodeReference(raw, field, dec)
|
|
339
350
|
if (isProseMirrorField(field)) {
|
|
@@ -344,10 +355,10 @@ function decodeLeaf(raw, field, dec, isBody) {
|
|
|
344
355
|
}
|
|
345
356
|
if (!field?.localized) return raw
|
|
346
357
|
if (Array.isArray(raw)) {
|
|
347
|
-
|
|
358
|
+
for (const item of raw) dec.collector?.add(item, dec.context)
|
|
348
359
|
return unwrapLocalizedList(raw, dec.sourceLocale)
|
|
349
360
|
}
|
|
350
|
-
|
|
361
|
+
dec.collector?.add(raw, dec.context)
|
|
351
362
|
return unwrapLocalized(raw, dec.sourceLocale)
|
|
352
363
|
}
|
|
353
364
|
|
package/src/uwx/data-schema.js
CHANGED
|
@@ -37,6 +37,40 @@ const CONTENT_TEXT_FORMATS = new Set(['markdown', 'html'])
|
|
|
37
37
|
// declares its own `name` is an error instead, which names the problem precisely.
|
|
38
38
|
const OPEN_MAP_KEY = 'name'
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* ⭐ AN OPEN MAP'S LOWERED SECTION. `values:` lowers to rows (`lowerField`): a `multiple` section
|
|
42
|
+
* whose required string `name` — the map's key — is made each row's identity by a `unique_field`
|
|
43
|
+
* rule. A record FILE holds it as the author wrote it, a map from key to value; the WIRE holds the
|
|
44
|
+
* rows. This is the one test, read off the declaration alone, because a pull into a clone has
|
|
45
|
+
* nothing else: the push turns a file's map into rows by it, and the pull the rows back into a map.
|
|
46
|
+
*/
|
|
47
|
+
export function isOpenMapSection(def) {
|
|
48
|
+
if (def?.multiple !== true || def.self_nesting === true) return false
|
|
49
|
+
const key = def.fields?.[OPEN_MAP_KEY]
|
|
50
|
+
if (key?.type !== 'string' || key.required !== true || key.localized === true) return false
|
|
51
|
+
return (def.constraints || []).some((c) => c?.kind === 'unique_field' && c.field === OPEN_MAP_KEY && c.scope === 'section')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A file's open map as the rows it lowers to, in the map's key order: each key is its row's `name`. */
|
|
55
|
+
export function openMapEntries(map) {
|
|
56
|
+
return Object.entries(map).map(([key, value]) => ({ key, value, row: { ...value, [OPEN_MAP_KEY]: key } }))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Rows back as the map a file holds, or null when they cannot be one — a row with no key, or a key
|
|
61
|
+
* two rows share — so a caller keeps them a list, which a push reads too, and nothing is lost.
|
|
62
|
+
*/
|
|
63
|
+
export function openMapFromRows(rows) {
|
|
64
|
+
const map = {}
|
|
65
|
+
for (const row of rows || []) {
|
|
66
|
+
const key = row?.[OPEN_MAP_KEY]
|
|
67
|
+
if (typeof key !== 'string' || key === '' || Object.hasOwn(map, key)) return null
|
|
68
|
+
const { [OPEN_MAP_KEY]: _key, ...value } = row
|
|
69
|
+
map[key] = value
|
|
70
|
+
}
|
|
71
|
+
return map
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
/**
|
|
41
75
|
* A `text` field marked as rich content (`format: markdown` or `html`): the
|
|
42
76
|
* file-based body target. Round-trips as the raw source string — what the retired
|
package/src/uwx/index.js
CHANGED
|
@@ -179,3 +179,12 @@ export {
|
|
|
179
179
|
// is the foundation-less variant (only data-schema entities) for a schemas-only package.
|
|
180
180
|
export { buildRegistryPackage, buildSchemaOnlyPackage } from './registry-package.js'
|
|
181
181
|
export { toDataSchemaDeclaration } from './data-schema.js'
|
|
182
|
+
|
|
183
|
+
// A registered foundation a project names by catalog ref — a clone's — kept under `.uniweb/` as the
|
|
184
|
+
// backend answered for it, so the build, the push and the pull can read its section types.
|
|
185
|
+
export {
|
|
186
|
+
registeredFoundationPath,
|
|
187
|
+
readRegisteredFoundation,
|
|
188
|
+
writeRegisteredFoundation,
|
|
189
|
+
} from '../site/registered-foundation.js'
|
|
190
|
+
export { parseCatalogRef } from '../site/foundation-ref.js'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ⭐ A PAGE'S OR A SITE'S OPEN GRAPH TITLE AND DESCRIPTION, PER LANGUAGE — `og_title` and
|
|
2
|
+
// `og_description` on the wire, `seo.ogTitle` / `seo.ogDescription` in a file.
|
|
3
|
+
//
|
|
4
|
+
// A file keeps them inside `seo`, whose other keys are crawler and sitemap directives a deployment
|
|
5
|
+
// stores as one object. Where the deployment's page — or `settings` — Section declares the two keys
|
|
6
|
+
// (`GET /dev/config` → `siteContent.pageFields` / `settingsFields`), a push sends them per language
|
|
7
|
+
// through those keys and the rest of `seo` as it is; elsewhere they ride inside `seo` as authored,
|
|
8
|
+
// in the source language only. A pull puts them back inside `seo`.
|
|
9
|
+
import { localizeScalar } from './locale-sync.js'
|
|
10
|
+
import { unwrapLocalized } from './backfill.js'
|
|
11
|
+
|
|
12
|
+
// wire key → the key inside a file's `seo`
|
|
13
|
+
const OPEN_GRAPH_KEYS = [
|
|
14
|
+
['og_title', 'ogTitle'],
|
|
15
|
+
['og_description', 'ogDescription'],
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
19
|
+
|
|
20
|
+
/** Whether a Section's declared keys take the two. */
|
|
21
|
+
export function takesOpenGraph(fields) {
|
|
22
|
+
return Array.isArray(fields) && OPEN_GRAPH_KEYS.every(([wire]) => fields.includes(wire))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A file's `seo` as a push sends it: `{ og_title?, og_description?, seo? }`. The two are taken out
|
|
27
|
+
* of `seo` and localized only where the Section takes them; `seo` is left out when nothing else is
|
|
28
|
+
* in it. A value that is not a string stays where it is.
|
|
29
|
+
*/
|
|
30
|
+
export function splitOpenGraph(seo, fields, sourceLocale, translations, context = null) {
|
|
31
|
+
if (!takesOpenGraph(fields) || !isRecord(seo)) return { seo }
|
|
32
|
+
const rest = { ...seo }
|
|
33
|
+
const out = {}
|
|
34
|
+
for (const [wire, key] of OPEN_GRAPH_KEYS) {
|
|
35
|
+
if (typeof seo[key] !== 'string') continue
|
|
36
|
+
out[wire] = localizeScalar(seo[key], sourceLocale, translations, context)
|
|
37
|
+
delete rest[key]
|
|
38
|
+
}
|
|
39
|
+
out.seo = Object.keys(rest).length ? rest : undefined
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One of the two, as a push sends it — or undefined where the Section does not take them. */
|
|
44
|
+
export function openGraphValue(seo, wire, fields, sourceLocale, translations, context = null) {
|
|
45
|
+
return splitOpenGraph(seo, fields, sourceLocale, translations, context)[wire]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `seo` as a push sends it beside the two — without them where the Section takes them. */
|
|
49
|
+
export function seoBesideOpenGraph(seo, fields) {
|
|
50
|
+
return splitOpenGraph(seo, fields, null, null).seo
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A document's `og_title` / `og_description` folded back into a file's `seo` — the source
|
|
55
|
+
* language's value — with the other languages handed to `collector` in `context`. Returns `seo`
|
|
56
|
+
* unchanged when the holder carries neither, and undefined for an empty result.
|
|
57
|
+
*/
|
|
58
|
+
export function foldOpenGraph(holder, seo, sourceLocale, collector = null, context = null) {
|
|
59
|
+
if (!OPEN_GRAPH_KEYS.some(([wire]) => holder?.[wire] !== undefined)) return seo
|
|
60
|
+
const out = isRecord(seo) ? { ...seo } : {}
|
|
61
|
+
for (const [wire, key] of OPEN_GRAPH_KEYS) {
|
|
62
|
+
const value = holder?.[wire]
|
|
63
|
+
if (value === undefined) continue
|
|
64
|
+
collector?.add(value, context)
|
|
65
|
+
const source = unwrapLocalized(value, sourceLocale)
|
|
66
|
+
if (typeof source === 'string' && source !== '') out[key] = source
|
|
67
|
+
}
|
|
68
|
+
return Object.keys(out).length ? out : undefined
|
|
69
|
+
}
|
|
@@ -201,10 +201,13 @@ export function writeSectionFile({ filePath, content, params, reserved = DEFAULT
|
|
|
201
201
|
// is not re-serialized: the writer's markdown differs from an author's in blank lines and line breaks,
|
|
202
202
|
// and a pull that changed nothing reformatted every section it touched (measured 2026-09-26 on the
|
|
203
203
|
// `marketing` template). A file with nothing to change is not written at all.
|
|
204
|
+
// ⛔ An EMPTY body is one too: until 2026-09-26 it never compared as unchanged, so a section holding
|
|
205
|
+
// only frontmatter — a parametric page's `type:` and `fetch:` — was re-serialized, and lost its
|
|
206
|
+
// comments, on a pull that changed nothing. So is no document at all (a params-only update).
|
|
204
207
|
const authoredBody = (existingBody || '').replace(/^\n+/, '').replace(/\s+$/, '')
|
|
205
|
-
const keepsBody =
|
|
206
|
-
if (keepsBody && canonicalJson(nextFrontmatter) === canonicalJson(frontmatter)) return 'unchanged'
|
|
207
|
-
const body =
|
|
208
|
+
const keepsBody = !content || sameMarkdownDocument(authoredBody, content)
|
|
209
|
+
if (existing && keepsBody && canonicalJson(nextFrontmatter) === canonicalJson(frontmatter)) return 'unchanged'
|
|
210
|
+
const body = keepsBody ? authoredBody : proseMirrorToMarkdown(content)
|
|
208
211
|
return writeIfChanged(filePath, assembleSection(nextFrontmatter, body))
|
|
209
212
|
}
|
|
210
213
|
|
|
@@ -255,8 +255,12 @@ function siteFoundationDeclarations(siteRoot) {
|
|
|
255
255
|
return { own: null, keyTypes: null }
|
|
256
256
|
}
|
|
257
257
|
const schema = siteYml ? foundationSchemaJson(siteRoot, siteYml) : null
|
|
258
|
+
// ⛔ Own names only from a schema that carries its data schemas. The registered version a clone
|
|
259
|
+
// keeps carries none (they travel as Models), and read as an empty set it said "owns nothing", so a
|
|
260
|
+
// clone placed a foundation's own `@std/exhibit` in `records/std/exhibit/`; unknown (null), the
|
|
261
|
+
// standard-schema rule decides (`unresolveSelfScope`), as it did before the kept version existed.
|
|
258
262
|
return schema
|
|
259
|
-
? { own: ownSchemaNames(schema.dataSchemas
|
|
263
|
+
? { own: ownSchemaNames(schema.dataSchemas), keyTypes: dataKeyTypes(schema) }
|
|
260
264
|
: { own: null, keyTypes: null }
|
|
261
265
|
}
|
|
262
266
|
|
|
@@ -318,7 +322,9 @@ const DECL_WIRE_CONSUMED = new Set([
|
|
|
318
322
|
'deferred',
|
|
319
323
|
'detail_url',
|
|
320
324
|
'detail',
|
|
321
|
-
'queryable'
|
|
325
|
+
'queryable',
|
|
326
|
+
// Says the query names no schema of its own — how the file is written, never a key in it.
|
|
327
|
+
'typed_by_data_key',
|
|
322
328
|
])
|
|
323
329
|
|
|
324
330
|
// A standard schema (`@std/<name>`), normalized as the build resolves one — known with no foundation
|
|
@@ -348,7 +354,7 @@ function isDerivedDeferred(d, dataSchemas, pulledModel = null) {
|
|
|
348
354
|
return d.deferred.every((f) => a.has(f))
|
|
349
355
|
}
|
|
350
356
|
|
|
351
|
-
function declToFileShape(wire, dataSchemas = null, scope = null, own = null, keyTypes = null, authored = null, models = null) {
|
|
357
|
+
function declToFileShape(wire, dataSchemas = null, scope = null, own = null, keyTypes = null, authored = null, models = null, markedOnly = false) {
|
|
352
358
|
// ⛔ UNDO THE PRODUCER'S QUALIFICATION FIRST, before anything compares against
|
|
353
359
|
// `schema`. The push qualifies a foundation-relative `@/x` to `@scope/x`
|
|
354
360
|
// (`site.js::queriesNested`), and both checks below are keyed by the author's
|
|
@@ -385,8 +391,15 @@ function declToFileShape(wire, dataSchemas = null, scope = null, own = null, key
|
|
|
385
391
|
// ⛔ UNLESS THE AUTHOR WROTE IT: a `schema:` the local file already states for this query is
|
|
386
392
|
// written back as it is. Until 2026-09-25 a pull into a working copy dropped international's
|
|
387
393
|
// `articles: { schema: '@std/article' }`, because a section types `articles` as `@std/article`.
|
|
388
|
-
|
|
394
|
+
// ⭐ A CLONE GOES BY THE MARK. Where the key's type comes only from the registered foundation a
|
|
395
|
+
// project keeps (`markedOnly`: its foundation is a catalog ref), a query is written terse only when
|
|
396
|
+
// the push marked it `typed_by_data_key` — the author wrote no schema — or when the author's own
|
|
397
|
+
// file declares it with none. Unmarked, the schema was the author's, and it is written.
|
|
389
398
|
const written = name ? authored?.get(name) : undefined
|
|
399
|
+
const typedHere = !markedOnly || wire.typed_by_data_key === true || written === null
|
|
400
|
+
const keyType = name && !own?.has(name) && typedHere ? keyTypes?.get(name) : null
|
|
401
|
+
// In the author's form: the registered version names the type qualified (`@acme/member`).
|
|
402
|
+
const keyDefault = keyType ? unresolveSelfScope(keyType, scope, own) : null
|
|
390
403
|
if (d.schema && (d.schema === written || (d.schema !== defaultSchema(name) && d.schema !== keyDefault))) {
|
|
391
404
|
decl.schema = d.schema
|
|
392
405
|
}
|
|
@@ -463,17 +476,41 @@ function authoredQuerySchemas(siteRoot, siteYml) {
|
|
|
463
476
|
} catch {
|
|
464
477
|
file = null
|
|
465
478
|
}
|
|
479
|
+
// name → the schema the author's file states for it, or null for a query it declares with none.
|
|
480
|
+
// ⛔ The string shorthand (`team: '@/member'`) is a schema too: until 2026-09-26 it read as none,
|
|
481
|
+
// so a pull could write the author's `team: '@/member'` back as `team: {}`.
|
|
466
482
|
const out = new Map()
|
|
467
483
|
for (const decls of [siteYml?.queries, file]) {
|
|
468
484
|
if (!decls || typeof decls !== 'object') continue
|
|
469
485
|
for (const [name, decl] of Object.entries(decls)) {
|
|
470
|
-
|
|
471
|
-
|
|
486
|
+
const schema = typeof decl === 'string' ? decl : typeof decl?.schema === 'string' ? decl.schema : null
|
|
487
|
+
out.set(name, schema)
|
|
472
488
|
}
|
|
473
489
|
}
|
|
474
490
|
return out
|
|
475
491
|
}
|
|
476
492
|
|
|
493
|
+
// ⭐ WHERE A NEW RECORD OF A KEY'S TYPE GOES: the folder of a query the site declares with no schema
|
|
494
|
+
// of its own, whose name is a data key typed as that Model — `records/team/` for `team:` typed
|
|
495
|
+
// `@acme/member` — which is where a push reads it from (`uwx/data-key-types.js`). model → folder.
|
|
496
|
+
function typedQueryFolders(siteRoot, recordsRoot, scope, own, keyTypes) {
|
|
497
|
+
const out = new Map()
|
|
498
|
+
if (!keyTypes?.size) return out
|
|
499
|
+
let siteYml = null
|
|
500
|
+
try {
|
|
501
|
+
siteYml = yaml.load(readFileSync(join(siteRoot, 'site.yml'), 'utf8'), YAML_OPTIONS) || null
|
|
502
|
+
} catch {
|
|
503
|
+
siteYml = null
|
|
504
|
+
}
|
|
505
|
+
for (const [name, schema] of authoredQuerySchemas(siteRoot, siteYml)) {
|
|
506
|
+
if (schema !== null || own?.has(name)) continue
|
|
507
|
+
const type = keyTypes.get(name)
|
|
508
|
+
const model = type ? resolveSelfScope(type, scope) : null
|
|
509
|
+
if (model && !out.has(model)) out.set(model, join(recordsRoot, name))
|
|
510
|
+
}
|
|
511
|
+
return out
|
|
512
|
+
}
|
|
513
|
+
|
|
477
514
|
export function declarationsToQueriesYml({ document, siteRoot, scope, models = null, ...rest }) {
|
|
478
515
|
refuseOrgOption(rest, 'declarationsToQueriesYml')
|
|
479
516
|
const decls = Array.isArray(document?.queries) ? document.queries : []
|
|
@@ -500,10 +537,12 @@ export function declarationsToQueriesYml({ document, siteRoot, scope, models = n
|
|
|
500
537
|
const own = ownSchemaNames(dataSchemas)
|
|
501
538
|
const keyTypes = siteYml ? dataKeyTypes(foundationSchemaJson(siteRoot, siteYml)) : null
|
|
502
539
|
const authored = authoredQuerySchemas(siteRoot, siteYml)
|
|
540
|
+
// The key types a catalog-ref foundation has come from the registered version the project keeps.
|
|
541
|
+
const markedOnly = Boolean(parseCatalogRef(siteYml?.foundation))
|
|
503
542
|
|
|
504
543
|
const queries = {}
|
|
505
544
|
for (const d of decls) {
|
|
506
|
-
const { name, decl } = declToFileShape(d, dataSchemas, selfScope, own, keyTypes, authored, models)
|
|
545
|
+
const { name, decl } = declToFileShape(d, dataSchemas, selfScope, own, keyTypes, authored, models, markedOnly)
|
|
507
546
|
if (!name) continue
|
|
508
547
|
queries[name] = decl
|
|
509
548
|
}
|
|
@@ -682,6 +721,7 @@ export function recordsToProject({ folderDoc, recordDocs = [], siteRoot, opts =
|
|
|
682
721
|
// The file holding a record already, by its own id: the derived folder first, then any
|
|
683
722
|
// other that reads as the same Model (`recordDirsReadingAs`).
|
|
684
723
|
const readingAs = (model) => recordDirsReadingAs(recordsRoot, model, selfScope, { own, keyTypes })
|
|
724
|
+
const keyFolders = typedQueryFolders(siteRoot, recordsRoot, selfScope, own, keyTypes)
|
|
685
725
|
const findRecordFile = (poolDir, model, ownId) => {
|
|
686
726
|
const dirs = [poolDir, ...readingAs(model).filter((dir) => dir !== poolDir)]
|
|
687
727
|
// Single-record files first, as before; then an entry of a list file.
|
|
@@ -735,8 +775,9 @@ export function recordsToProject({ folderDoc, recordDocs = [], siteRoot, opts =
|
|
|
735
775
|
isNew = false
|
|
736
776
|
} else {
|
|
737
777
|
// A new record goes where the project already keeps records of its Model — its derived
|
|
738
|
-
// folder, else another that reads as the Model (`records/team/` for `@std/member`)
|
|
739
|
-
|
|
778
|
+
// folder, else another that reads as the Model (`records/team/` for `@std/member`), else the
|
|
779
|
+
// folder of a query typed by its data key as the Model (`typedQueryFolders`) — a clone's.
|
|
780
|
+
const home = existsSync(poolDir) ? poolDir : readingAs(schema)[0] || keyFolders.get(schema) || poolDir
|
|
740
781
|
format = defaultFormat(home, declaration)
|
|
741
782
|
filePath = join(home, where.slug + EXT_FOR_FORMAT[format])
|
|
742
783
|
isNew = true
|
package/src/uwx/records.js
CHANGED
|
@@ -44,6 +44,7 @@ import { readFileSync, existsSync } from 'node:fs'
|
|
|
44
44
|
import yaml from 'js-yaml'
|
|
45
45
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
46
46
|
import { detectFoundationType } from '../site/foundation-ref.js'
|
|
47
|
+
import { foundationSchemaJson } from '../site/queries-config.js'
|
|
47
48
|
import { join, resolve } from 'node:path'
|
|
48
49
|
|
|
49
50
|
import { resolveQueriesConfig } from './queries-config.js'
|
|
@@ -55,7 +56,7 @@ import {
|
|
|
55
56
|
poolPathReadings,
|
|
56
57
|
poolDirsForSchema,
|
|
57
58
|
} from '../site/entity-pool.js'
|
|
58
|
-
import { toDataSchemaDeclaration, isProseMirrorField, isMarkupTextField } from './data-schema.js'
|
|
59
|
+
import { toDataSchemaDeclaration, isProseMirrorField, isMarkupTextField, isOpenMapSection, openMapEntries } from './data-schema.js'
|
|
59
60
|
import { recordFileLayout, contentBodyTarget } from './record-layout.js'
|
|
60
61
|
import { emitEntitySyncPackage } from './entity-document.js'
|
|
61
62
|
import { resolveSelfScope, siteSelfScope, refuseOrgOption } from './self-scope.js'
|
|
@@ -268,6 +269,25 @@ export function recordsToEntities({
|
|
|
268
269
|
`"${bodyTarget.section}.${bodyTarget.key}"`
|
|
269
270
|
)
|
|
270
271
|
}
|
|
272
|
+
// ⛔ A SITE'S FOLDER PLACES ONLY A RECORD SOMETHING CAN REFERENCE. A folder entry is a
|
|
273
|
+
// reference, and a Model with no brief is not linkable (`toDataSchemaDeclaration`), so a
|
|
274
|
+
// backend refuses to place its records — measured 2026-09-25 as a `409 … linkable: false`,
|
|
275
|
+
// and by design (backend, 2026-09-26). Refused here, once for the schema, before anything
|
|
276
|
+
// is sent — and saying why, which the backend's refusal names only by its design doc.
|
|
277
|
+
if (declaration.linkable === false) {
|
|
278
|
+
const slugs = (records || []).map((r) => r?.slug).filter(Boolean)
|
|
279
|
+
if (slugs.length) {
|
|
280
|
+
const named = slugs.slice(0, 3).join(', ') + (slugs.length > 3 ? ` and ${slugs.length - 3} more` : '')
|
|
281
|
+
refusals.push(
|
|
282
|
+
`${label}: ${declaration.name} has no brief, so a site's folder cannot place its records ` +
|
|
283
|
+
`(${named}) — a folder entry is a reference, and only a record of a schema with a brief ` +
|
|
284
|
+
'can be one. Give the schema a brief (`brief: true` on one of its sections), or keep this ' +
|
|
285
|
+
'data out of records/.'
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
return { entities, warnings, refusals }
|
|
289
|
+
}
|
|
290
|
+
|
|
271
291
|
for (const record of records || []) {
|
|
272
292
|
const slug = record.slug
|
|
273
293
|
if (!slug) {
|
|
@@ -429,6 +449,20 @@ function readBySection(layout, record, enc) {
|
|
|
429
449
|
// are its child sections), a list of records for a `many` one.
|
|
430
450
|
function encodeSection(def, value, enc, path) {
|
|
431
451
|
if (def.multiple === true) {
|
|
452
|
+
// ⭐ AN OPEN MAP is the author's map in a file and rows on the wire (`isOpenMapSection`): each
|
|
453
|
+
// key becomes its row's `name`. ⛔ Until 2026-09-26 the map was refused as "a list of records",
|
|
454
|
+
// so no record holding one could be pushed. Rows written as a list are read as they are.
|
|
455
|
+
if (isOpenMapSection(def) && isPlainObject(value)) {
|
|
456
|
+
const rows = []
|
|
457
|
+
for (const { key, value: entry, row } of openMapEntries(value)) {
|
|
458
|
+
if (!isPlainObject(entry)) {
|
|
459
|
+
enc.shape.push(`"${path}.${key}" is one entry of a map — write it as a map of its fields.`)
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
rows.push(encodeItem(def, row, enc, `${path}.${key}`))
|
|
463
|
+
}
|
|
464
|
+
return rows
|
|
465
|
+
}
|
|
432
466
|
if (!Array.isArray(value)) {
|
|
433
467
|
enc.shape.push(`"${path}" holds a list of records — write it as a list ("- …" under "${path}:").`)
|
|
434
468
|
return undefined
|
|
@@ -757,6 +791,17 @@ function resolveFoundationDir(siteRoot, opts) {
|
|
|
757
791
|
}
|
|
758
792
|
}
|
|
759
793
|
|
|
794
|
+
// The site's foundation's schema as a project keeps it for a catalog ref (`foundationSchemaJson`,
|
|
795
|
+
// `site/registered-foundation.js`) — its section types, which is all this is read for.
|
|
796
|
+
function registeredFoundationSchema(siteRoot) {
|
|
797
|
+
try {
|
|
798
|
+
const siteYml = yaml.load(readFileSync(join(siteRoot, 'site.yml'), 'utf8'), YAML_OPTIONS) || {}
|
|
799
|
+
return foundationSchemaJson(siteRoot, siteYml)
|
|
800
|
+
} catch {
|
|
801
|
+
return null
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
760
805
|
// Load the local foundation's built schema.json (the source of locally-defined
|
|
761
806
|
// Model declarations), or null when there's no local foundation. `required` (set
|
|
762
807
|
// when no remote resolver is available) turns "missing" into a helpful error
|
|
@@ -988,7 +1033,8 @@ export async function buildRecordEntities(siteRoot, opts = {}) {
|
|
|
988
1033
|
// holds records of that type. `keyTyped` records each such name for the `queries` Section,
|
|
989
1034
|
// which must name the same data schema (`sync-package.js` hands it on). ⛔ Never for a schema
|
|
990
1035
|
// a query asked for explicitly: that one is the author's, and fails loudly.
|
|
991
|
-
|
|
1036
|
+
// A clone has no local foundation: its section types come from the registered version it keeps.
|
|
1037
|
+
const keyTypes = dataKeyTypes(localSchema || registeredFoundationSchema(siteRoot))
|
|
992
1038
|
const keyTyped = new Map()
|
|
993
1039
|
const typedFor = async (schema) => {
|
|
994
1040
|
const key = keyOfDefaultRef(schema)
|
package/src/uwx/site-project.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// we unwrap to the source locale for the file surface (other locales stay in
|
|
33
33
|
// the i18n pipeline). Absent `info` keys are left untouched on disk.
|
|
34
34
|
|
|
35
|
+
import { foldOpenGraph } from './open-graph.js'
|
|
35
36
|
import { join, relative, extname, basename, dirname } from 'node:path'
|
|
36
37
|
import { restoreAssetRefs } from './asset-map.js'
|
|
37
38
|
import { readBackendState, updateBackendState } from './sync-store.js'
|
|
@@ -43,7 +44,7 @@ import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
|
43
44
|
import { writeSiteConfig, writeThemeFile, writeIfChanged, writeSectionFile, writeMergedYaml } from './project-writer.js'
|
|
44
45
|
import { declarationsToQueriesYml } from './records-project.js'
|
|
45
46
|
import { FILLED_SECTION_TYPE, buildRouteOf } from './site.js'
|
|
46
|
-
import { translationContext } from '../i18n/extract.js'
|
|
47
|
+
import { translationContext, SITE_META_CONTEXT } from '../i18n/extract.js'
|
|
47
48
|
import { authorableDeclaration, DECLARATION_KEYS } from '../site/fetch-shapes.js'
|
|
48
49
|
import { createTranslationCollector, writeLocaleTranslations, writeFreeformTranslations, unwrapLocalizedContent, localesDir } from './locale-sync.js'
|
|
49
50
|
import { resolveLocaleList } from '../i18n/locales.js'
|
|
@@ -282,6 +283,11 @@ export function siteInfoToConfig({ document, siteRoot, backend = null, sourceLoc
|
|
|
282
283
|
for (const [settingsKey, ymlKey] of Object.entries(SETTINGS_TO_SITE_YML)) {
|
|
283
284
|
if (settingsSection[settingsKey] !== undefined) siteChanges[ymlKey] = settingsSection[settingsKey]
|
|
284
285
|
}
|
|
286
|
+
// ⭐ The site's Open Graph title and description, sent per language as `og_title` /
|
|
287
|
+
// `og_description` where the deployment takes them: back inside `seo`, the source language's,
|
|
288
|
+
// the others to the collector in the site's context (`uwx/open-graph.js`).
|
|
289
|
+
const siteSeo = foldOpenGraph(settingsSection, siteChanges.seo, sourceLocale, collector, SITE_META_CONTEXT)
|
|
290
|
+
if (siteSeo !== undefined) siteChanges.seo = siteSeo
|
|
285
291
|
|
|
286
292
|
// `settings.keywords` is a LOCALIZED list (it renders into `<meta name="keywords">`)
|
|
287
293
|
// → unwrap to the source locale; the target locales go to the collector.
|
|
@@ -696,7 +702,10 @@ function pageRecordToYml(record, sectionsArray, sourceLocale, existing = null, {
|
|
|
696
702
|
if (record.redirect !== undefined) y.redirect = record.redirect
|
|
697
703
|
if (record.rewrite !== undefined) y.rewrite = record.rewrite
|
|
698
704
|
if (record.layout !== undefined) y.layout = record.layout
|
|
699
|
-
|
|
705
|
+
// Its Open Graph title and description, per language on the wire, back inside `seo`
|
|
706
|
+
// (`uwx/open-graph.js`); their other languages are collected with the page's metadata.
|
|
707
|
+
const seo = foldOpenGraph(record, record.seo, sourceLocale)
|
|
708
|
+
if (seo !== undefined) y.seo = seo
|
|
700
709
|
// Invert the build's resolution rather than copy it — see fetch-shapes.js. Under
|
|
701
710
|
// the key the file already uses (`existing`), else `query:` for names alone.
|
|
702
711
|
if (record.fetch !== undefined) {
|
|
@@ -816,6 +825,8 @@ function writePagesTree(pages, pagesDir, sourceLocale, report, ctx, routePrefix
|
|
|
816
825
|
ctx.collector?.add(record.title, meta)
|
|
817
826
|
ctx.collector?.add(record.label, meta)
|
|
818
827
|
ctx.collector?.add(record.description, meta)
|
|
828
|
+
ctx.collector?.add(record.og_title, meta)
|
|
829
|
+
ctx.collector?.add(record.og_description, meta)
|
|
819
830
|
// keywords is a localized ARRAY — capture each element's target locales.
|
|
820
831
|
if (Array.isArray(record.keywords)) record.keywords.forEach((kw) => ctx.collector?.add(kw, meta))
|
|
821
832
|
else ctx.collector?.add(record.keywords, meta)
|
package/src/uwx/site.js
CHANGED
|
@@ -76,7 +76,8 @@ import { loadLocaleTranslations, localizeScalar, localizeScalarList, localizeCon
|
|
|
76
76
|
import { unwrapLocalized } from './backfill.js'
|
|
77
77
|
import { loadFreeformTranslation } from '../i18n/freeform.js'
|
|
78
78
|
import { resolveLocaleList } from '../i18n/locales.js'
|
|
79
|
-
import { translationContext } from '../i18n/extract.js'
|
|
79
|
+
import { translationContext, SITE_META_CONTEXT } from '../i18n/extract.js'
|
|
80
|
+
import { splitOpenGraph, openGraphValue, seoBesideOpenGraph } from './open-graph.js'
|
|
80
81
|
import { updateBackendState, readBackendState } from './sync-store.js'
|
|
81
82
|
import { upsertYamlScalar } from './yaml-upsert.js'
|
|
82
83
|
import { resolveQueriesConfig } from './queries-config.js'
|
|
@@ -293,7 +294,9 @@ function localizePageMeta(page, route, sourceLocale, translations) {
|
|
|
293
294
|
value && typeof value === 'object' && typeof value[sourceLocale] === 'string'
|
|
294
295
|
? localizeScalar(value[sourceLocale], sourceLocale, translations, context)
|
|
295
296
|
: value
|
|
296
|
-
for (const key of ['title', 'label', 'description'
|
|
297
|
+
for (const key of ['title', 'label', 'description', 'og_title', 'og_description']) {
|
|
298
|
+
if (page[key] !== undefined) page[key] = again(page[key])
|
|
299
|
+
}
|
|
297
300
|
if (Array.isArray(page.keywords)) page.keywords = page.keywords.map(again)
|
|
298
301
|
}
|
|
299
302
|
|
|
@@ -318,7 +321,7 @@ function mapSectionData(section) {
|
|
|
318
321
|
}
|
|
319
322
|
|
|
320
323
|
function buildPageData(config, ctx) {
|
|
321
|
-
const { slug, mode, isDynamic, paramName, isRoot, siteIndex, sourceLocale, translations, where } =
|
|
324
|
+
const { slug, mode, isDynamic, paramName, isRoot, siteIndex, sourceLocale, translations, where, pageFields } =
|
|
322
325
|
ctx
|
|
323
326
|
// The page `slug` is the localized route source — a `{lang: slug}` map (the
|
|
324
327
|
// site-content Model declares it localized; greenlit 2026-06-13). The source
|
|
@@ -355,7 +358,13 @@ function buildPageData(config, ctx) {
|
|
|
355
358
|
setIf(data, 'redirect', config.redirect)
|
|
356
359
|
setIf(data, 'rewrite', config.rewrite)
|
|
357
360
|
setIf(data, 'layout', config.layout)
|
|
358
|
-
|
|
361
|
+
// ⭐ Its Open Graph title and description per language, where the deployment's page takes them
|
|
362
|
+
// (`uwx/open-graph.js`) — out of `seo`, which travels as one object. ⛔ Until 2026-09-26 they rode
|
|
363
|
+
// inside `seo` only, in the source language, though the build translates them.
|
|
364
|
+
const og = splitOpenGraph(config.seo, pageFields, sourceLocale, translations)
|
|
365
|
+
setIf(data, 'og_title', og.og_title)
|
|
366
|
+
setIf(data, 'og_description', og.og_description)
|
|
367
|
+
setIf(data, 'seo', og.seo)
|
|
359
368
|
// ⭐ A `query:` or `fetch:` LIST means "fetch each" — one declaration per entry. Before
|
|
360
369
|
// 2026-09-02 this kept `[0]` and dropped the rest silently, so the wire
|
|
361
370
|
// carried one dataset for a page that asked for several.
|
|
@@ -729,7 +738,7 @@ const REPUBLISH_CLAUSE =
|
|
|
729
738
|
// Recursively build the `pages` tree: each record carries its fields, its inline
|
|
730
739
|
// `page_sections` (page mode only), and its child pages under `$children`.
|
|
731
740
|
async function walkPagesNested(ctx, dirPath, parentSlugPath, inheritedMode, parentConfig, isRoot) {
|
|
732
|
-
const { siteRoot, siteIndex, sourceLocale, translations } = ctx
|
|
741
|
+
const { siteRoot, siteIndex, sourceLocale, translations, pageFields } = ctx
|
|
733
742
|
const folders = await orderedSubfolders(dirPath, inheritedMode, parentConfig)
|
|
734
743
|
const out = []
|
|
735
744
|
// A folder inside a `[...path]` folder can never be reached, and `[dir]` /
|
|
@@ -763,6 +772,7 @@ async function walkPagesNested(ctx, dirPath, parentSlugPath, inheritedMode, pare
|
|
|
763
772
|
siteIndex,
|
|
764
773
|
sourceLocale,
|
|
765
774
|
translations,
|
|
775
|
+
pageFields,
|
|
766
776
|
})
|
|
767
777
|
// `$id` is the stableId when authored (rename-stable), else the slug (the
|
|
768
778
|
// natural handle — spec default). The path is NEVER the identity.
|
|
@@ -980,7 +990,9 @@ const DECL_EMITTED_ABOVE = new Set([
|
|
|
980
990
|
'limit',
|
|
981
991
|
'excerpt',
|
|
982
992
|
'deferred',
|
|
983
|
-
'queryable'
|
|
993
|
+
'queryable',
|
|
994
|
+
// Derived by the rule above, never taken from a file.
|
|
995
|
+
'typed_by_data_key',
|
|
984
996
|
])
|
|
985
997
|
|
|
986
998
|
const DECL_NOT_ON_WIRE = new Set([
|
|
@@ -1022,7 +1034,7 @@ function externalSource(d) {
|
|
|
1022
1034
|
return source
|
|
1023
1035
|
}
|
|
1024
1036
|
|
|
1025
|
-
function queriesNested(declarations, uuids = null, scope = null, keyTyped = null) {
|
|
1037
|
+
function queriesNested(declarations, uuids = null, scope = null, keyTyped = null, queryFields = null) {
|
|
1026
1038
|
const out = []
|
|
1027
1039
|
for (const [name, d] of Object.entries(declarations)) {
|
|
1028
1040
|
refuseUnder(d.where, `queries.${name}`)
|
|
@@ -1041,8 +1053,13 @@ function queriesNested(declarations, uuids = null, scope = null, keyTyped = null
|
|
|
1041
1053
|
// empty. The pull puts the author's `@/` back (`records-project.js::declarationsToQueriesYml`).
|
|
1042
1054
|
// A query whose name-defaulted schema stands for the type of the data key of its name
|
|
1043
1055
|
// names that type — the data schema its records are sent as (`data-key-types.js`).
|
|
1044
|
-
const
|
|
1056
|
+
const typed = (!d.schemaExplicit && keyTyped?.get(d.schema)) || null
|
|
1057
|
+
const schema = typed || d.schema
|
|
1045
1058
|
setIf(data, 'schema', resolveSelfScope(schema, scope))
|
|
1059
|
+
// ⭐ …AND SAYS SO, so a pull can tell the author's form (`team:`, no schema) from an explicit
|
|
1060
|
+
// one — sent only where the deployment's `queries` Section declares the key (`GET /dev/config`
|
|
1061
|
+
// → `siteContent.queryFields`), since one that does not refuses a push carrying it.
|
|
1062
|
+
if (typed && Array.isArray(queryFields) && queryFields.includes('typed_by_data_key')) data.typed_by_data_key = true
|
|
1046
1063
|
setIf(data, 'sort', d.sort)
|
|
1047
1064
|
// Legacy `filter:` is not synced — it is translated to `where` upstream
|
|
1048
1065
|
// (the canonical predicate). No legacy fields on the wire.
|
|
@@ -1238,7 +1255,7 @@ function secretsNested(provisioned) {
|
|
|
1238
1255
|
// ROUND-TRIP LAW). `site-project.js::SETTINGS_TO_SITE_YML` plus its explicit
|
|
1239
1256
|
// branches is the other half, and `producer-list-drift.test.js` fails if a key
|
|
1240
1257
|
// emitted here has neither.
|
|
1241
|
-
function settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translations, languages = null } = {}) {
|
|
1258
|
+
function settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translations, languages = null, settingsFields = null } = {}) {
|
|
1242
1259
|
const settings = {}
|
|
1243
1260
|
|
|
1244
1261
|
// Site-wide values an author declares once and references from page content as
|
|
@@ -1287,7 +1304,12 @@ function settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translation
|
|
|
1287
1304
|
// (`core/src/seo.js`). Two of those are literally sitemap.xml columns. It only
|
|
1288
1305
|
// ever passed the card test because `image` was inside it; the card's picture is
|
|
1289
1306
|
// `info.preview` now.
|
|
1290
|
-
|
|
1307
|
+
// The site's Open Graph title and description per language, where the deployment's `settings`
|
|
1308
|
+
// takes them (`uwx/open-graph.js`), in the site's context (`SITE_META_CONTEXT`).
|
|
1309
|
+
// (Each read inline from `siteYml.seo`, so the emit surface names its source.)
|
|
1310
|
+
setIf(settings, 'og_title', openGraphValue(siteYml.seo, 'og_title', settingsFields, sourceLocale, translations, SITE_META_CONTEXT))
|
|
1311
|
+
setIf(settings, 'og_description', openGraphValue(siteYml.seo, 'og_description', settingsFields, sourceLocale, translations, SITE_META_CONTEXT))
|
|
1312
|
+
setIf(settings, 'seo', seoBesideOpenGraph(siteYml.seo, settingsFields))
|
|
1291
1313
|
// ⭐ `keywords` IS seo by function — it renders into `<meta name="keywords">`
|
|
1292
1314
|
// (`runtime/src/ssr-renderer.js`). It is top-level in site.yml for authoring
|
|
1293
1315
|
// convenience, not because it is a different kind of thing. Localized, so it
|
|
@@ -1569,7 +1591,8 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1569
1591
|
// (uwx-format.md → info.url.)
|
|
1570
1592
|
|
|
1571
1593
|
// `siteIndex` — the homepage — joins below, once the pages directory's own config is read.
|
|
1572
|
-
|
|
1594
|
+
// …and the keys this deployment's page takes (`siteContent.pageFields`), for its Open Graph texts.
|
|
1595
|
+
const ctx = { siteRoot, sourceLocale, translations, pageFields: opts.pageFields }
|
|
1573
1596
|
// ⛔ ONE `paths:` CONVENTION, TWO READERS, AND THIS ONE IS A STRICT SUBSET.
|
|
1574
1597
|
// The build honours sub-mounts — `paths: { pages/<segment>: <dir> }`, through
|
|
1575
1598
|
// `resolveMounts`/`mountEntriesOf` in `site/content-collector.js` — and this
|
|
@@ -1650,7 +1673,7 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1650
1673
|
// Emitted only when the file declares something — see `settingsNested`.
|
|
1651
1674
|
// The languages the site's translation files make, for a site that declares none (`settingsNested`).
|
|
1652
1675
|
const languages = targetLocales.length > 0 ? [sourceLocale, ...targetLocales] : null
|
|
1653
|
-
const settings = settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translations, languages })
|
|
1676
|
+
const settings = settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translations, languages, settingsFields: opts.settingsFields })
|
|
1654
1677
|
if (settings) doc.settings = settings
|
|
1655
1678
|
doc.pages = pages
|
|
1656
1679
|
doc.layout_sections = layoutSections
|
|
@@ -1663,7 +1686,7 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1663
1686
|
// ⚠️ `queriesNested` keeps its name. §2's rule: rename what an author or a
|
|
1664
1687
|
// consumer sees, leave the identifier alone.
|
|
1665
1688
|
const scope = opts.scope !== undefined ? opts.scope : await siteSelfScope(siteRoot)
|
|
1666
|
-
doc.queries = queriesNested(colConfig.declarations, opts.queryUuids, scope, opts.keyTyped)
|
|
1689
|
+
doc.queries = queriesNested(colConfig.declarations, opts.queryUuids, scope, opts.keyTyped, opts.queryFields)
|
|
1667
1690
|
// Emitted ONLY when the file declares the key — see the header above
|
|
1668
1691
|
// `serviceRecords`: on a replaced Section, absent and empty are different
|
|
1669
1692
|
// requests and one of them is destructive.
|