@uniweb/build 0.29.0 → 0.29.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 +4 -4
- package/src/site/fetch-shapes.js +70 -0
- package/src/uwx/index.js +1 -0
- package/src/uwx/site-diff.js +23 -0
- package/src/uwx/site-project.js +5 -2
- package/src/uwx/site.js +38 -3
- package/src/uwx/sync-package.js +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"sharp": "^0.35.3",
|
|
61
61
|
"yaml": "^2.5.0",
|
|
62
62
|
"@uniweb/content-reader": "^1.2.4",
|
|
63
|
-
"@uniweb/semantic-parser": "^1.3.1",
|
|
64
|
-
"@uniweb/projections": "^0.5.1",
|
|
65
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
66
63
|
"@uniweb/schemas": "^0.2.11",
|
|
64
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
65
|
+
"@uniweb/projections": "^0.5.1",
|
|
66
|
+
"@uniweb/semantic-parser": "^1.3.1",
|
|
67
67
|
"@uniweb/theming": "^0.1.15"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// What shape a `fetch:` declaration is, and what a PROJECTION may write back.
|
|
2
|
+
//
|
|
3
|
+
// ⛔ WHY THIS EXISTS. A `fetch:` declaration has three shapes, and the keys each one
|
|
4
|
+
// accepts differ (`data-fetcher.js` RECOGNIZED_FETCH_KEYS):
|
|
5
|
+
//
|
|
6
|
+
// refine refine · inherit · detail · limit · sort · where · filter
|
|
7
|
+
// collection collection · schema · … — and NOT `path`/`url`
|
|
8
|
+
// source path · url · schema · …
|
|
9
|
+
//
|
|
10
|
+
// The build RESOLVES a `collection:` shorthand into a concrete location, so the
|
|
11
|
+
// declaration that rides the sync wire carries BOTH the authored `collection` and
|
|
12
|
+
// the derived `path`. Projecting that back verbatim writes a file that is neither
|
|
13
|
+
// shape cleanly: `collection` wins the classification, and the `path` beside it is
|
|
14
|
+
// then an unrecognized key on its own declaration.
|
|
15
|
+
//
|
|
16
|
+
// ⚠️ Measured on matinee 2026-08-29 — `push → pull → push`, where the third step is
|
|
17
|
+
// rejected by our OWN validator against a file our OWN projector had just written:
|
|
18
|
+
//
|
|
19
|
+
// fetch:
|
|
20
|
+
// path: /data/members.json ← derived; also a build artifact path
|
|
21
|
+
// schema: members
|
|
22
|
+
// collection: members ← what the author actually wrote
|
|
23
|
+
//
|
|
24
|
+
// [uniweb] fetch: unrecognized key "path" was ignored. Keys recognized on this
|
|
25
|
+
// declaration: collection, detailPage, filter, limit, merge, prerender, schema,
|
|
26
|
+
// sort, transform, where.
|
|
27
|
+
//
|
|
28
|
+
// ⭐ The round trip has to invert the resolution, not copy it. `/data/<name>.json`
|
|
29
|
+
// is a materialization of a collection, never its definition — so it is precisely
|
|
30
|
+
// the thing an authored file should not contain.
|
|
31
|
+
//
|
|
32
|
+
// ⚖️ DROPS ONLY WHAT IS DERIVABLE, not everything unrecognized. A key we do not know
|
|
33
|
+
// might be one a newer producer authored, and silently discarding it on every pull
|
|
34
|
+
// would make the round trip lossy in a way nothing reports. `path` and `url` beside
|
|
35
|
+
// a `collection` are recoverable from the collection itself; anything else survives
|
|
36
|
+
// and the validator's warning stays the honest signal.
|
|
37
|
+
|
|
38
|
+
/** Which of the three shapes a declaration is — the same order `data-fetcher` uses. */
|
|
39
|
+
export function fetchShapeOf(fetch) {
|
|
40
|
+
if (!fetch || typeof fetch !== 'object') return null
|
|
41
|
+
if (fetch.refine === true || fetch.inherit === true) return 'refine'
|
|
42
|
+
if (fetch.collection) return 'collection'
|
|
43
|
+
return 'source'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Keys a shape derives rather than the author writing them. */
|
|
47
|
+
const DERIVED_BY_SHAPE = {
|
|
48
|
+
collection: ['path', 'url'],
|
|
49
|
+
refine: [],
|
|
50
|
+
source: []
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The declaration as an author would have written it — the wire's resolved form
|
|
55
|
+
* minus what the build derived.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} fetch a `fetch:` declaration off the sync wire
|
|
58
|
+
* @returns {object} the same declaration, safe to write into authored config
|
|
59
|
+
*/
|
|
60
|
+
export function authorableFetch(fetch) {
|
|
61
|
+
const shape = fetchShapeOf(fetch)
|
|
62
|
+
const derived = DERIVED_BY_SHAPE[shape]
|
|
63
|
+
if (!derived || derived.length === 0) return fetch
|
|
64
|
+
const out = {}
|
|
65
|
+
for (const [k, v] of Object.entries(fetch)) {
|
|
66
|
+
if (derived.includes(k)) continue
|
|
67
|
+
out[k] = v
|
|
68
|
+
}
|
|
69
|
+
return out
|
|
70
|
+
}
|
package/src/uwx/index.js
CHANGED
package/src/uwx/site-diff.js
CHANGED
|
@@ -292,3 +292,26 @@ export function describeSiteDiff(diff, { limit = 8 } = {}) {
|
|
|
292
292
|
}
|
|
293
293
|
return lines
|
|
294
294
|
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Collection-declaration identity from a document the BACKEND produced (a pull, or a
|
|
299
|
+
* push response's `finalized[].document`): `{ <collection name>: <$uuid> }`.
|
|
300
|
+
*
|
|
301
|
+
* ⛔ The sibling of collectFolderItemUuids, and it exists for the same reason: these
|
|
302
|
+
* items have no file of their own, so nothing in a path-keyed map can hold their
|
|
303
|
+
* identity, and a push that omits it re-sends the whole section uuid-less. The
|
|
304
|
+
* backend refuses that rather than deleting every stored row — see collectionsNested.
|
|
305
|
+
*
|
|
306
|
+
* Keyed by `name`, which the backend enforces unique within the section. `$id` is
|
|
307
|
+
* deliberately not consulted: it is a payload-local handle the backend never stores.
|
|
308
|
+
*/
|
|
309
|
+
export function collectCollectionUuids(doc) {
|
|
310
|
+
const out = {}
|
|
311
|
+
for (const item of doc?.collections || []) {
|
|
312
|
+
const name = item?.name
|
|
313
|
+
const uuid = item?.$uuid
|
|
314
|
+
if (typeof name === 'string' && typeof uuid === 'string' && name && uuid) out[name] = uuid
|
|
315
|
+
}
|
|
316
|
+
return out
|
|
317
|
+
}
|
package/src/uwx/site-project.js
CHANGED
|
@@ -39,6 +39,7 @@ import { createHash } from 'node:crypto'
|
|
|
39
39
|
import yaml from 'js-yaml'
|
|
40
40
|
import { writeSiteConfig, writeThemeFile, writeIfChanged, writeSectionFile, writeMergedYaml } from './project-writer.js'
|
|
41
41
|
import { declarationsToCollectionsYml } from './collections-project.js'
|
|
42
|
+
import { authorableFetch } from '../site/fetch-shapes.js'
|
|
42
43
|
import { createTranslationCollector, writeLocaleTranslations, writeFreeformTranslations, unwrapLocalizedContent } from './locale-sync.js'
|
|
43
44
|
import { buildFreeformPath } from '../i18n/freeform.js'
|
|
44
45
|
import { unwrapLocalized, unwrapLocalizedList } from './backfill.js'
|
|
@@ -303,7 +304,8 @@ export function sectionRecordToFile({ filePath, record, sourceLocale = LOCALIZED
|
|
|
303
304
|
if (theme_override !== undefined) frontmatter.theme = theme_override
|
|
304
305
|
if (preset !== undefined) frontmatter.preset = preset
|
|
305
306
|
if (input !== undefined) frontmatter.input = input
|
|
306
|
-
|
|
307
|
+
// Invert the build's resolution rather than copy it — see fetch-shapes.js.
|
|
308
|
+
if (fetch !== undefined) frontmatter.fetch = authorableFetch(fetch)
|
|
307
309
|
if (stable_id !== undefined) frontmatter.id = stable_id
|
|
308
310
|
|
|
309
311
|
const body = insets ? reinlineInsets(sourceContent, insets) : sourceContent
|
|
@@ -424,7 +426,8 @@ function pageRecordToYml(record, sectionsArray, sourceLocale) {
|
|
|
424
426
|
if (record.rewrite !== undefined) y.rewrite = record.rewrite
|
|
425
427
|
if (record.layout !== undefined) y.layout = record.layout
|
|
426
428
|
if (record.seo !== undefined) y.seo = record.seo
|
|
427
|
-
|
|
429
|
+
// Invert the build's resolution rather than copy it — see fetch-shapes.js.
|
|
430
|
+
if (record.fetch !== undefined) y.fetch = authorableFetch(record.fetch)
|
|
428
431
|
// `sections:` exists to preserve ORDER and NESTING, which the projected filenames
|
|
429
432
|
// can't carry (they're `<stableId>.md`, with no numeric prefix). It must not also
|
|
430
433
|
// decide MEMBERSHIP — and a bare list does: the collector reads a list without
|
package/src/uwx/site.js
CHANGED
|
@@ -639,7 +639,39 @@ export function isSiteRelativeExtensionUrl(decl) {
|
|
|
639
639
|
// The collection DECLARATIONS carried inside site-content `info`-adjacent metadata.
|
|
640
640
|
// Merges the co-located `collections.yml` (the home for file-based decls) over the
|
|
641
641
|
// legacy `site.yml::collections` (kept for remote `url:` sources + back-compat).
|
|
642
|
-
|
|
642
|
+
/**
|
|
643
|
+
* The site's collection DECLARATIONS as `$`-records.
|
|
644
|
+
*
|
|
645
|
+
* ⛔ IDENTITY IS KEYED BY `name`, AND IT HAS TO BE.
|
|
646
|
+
*
|
|
647
|
+
* Every other item on this entity gets its `$uuid` from a map keyed by the file it
|
|
648
|
+
* was projected to — but a collection declaration has no file of its own: they all
|
|
649
|
+
* come from one `collections/collections.yml`. So a path-keyed map has no shape a
|
|
650
|
+
* declaration could occupy, nothing is ever recorded for one, and every push re-sent
|
|
651
|
+
* this whole section uuid-less. The backend refuses that (an all-blank section over
|
|
652
|
+
* stored items would delete every stored row), which is why `push` worked once and
|
|
653
|
+
* every push after it was refused. Measured 2026-08-29; collab framework-backend-812b.
|
|
654
|
+
*
|
|
655
|
+
* ⭐ `name` is the right key and not merely the available one — the backend enforces
|
|
656
|
+
* `unique_field(name, scope: section)` on this section, and it is the join key its
|
|
657
|
+
* `resolve_collection_model` matches and the `/data/{name}.json` serve segment. An
|
|
658
|
+
* author-facing rename is `label`, so the key does not move under either lane.
|
|
659
|
+
*
|
|
660
|
+
* ⛔ NOT `$id`, though it happens to hold the same string. `$id` is a payload-local
|
|
661
|
+
* handle the backend skips on parse and never stores, correlated by submission index
|
|
662
|
+
* rather than by value — keying on it would key on something that exists only inside
|
|
663
|
+
* our own outgoing document.
|
|
664
|
+
*
|
|
665
|
+
* ⚠️ Because `name` IS the identity, renaming a collection is by design
|
|
666
|
+
* indistinguishable from delete-plus-create. Rename one of two and the mix passes;
|
|
667
|
+
* rename every collection at once and the section goes all-blank and is refused.
|
|
668
|
+
* That is the semantics, not a defect.
|
|
669
|
+
*
|
|
670
|
+
* @param {object} declarations resolved collection declarations, keyed by name
|
|
671
|
+
* @param {Object<string,string>} [uuids] `name` → backend `$uuid`, from a push
|
|
672
|
+
* response or a pull. Absent on a first sync, where minting is correct.
|
|
673
|
+
*/
|
|
674
|
+
function collectionsNested(declarations, uuids = null) {
|
|
643
675
|
const out = []
|
|
644
676
|
for (const [name, d] of Object.entries(declarations)) {
|
|
645
677
|
const data = {}
|
|
@@ -655,7 +687,10 @@ function collectionsNested(declarations) {
|
|
|
655
687
|
setIf(data, 'deferred', d.deferred)
|
|
656
688
|
setIf(data, 'detail_url', d.detailUrl)
|
|
657
689
|
setIf(data, 'queryable', d.queryable)
|
|
658
|
-
|
|
690
|
+
const rec = withIdentity(name, { name, ...data })
|
|
691
|
+
const uuid = uuids?.[name]
|
|
692
|
+
if (typeof uuid === 'string' && uuid) rec.$uuid = uuid
|
|
693
|
+
out.push(rec)
|
|
659
694
|
}
|
|
660
695
|
return out
|
|
661
696
|
}
|
|
@@ -860,7 +895,7 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
860
895
|
doc.pages = pages
|
|
861
896
|
doc.layout_sections = layoutSections
|
|
862
897
|
doc.extensions = extensionsNested(siteYml)
|
|
863
|
-
doc.collections = collectionsNested(colConfig.declarations)
|
|
898
|
+
doc.collections = collectionsNested(colConfig.declarations, opts.collectionUuids)
|
|
864
899
|
return doc
|
|
865
900
|
}
|
|
866
901
|
|
package/src/uwx/sync-package.js
CHANGED
|
@@ -285,7 +285,14 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
285
285
|
...(opts.folderItemUuids ? { itemUuids: opts.folderItemUuids } : {}),
|
|
286
286
|
})
|
|
287
287
|
|
|
288
|
-
|
|
288
|
+
// `collectionUuids` — identity for the `collections` section, keyed by collection
|
|
289
|
+
// NAME because a declaration has no file of its own (see collectionsNested).
|
|
290
|
+
const siteDoc = includeSite
|
|
291
|
+
? await siteProjectToDocument(siteRoot, {
|
|
292
|
+
sourceLocale,
|
|
293
|
+
...(opts.collectionUuids ? { collectionUuids: opts.collectionUuids } : {})
|
|
294
|
+
})
|
|
295
|
+
: null
|
|
289
296
|
// Deploy-derived `info` fields (e.g. `data_bundle`, the static-data ball URL) are
|
|
290
297
|
// stamped here — NOT authored in site.yml, so they ride the wire but never project
|
|
291
298
|
// back on pull (the `info.assets` precedent). They are part of the hashed content,
|