@uniweb/build 0.44.4 → 0.45.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/prerender.js +221 -68
- package/src/site/content-collector.js +68 -27
- package/src/site/data-fetcher.js +49 -5
- package/src/site/query-processor.js +33 -4
- package/src/uwx/asset-map.js +74 -2
- package/src/uwx/emit-surface.json +6 -0
- package/src/uwx/index.js +2 -1
- package/src/uwx/records-project.js +21 -26
- package/src/uwx/records.js +6 -7
- package/src/uwx/self-scope.js +78 -0
- package/src/uwx/site-project.js +13 -5
- package/src/uwx/site.js +65 -9
- package/src/uwx/sync-package.js +24 -9
- package/src/uwx/yaml-upsert.js +26 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// `@/x` — a Model ref in the site's own foundation scope — and its qualified form.
|
|
2
|
+
//
|
|
3
|
+
// `@/member` names a data schema in the foundation's own `schemas/`. It is
|
|
4
|
+
// AUTHORING shorthand: a backend resolves Models by name and never mints one, so a
|
|
5
|
+
// ref that leaves the CLI must be the name the Model was stored under,
|
|
6
|
+
// `@<org>/member`. Everything that reads a ref off the wire matches it exactly —
|
|
7
|
+
// the entity store at restore, and a records service answering a hosted page's
|
|
8
|
+
// query.
|
|
9
|
+
//
|
|
10
|
+
// ⭐ ONE RULE FOR EVERY PATH THAT SHIPS A REF FROM ONE PUBLISH, and its inverse:
|
|
11
|
+
//
|
|
12
|
+
// - a record's `$model` `records.js::buildRecordEntities`
|
|
13
|
+
// - a query's `schema` `site.js::queriesNested` (the `queries` Section)
|
|
14
|
+
// - the author's spelling, on pull `records-project.js` (placement and declarations)
|
|
15
|
+
//
|
|
16
|
+
// ⛔ They must agree on one alias. When the query path shipped `@/member` verbatim
|
|
17
|
+
// while the records beside it were qualified, the query named a Model its own
|
|
18
|
+
// records were not stored under: a hosted page's question named a Model that does
|
|
19
|
+
// not exist, the records service refused that key, and the section rendered
|
|
20
|
+
// nothing — the key absent from `content.data`, the reason only on
|
|
21
|
+
// `block.dataError`, the console clean. Nothing on either side was malformed; the
|
|
22
|
+
// two paths disagreed.
|
|
23
|
+
//
|
|
24
|
+
// Registering a foundation qualifies its declarations separately, from the
|
|
25
|
+
// foundation's publish scope (`registry-package.js`). That is a different input
|
|
26
|
+
// (the foundation's scope, not the site's publish org) and stays there.
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An org handle as `--org` or `site.yml::$org` gives it — `@acme`, `acme`, or
|
|
30
|
+
* `@acme/…` — reduced to the bare handle, or `''`.
|
|
31
|
+
*
|
|
32
|
+
* @param {unknown} org
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function bareOrg(org) {
|
|
36
|
+
return typeof org === 'string' ? org.replace(/^@/, '').replace(/\/.*$/, '') : ''
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `@/x` → `@<org>/x`. Any other ref (`@std/x`, `@acme/x`) passes through, and so
|
|
41
|
+
* does `@/x` when no org is known — callers that ship an unresolved alias say so
|
|
42
|
+
* themselves (`buildRecordEntities` warns per query).
|
|
43
|
+
*
|
|
44
|
+
* @param {unknown} ref
|
|
45
|
+
* @param {unknown} org
|
|
46
|
+
* @returns {unknown}
|
|
47
|
+
*/
|
|
48
|
+
export function resolveSelfScope(ref, org) {
|
|
49
|
+
const handle = bareOrg(org)
|
|
50
|
+
return typeof ref === 'string' && ref.startsWith('@/') && handle
|
|
51
|
+
? `@${handle}/${ref.slice(2)}`
|
|
52
|
+
: ref
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The inverse, for a pull: `@<org>/x` → `@/x` for the site's own org.
|
|
57
|
+
*
|
|
58
|
+
* ⛔ WITHOUT THIS THE ROUND TRIP IS NOT A FIXED POINT, and the failure is silent
|
|
59
|
+
* on both ends. A record authored under `entities/article/` comes back as
|
|
60
|
+
* `@acme/article` and, placed literally, lands under `entities/acme/article/` — a
|
|
61
|
+
* different schema folder, which the next build reads as a different schema. A
|
|
62
|
+
* query declared `schema: '@/member'` comes back as `@acme/member`, and one that
|
|
63
|
+
* relied on the query-name default comes back with an explicit schema it never
|
|
64
|
+
* had.
|
|
65
|
+
*
|
|
66
|
+
* ⭐ The site records its own org at create (`site.yml::$org` — "whose this is"),
|
|
67
|
+
* which is exactly the inverse. A model scoped to ANOTHER org is left alone: it
|
|
68
|
+
* genuinely is that org's, and `@/` would be a lie.
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} ref
|
|
71
|
+
* @param {unknown} org
|
|
72
|
+
* @returns {unknown}
|
|
73
|
+
*/
|
|
74
|
+
export function unresolveSelfScope(ref, org) {
|
|
75
|
+
const handle = bareOrg(org)
|
|
76
|
+
if (typeof ref !== 'string' || !handle) return ref
|
|
77
|
+
return ref.startsWith(`@${handle}/`) ? `@/${ref.slice(handle.length + 2)}` : ref
|
|
78
|
+
}
|
package/src/uwx/site-project.js
CHANGED
|
@@ -144,11 +144,19 @@ const INFO_TO_SITE_YML = {
|
|
|
144
144
|
// ⭐ `tags` — authored, non-localized tokens; the filter facet for a list of site
|
|
145
145
|
// cards. Round-trips verbatim like any authored list.
|
|
146
146
|
tags: 'tags',
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
147
|
+
// ⭐ `preview` — the card image, verbatim: the app's timestamp, an author's URL, or
|
|
148
|
+
// the path an author wrote for an image in the project, which `restoreAssetRefs`
|
|
149
|
+
// has already put back by recognizing the serve URL the push recorded in
|
|
150
|
+
// `assets.json` (asset-map.js). [Diego, 2026-09-10]
|
|
151
|
+
preview: 'preview',
|
|
152
|
+
// ⭐ `url` — where the site is live → `site.yml::$url`. The backend records it at
|
|
153
|
+
// every publish, and pull is its only way onto disk; recorded rather than authored,
|
|
154
|
+
// so it lands under the `$` like `$uuid`.
|
|
155
|
+
//
|
|
156
|
+
// ⚠️ Both were listed here as "DELIBERATELY ABSENT… BACKEND-STAMPED" until
|
|
157
|
+
// 2026-09-10. Leaving an app-written field out of this map is what lets the next
|
|
158
|
+
// push destroy it, since `info` is replaced whole.
|
|
159
|
+
url: '$url',
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
// ── `settings` Section → site.yml ─────────────────────────────────────────────
|
package/src/uwx/site.js
CHANGED
|
@@ -53,7 +53,9 @@ import {
|
|
|
53
53
|
applyWildcardOrder,
|
|
54
54
|
processMarkdownFile,
|
|
55
55
|
fetchFromDataShorthand,
|
|
56
|
+
assertRouteFolder,
|
|
56
57
|
} from '../site/content-collector.js'
|
|
58
|
+
import { refuseUnder } from '../site/data-fetcher.js'
|
|
57
59
|
import { normalizeHideIn } from '../site/nav-visibility.js'
|
|
58
60
|
import { resolveDefaultLocale, validateLanguageConfig, queryDataUrl } from '@uniweb/core'
|
|
59
61
|
import { emitEntitySyncPackage } from './entity-document.js'
|
|
@@ -62,6 +64,7 @@ import { unwrapLocalized } from './backfill.js'
|
|
|
62
64
|
import { loadFreeformTranslation } from '../i18n/freeform.js'
|
|
63
65
|
import { upsertYamlScalar } from './yaml-upsert.js'
|
|
64
66
|
import { resolveQueriesConfig } from './queries-config.js'
|
|
67
|
+
import { resolveSelfScope } from './self-scope.js'
|
|
65
68
|
|
|
66
69
|
const SITE_ENTITY_KEY = 'site-content' // one content entity per site project
|
|
67
70
|
|
|
@@ -69,6 +72,16 @@ function setIf(obj, key, value) {
|
|
|
69
72
|
if (value !== undefined) obj[key] = value
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
// A YAML scalar the author may have written unquoted: `20260910` loads as a number
|
|
76
|
+
// and `2026-09-10` as a Date. Carry either as the string it stands for; anything
|
|
77
|
+
// else that is not a string is not a value a string field can hold.
|
|
78
|
+
function scalarString(value) {
|
|
79
|
+
if (typeof value === 'string') return value
|
|
80
|
+
if (typeof value === 'number') return String(value)
|
|
81
|
+
if (value instanceof Date) return value.toISOString()
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
// Credential-shaped keys, mirroring the set the delivery edge strips on the
|
|
73
86
|
// reading side. Deliberately the SAME list rather than a stricter one, so the
|
|
74
87
|
// two guards are visibly twins and a key added to one is obviously owed to the
|
|
@@ -225,6 +238,10 @@ function buildPageData(config, ctx) {
|
|
|
225
238
|
// 2026-09-02 this kept `[0]` and dropped the rest silently, so the wire
|
|
226
239
|
// carried one dataset for a page that asked for several.
|
|
227
240
|
let fetch = config.fetch ?? fetchFromDataShorthand(config.data)
|
|
241
|
+
// `where: { path: { under } }` is refused here as the build refuses it
|
|
242
|
+
// (`parseFetchConfig`): a site that cannot build must not sync either. A
|
|
243
|
+
// section's fetch is refused where the collector parses it.
|
|
244
|
+
for (const one of [fetch].flat()) refuseUnder(one?.where, 'fetch')
|
|
228
245
|
// Resolve the authored `query:` shorthand to the runtime-fetchable
|
|
229
246
|
// `path: /data/<name>.json` (the static convention the default-fetcher uses).
|
|
230
247
|
// A shell/backend-hosted site renders client-side with NO prerender, so the
|
|
@@ -537,8 +554,13 @@ async function walkPagesNested(ctx, dirPath, parentSlugPath, inheritedMode, pare
|
|
|
537
554
|
const { siteRoot, siteIndex, sourceLocale, translations } = ctx
|
|
538
555
|
const folders = await orderedSubfolders(dirPath, inheritedMode, parentConfig)
|
|
539
556
|
const out = []
|
|
557
|
+
// A folder inside a `[...path]` folder can never be reached, and `[dir]` /
|
|
558
|
+
// `[path]` would name a route variable — refused here as the collector refuses
|
|
559
|
+
// them, so a site that cannot build cannot sync either (ruled 2026-09-11).
|
|
560
|
+
const insideCatchAll = (parentSlugPath || '').split('/').includes(CATCH_ALL_MARKER)
|
|
540
561
|
for (let i = 0; i < folders.length; i++) {
|
|
541
562
|
const f = folders[i]
|
|
563
|
+
assertRouteFolder(f.dirName, insideCatchAll ? '/:path*' : '/')
|
|
542
564
|
const dyn = f.dirName.match(DYNAMIC_RE)
|
|
543
565
|
const slug = dyn ? dyn[1] : f.name
|
|
544
566
|
const mode = f.source === 'folder.yml' ? 'folder' : 'page'
|
|
@@ -731,6 +753,9 @@ export function isSiteRelativeExtensionUrl(decl) {
|
|
|
731
753
|
* @param {object} declarations resolved collection declarations, keyed by name
|
|
732
754
|
* @param {Object<string,string>} [uuids] `name` → backend `$uuid`, from a push
|
|
733
755
|
* response or a pull. Absent on a first sync, where minting is correct.
|
|
756
|
+
* @param {string} [org] the publish org. A foundation-relative `schema` (`@/x`)
|
|
757
|
+
* is qualified with it (`./self-scope.js`), exactly as the records' `$model`
|
|
758
|
+
* is — see the note at the `schema` line below.
|
|
734
759
|
*/
|
|
735
760
|
// ⛔ KEYS THAT MUST NOT REACH THE WIRE. Everything else on an authored declaration
|
|
736
761
|
// is emitted, including fields this build does not model — see the note in
|
|
@@ -786,13 +811,20 @@ const DECL_NOT_ON_WIRE = new Set([
|
|
|
786
811
|
'filter'
|
|
787
812
|
])
|
|
788
813
|
|
|
789
|
-
function queriesNested(declarations, uuids = null) {
|
|
814
|
+
function queriesNested(declarations, uuids = null, org = null) {
|
|
790
815
|
const out = []
|
|
791
816
|
for (const [name, d] of Object.entries(declarations)) {
|
|
817
|
+
refuseUnder(d.where, `queries.${name}`)
|
|
792
818
|
const data = {}
|
|
793
819
|
const source = d.path ? { path: d.path } : d.url ? { url: d.url } : d.source
|
|
794
820
|
setIf(data, 'source', source)
|
|
795
|
-
|
|
821
|
+
// ⛔ QUALIFIED, WITH THE SAME RULE AND THE SAME ORG AS THE RECORDS' `$model`
|
|
822
|
+
// (`records.js::buildRecordEntities`). A consumer answers a query by matching
|
|
823
|
+
// this name against the Models its records were stored under, so a verbatim
|
|
824
|
+
// `@/member` beside records stored as `@org/member` names nothing: the query
|
|
825
|
+
// resolves no Model and the page that binds it renders empty. The pull puts the
|
|
826
|
+
// author's `@/` back (`records-project.js::declarationsToQueriesYml`).
|
|
827
|
+
setIf(data, 'schema', resolveSelfScope(d.schema, org))
|
|
796
828
|
setIf(data, 'sort', d.sort)
|
|
797
829
|
// Legacy `filter:` is not synced — it is translated to `where` upstream
|
|
798
830
|
// (the canonical predicate). No legacy fields on the wire.
|
|
@@ -1021,7 +1053,7 @@ function settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translation
|
|
|
1021
1053
|
// `ogTitle`, `ogDescription`, `noindex`, `canonical`, `changefreq`, `priority`
|
|
1022
1054
|
// (`core/src/seo.js`). Two of those are literally sitemap.xml columns. It only
|
|
1023
1055
|
// ever passed the card test because `image` was inside it; the card's picture is
|
|
1024
|
-
// `info.
|
|
1056
|
+
// `info.preview` now.
|
|
1025
1057
|
setIf(settings, 'seo', siteYml.seo)
|
|
1026
1058
|
// ⭐ `keywords` IS seo by function — it renders into `<meta name="keywords">`
|
|
1027
1059
|
// (`runtime/src/ssr-renderer.js`). It is top-level in site.yml for authoring
|
|
@@ -1051,6 +1083,10 @@ function settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translation
|
|
|
1051
1083
|
// ⭐ The site-level fetch, DESUGARED and under its real name. `data:` is the
|
|
1052
1084
|
// authoring shorthand for `fetch:` and every other tier already calls the wire
|
|
1053
1085
|
// field `fetch`; the site tier called it `data` until 2026-09-09.
|
|
1086
|
+
// `under` is refused as the build refuses it; the `data:` shorthand carries no
|
|
1087
|
+
// `where`. ⚠️ The source expression stays inside `setIf`: `gen-emit-surface.mjs`
|
|
1088
|
+
// reads the published key's sources off it.
|
|
1089
|
+
for (const one of [siteYml.fetch].flat()) refuseUnder(one?.where, 'site.yml fetch')
|
|
1054
1090
|
setIf(settings, 'fetch', siteYml.fetch ?? fetchFromDataShorthand(siteYml.data))
|
|
1055
1091
|
|
|
1056
1092
|
// ⭐ The SITE TIER of framework's own `{name, hide, params}` layout object, which
|
|
@@ -1076,6 +1112,9 @@ function settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translation
|
|
|
1076
1112
|
* @param {string} [opts.sourceLocale] - localized-field wrap locale. Defaults to
|
|
1077
1113
|
* the site's effective default locale (`defaultLanguage || languages[0] ||
|
|
1078
1114
|
* 'en'` — the shared `resolveDefaultLocale` rule), NOT a bare 'en'.
|
|
1115
|
+
* @param {string} [opts.org] - the publish org, which qualifies a query's
|
|
1116
|
+
* foundation-relative `schema` (`@/x` → `@org/x`). Pass the same org the
|
|
1117
|
+
* records are emitted with; absent, `@/x` ships as written.
|
|
1079
1118
|
* @returns {Promise<object>} the section-keyed `$`-document:
|
|
1080
1119
|
* `{ $uuid?, $id, $model, info, pages, layout_sections, extensions, queries }`
|
|
1081
1120
|
*/
|
|
@@ -1271,11 +1310,28 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1271
1310
|
// over an already-fetched list and never asks the database, while a DB-filterable
|
|
1272
1311
|
// facet is separately useful — only the second needs a predicable brief field.
|
|
1273
1312
|
setIf(info, 'tags', siteYml.tags)
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1313
|
+
// ⭐ `preview` — the site card's image. ONE field, TWO writers, and it round-trips
|
|
1314
|
+
// whichever wrote it [Diego, 2026-09-10]:
|
|
1315
|
+
// · the APP, when it generates a card image — a timestamp, carried verbatim;
|
|
1316
|
+
// · an AUTHOR, deliberately — typically a template site's custom image: a URL, or
|
|
1317
|
+
// a site-root path to an image in the project, which push uploads like any
|
|
1318
|
+
// content image and sends as its serve URL, and which pull puts back as the
|
|
1319
|
+
// path the author wrote (`restoreAssetRefs` recognizes the URL by the
|
|
1320
|
+
// fingerprint `assets.json` recorded for it).
|
|
1321
|
+
// The app leaves an author's value alone.
|
|
1322
|
+
//
|
|
1323
|
+
// ⚠️ This read "`url` and `preview_image` are BACKEND-STAMPED and framework emits
|
|
1324
|
+
// NEITHER" until 2026-09-10. Neither half held: nothing stamped `url`, and leaving
|
|
1325
|
+
// an app-written field off an allowlist destroys it on every push, because `info`
|
|
1326
|
+
// is replaced whole.
|
|
1327
|
+
setIf(info, 'preview', scalarString(siteYml.preview))
|
|
1328
|
+
// ⭐ `url` — where the site is live, so a site card can link to it without opening
|
|
1329
|
+
// the editor. The backend records it at every publish (stated by backend,
|
|
1330
|
+
// 2026-09-10) and pull brings it into `site.yml::$url`; nothing in framework writes
|
|
1331
|
+
// it. The `$` marks it as recorded rather than authored, like `$uuid` — and keeps it
|
|
1332
|
+
// out of the rendered payload, where a bare `url:` would sit beside `seo.baseUrl`,
|
|
1333
|
+
// the authored canonical address.
|
|
1334
|
+
setIf(info, 'url', siteYml.$url)
|
|
1279
1335
|
|
|
1280
1336
|
const ctx = { siteRoot, siteIndex: siteYml.index, sourceLocale, translations }
|
|
1281
1337
|
const pagesPath = siteYml.paths?.pages
|
|
@@ -1327,7 +1383,7 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1327
1383
|
//
|
|
1328
1384
|
// ⚠️ `queriesNested` keeps its name. §2's rule: rename what an author or a
|
|
1329
1385
|
// consumer sees, leave the identifier alone.
|
|
1330
|
-
doc.queries = queriesNested(colConfig.declarations, opts.queryUuids)
|
|
1386
|
+
doc.queries = queriesNested(colConfig.declarations, opts.queryUuids, opts.org)
|
|
1331
1387
|
// Emitted ONLY when the file declares the key — see the header above
|
|
1332
1388
|
// `serviceRecords`: on a replaced Section, absent and empty are different
|
|
1333
1389
|
// requests and one of them is destructive.
|
package/src/uwx/sync-package.js
CHANGED
|
@@ -126,10 +126,13 @@ function walkEntityAssets(node, visitor) {
|
|
|
126
126
|
//
|
|
127
127
|
// ⚠️ **This bullet used to justify itself with "no deployment emits
|
|
128
128
|
// `config.assets.url` yet". FALSIFIED 2026-08-18** by the backend lane:
|
|
129
|
-
// `serve` publishes the pattern **unconditionally**, falling back to
|
|
130
|
-
// direct form, so a deployment with no asset storage emits
|
|
131
|
-
//
|
|
132
|
-
//
|
|
129
|
+
// `serve` publishes the pattern **unconditionally**, falling back to a
|
|
130
|
+
// direct form, so a deployment with no asset storage emits a pattern rather
|
|
131
|
+
// than nothing (measured by them on a running daemon, not read off a type).
|
|
132
|
+
// ⛔ This named that direct form `/gateway/asset/dist/{id}/base.{ext}` until
|
|
133
|
+
// 2026-09-10, and there is no such route [Diego] — the name had spread into
|
|
134
|
+
// tests and kb as if it were THE serve URL. A serve URL is whatever the host
|
|
135
|
+
// returns; nothing here depends on its shape. ⛔ Scoped to deployments
|
|
133
136
|
// running code from 2026-08-17 or later; an older one emits nothing, and
|
|
134
137
|
// absent stays absent. A present-tense negative about someone else's
|
|
135
138
|
// deployments is the claim nothing in this repo can ever contradict — it
|
|
@@ -157,19 +160,22 @@ function walkEntityAssets(node, visitor) {
|
|
|
157
160
|
// ProseMirror image node's attrs (`{src, alt, …}`) and a section background's
|
|
158
161
|
// media object (`{image: {src}}`) with one rule — the two shapes framework
|
|
159
162
|
// resolves, reached through the same walk.
|
|
160
|
-
|
|
163
|
+
//
|
|
164
|
+
// `noStamp` is the one object that must get NO identity attrs even when a slot
|
|
165
|
+
// matches — the site's `info`, whose fields the host declares (see the call site).
|
|
166
|
+
function rewriteEntityAssets(node, map, ids, noStamp = null) {
|
|
161
167
|
if (Array.isArray(node)) {
|
|
162
168
|
for (let i = 0; i < node.length; i++) {
|
|
163
169
|
const v = node[i]
|
|
164
170
|
if (typeof v === 'string') { if (map[v]) node[i] = map[v] }
|
|
165
|
-
else rewriteEntityAssets(v, map, ids)
|
|
171
|
+
else rewriteEntityAssets(v, map, ids, noStamp)
|
|
166
172
|
}
|
|
167
173
|
return node
|
|
168
174
|
}
|
|
169
175
|
if (node && typeof node === 'object') {
|
|
170
176
|
// Stamp BEFORE the string swap below, while the reference is still the
|
|
171
177
|
// local ref the ids map is keyed by.
|
|
172
|
-
if (ids) {
|
|
178
|
+
if (ids && node !== noStamp) {
|
|
173
179
|
// Every asset slot, not just the primary: a video's `poster` and a
|
|
174
180
|
// document's `preview` are assets like any other, and each has identity
|
|
175
181
|
// attrs naming which reference they belong to (ASSET_SLOTS).
|
|
@@ -187,7 +193,7 @@ function rewriteEntityAssets(node, map, ids) {
|
|
|
187
193
|
for (const key of Object.keys(node)) {
|
|
188
194
|
const v = node[key]
|
|
189
195
|
if (typeof v === 'string') { if (map[v]) node[key] = map[v] }
|
|
190
|
-
else rewriteEntityAssets(v, map, ids)
|
|
196
|
+
else rewriteEntityAssets(v, map, ids, noStamp)
|
|
191
197
|
}
|
|
192
198
|
}
|
|
193
199
|
return node
|
|
@@ -311,6 +317,10 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
311
317
|
? await siteProjectToDocument(siteRoot, {
|
|
312
318
|
sourceLocale,
|
|
313
319
|
...(opts.queryUuids ? { queryUuids: opts.queryUuids } : {}),
|
|
320
|
+
// ⛔ THE SAME ORG `buildRecordEntities` WAS GIVEN ABOVE. A query's `schema`
|
|
321
|
+
// must name the Model its records are stored under, and both are qualified
|
|
322
|
+
// from one `@/x` by one rule (`./self-scope.js`) — so they take one org.
|
|
323
|
+
...(opts.org ? { org: opts.org } : {}),
|
|
314
324
|
// Withhold the `$services`/`$secrets` Sections when the caller has
|
|
315
325
|
// determined the file is not asking for anything new by them. Passed
|
|
316
326
|
// through rather than decided here: the last-agreed state is project
|
|
@@ -387,7 +397,12 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
387
397
|
const assetIds =
|
|
388
398
|
opts.assetIds && typeof opts.assetIds === 'object' ? opts.assetIds : null
|
|
389
399
|
if (assetRewrite) {
|
|
390
|
-
|
|
400
|
+
// ⛔ No identity attrs on `info`: it is a Section whose fields the host declares,
|
|
401
|
+
// and `preview` is a real ASSET_SLOTS slot, so stamping would add
|
|
402
|
+
// `previewAssetId`/`previewAssetExt` — fields the host refuses. Its bare strings
|
|
403
|
+
// are still swapped for their serve URLs, which `assets.json` recognizes on pull
|
|
404
|
+
// by fingerprint (asset-map.js → `servedFingerprint`).
|
|
405
|
+
if (siteDoc) rewriteEntityAssets(siteDoc, assetRewrite, assetIds, siteDoc.info)
|
|
391
406
|
for (const e of col.entities) rewriteEntityAssets(e.document, assetRewrite, assetIds)
|
|
392
407
|
}
|
|
393
408
|
// Collect the site-root local refs the deploy must upload (`/images/x.png`).
|
package/src/uwx/yaml-upsert.js
CHANGED
|
@@ -51,3 +51,29 @@ export function upsertYamlScalar(filePath, key, value) {
|
|
|
51
51
|
writeFileSync(filePath, next)
|
|
52
52
|
return true
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Remove a TOP-LEVEL scalar key from the YAML file at `filePath`, preserving every
|
|
57
|
+
* other line (comments included) — the inverse of `upsertYamlScalar`, with the same
|
|
58
|
+
* scope: one `key: value` line at column 0. A key followed by indented lines, or
|
|
59
|
+
* opening a block scalar (`|` / `>`), is not a one-line scalar and is left alone
|
|
60
|
+
* rather than half-removed.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} filePath
|
|
63
|
+
* @param {string} key - a top-level scalar key (e.g. `$url`)
|
|
64
|
+
* @returns {boolean} true if the file changed
|
|
65
|
+
*/
|
|
66
|
+
export function removeYamlScalar(filePath, key) {
|
|
67
|
+
if (!existsSync(filePath)) return false
|
|
68
|
+
const lines = readFileSync(filePath, 'utf8').split('\n')
|
|
69
|
+
const esc = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
70
|
+
const re = new RegExp(`^${esc}:(.*)$`)
|
|
71
|
+
const at = lines.findIndex((l) => re.test(l))
|
|
72
|
+
if (at === -1) return false
|
|
73
|
+
const inline = lines[at].replace(re, '$1').trim()
|
|
74
|
+
const continues = at + 1 < lines.length && /^[ \t]+\S/.test(lines[at + 1])
|
|
75
|
+
if (continues || /^[|>]/.test(inline)) return false
|
|
76
|
+
lines.splice(at, 1)
|
|
77
|
+
writeFileSync(filePath, lines.join('\n'))
|
|
78
|
+
return true
|
|
79
|
+
}
|