@uniweb/build 0.43.0 → 0.43.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 +7 -7
- package/src/schema.js +19 -6
- package/src/site/content-collector.js +36 -4
- package/src/uwx/site-project.js +25 -0
- package/src/uwx/site.js +77 -2
- package/src/uwx/sync-package.js +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.1",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,14 +57,14 @@
|
|
|
57
57
|
"sharp": "^0.35.3",
|
|
58
58
|
"yaml": "^2.5.0",
|
|
59
59
|
"@uniweb/content-reader": "^1.2.4",
|
|
60
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
61
|
-
"@uniweb/projections": "^0.5.13",
|
|
62
|
-
"@uniweb/semantic-parser": "^1.4.0",
|
|
63
60
|
"@uniweb/schemas": "^0.2.13",
|
|
64
|
-
"@uniweb/theming": "^0.1.15"
|
|
61
|
+
"@uniweb/theming": "^0.1.15",
|
|
62
|
+
"@uniweb/projections": "^0.5.13",
|
|
63
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
64
|
+
"@uniweb/semantic-parser": "^1.4.0"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"@uniweb/runtime": "^0.19.
|
|
67
|
+
"@uniweb/runtime": "^0.19.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@tailwindcss/vite": "^4.0.0",
|
|
74
74
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
75
75
|
"vite-plugin-svgr": "^4.0.0",
|
|
76
|
-
"@uniweb/core": "^0.24.
|
|
76
|
+
"@uniweb/core": "^0.24.2"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|
|
79
79
|
"vite": {
|
package/src/schema.js
CHANGED
|
@@ -758,13 +758,26 @@ function reportSupports(srcDir, authored, derived, emitted) {
|
|
|
758
758
|
}
|
|
759
759
|
}
|
|
760
760
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
761
|
+
// ⛔ WARN WHENEVER THE DERIVATION WAS BLIND — not only when it came back empty.
|
|
762
|
+
//
|
|
763
|
+
// This used to require an empty result, which left the more likely case
|
|
764
|
+
// silent: a foundation that reaches `submit` through a literal and `booking`
|
|
765
|
+
// through a computed name publishes `["submit"]` — short, with no warning, and
|
|
766
|
+
// the one person who could fix it never hears about it.
|
|
767
|
+
//
|
|
768
|
+
// ⭐ That is what makes the lower bound tolerable. How often a foundation
|
|
769
|
+
// computes a service name is unknowable from here — foundations are
|
|
770
|
+
// third-party — but it does not need to be known, because the build detects
|
|
771
|
+
// its own blindness and can say so to the developer at the moment they build.
|
|
772
|
+
if (derived.blind) {
|
|
773
|
+
const empty = emitted === undefined || emitted.length === 0
|
|
765
774
|
console.warn(
|
|
766
|
-
|
|
767
|
-
`
|
|
775
|
+
empty
|
|
776
|
+
? `Warning: a service is resolved by a computed name, so \`uniweb.supports\` cannot be ` +
|
|
777
|
+
`derived and is left undeclared. List it in package.json to publish it.`
|
|
778
|
+
: `Warning: a service is resolved by a computed name, so the derived ` +
|
|
779
|
+
`\`uniweb.supports\` may be incomplete. Add any service missing from ` +
|
|
780
|
+
`[${(emitted || []).join(', ')}] to package.json.`,
|
|
768
781
|
)
|
|
769
782
|
for (const at of derived.blindAt || []) console.warn(` at ${at}`)
|
|
770
783
|
}
|
|
@@ -2095,11 +2095,19 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
2095
2095
|
/**
|
|
2096
2096
|
* Load foundation schema data needed by the content collector.
|
|
2097
2097
|
*
|
|
2098
|
+
* `hasContentHandler` reports whether the foundation declares `handlers.content`
|
|
2099
|
+
* — the hook that resolves `{…}` in page content. It is NOT used to decide
|
|
2100
|
+
* anything about the build; its only consumer is the `placeholders:` warning at
|
|
2101
|
+
* the call site, which needs to know whether a declared value has any reader.
|
|
2102
|
+
* ⚠️ It cannot tell WHICH engine the handler uses (a handler is a function, and
|
|
2103
|
+
* the build never calls it), so it answers "something could resolve this",
|
|
2104
|
+
* never "Loom will".
|
|
2105
|
+
*
|
|
2098
2106
|
* @param {string} foundationPath - Path to foundation directory
|
|
2099
|
-
* @returns {Promise<{ vars: Object, layoutNames: Set<string
|
|
2107
|
+
* @returns {Promise<{ vars: Object, layoutNames: Set<string>, hasContentHandler: boolean }>}
|
|
2100
2108
|
*/
|
|
2101
2109
|
export async function loadFoundationInfo(foundationPath) {
|
|
2102
|
-
if (!foundationPath) return { vars: {}, layoutNames: new Set() }
|
|
2110
|
+
if (!foundationPath) return { vars: {}, layoutNames: new Set(), hasContentHandler: false }
|
|
2103
2111
|
|
|
2104
2112
|
// ⛔ **NOT `dist/meta/schema.json`.** That file is the EDITOR's artifact — the
|
|
2105
2113
|
// rich per-section declaration a visual editor needs to render parameter forms
|
|
@@ -2133,9 +2141,13 @@ export async function loadFoundationInfo(foundationPath) {
|
|
|
2133
2141
|
// Two independent reads, so a failure in one does not cost the other. The
|
|
2134
2142
|
// previous single try/catch lost the layouts when only the config was broken.
|
|
2135
2143
|
let vars = {}
|
|
2144
|
+
let hasContentHandler = false
|
|
2136
2145
|
try {
|
|
2137
2146
|
const config = await loadFoundationConfig(srcDir)
|
|
2138
2147
|
vars = config?.vars || {}
|
|
2148
|
+
// `loadFoundationConfig` spreads the module's default export, so `handlers`
|
|
2149
|
+
// arrives intact even though it holds functions and never reaches schema.json.
|
|
2150
|
+
hasContentHandler = typeof config?.handlers?.content === 'function'
|
|
2139
2151
|
} catch (err) {
|
|
2140
2152
|
console.warn(
|
|
2141
2153
|
`[content-collector] Could not read the foundation's declared theme vars from ${srcDir}: ${err.message}\n` +
|
|
@@ -2154,7 +2166,7 @@ export async function loadFoundationInfo(foundationPath) {
|
|
|
2154
2166
|
)
|
|
2155
2167
|
}
|
|
2156
2168
|
|
|
2157
|
-
return { vars, layoutNames }
|
|
2169
|
+
return { vars, layoutNames, hasContentHandler }
|
|
2158
2170
|
}
|
|
2159
2171
|
|
|
2160
2172
|
/**
|
|
@@ -2372,7 +2384,27 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2372
2384
|
const rawThemeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
|
|
2373
2385
|
|
|
2374
2386
|
// Load foundation info (vars + layout names) and process theme
|
|
2375
|
-
const { vars: foundationVars, layoutNames: layoutNames } =
|
|
2387
|
+
const { vars: foundationVars, layoutNames: layoutNames, hasContentHandler } =
|
|
2388
|
+
await loadFoundationInfo(foundationPath)
|
|
2389
|
+
|
|
2390
|
+
// ⭐ `placeholders:` IS DECLARED FOR A READER THAT MAY NOT EXIST, and that is
|
|
2391
|
+
// the one way this feature fails. Resolving `{…}` in page content is a
|
|
2392
|
+
// FOUNDATION capability (`handlers.content`, normally @uniweb/loom), not
|
|
2393
|
+
// something the framework does for every site — so on a foundation that
|
|
2394
|
+
// declares no content handler the block is inert and the page renders the
|
|
2395
|
+
// literal `{vendor.email}`. That reads as an authoring typo, which is why it
|
|
2396
|
+
// is worth a build-time line rather than leaving the author to find it.
|
|
2397
|
+
//
|
|
2398
|
+
// ⚖️ A WARNING, never an error: the site may be mid-migration, or the author
|
|
2399
|
+
// may be about to switch foundations, and a declared-but-unread value harms
|
|
2400
|
+
// nothing. Same rule as the retired-`fetcher:` keys above — warn once, carry on.
|
|
2401
|
+
if (siteConfig.placeholders && !hasContentHandler) {
|
|
2402
|
+
console.warn(
|
|
2403
|
+
`[uniweb] site.yml declares \`placeholders:\` but the foundation has no \`handlers.content\`, ` +
|
|
2404
|
+
`so nothing will resolve them — pages will render the literal \`{name}\` text.\n` +
|
|
2405
|
+
`[uniweb] A foundation opts in with \`handlers: createLoomHandlers({ vars })\` from @uniweb/loom.`
|
|
2406
|
+
)
|
|
2407
|
+
}
|
|
2376
2408
|
// `base` reaches the theme because self-hosted font faces are authored
|
|
2377
2409
|
// root-relative (`/fonts/x.woff2`) and the emitted @font-face lives in an
|
|
2378
2410
|
// inline <style> — under a subdirectory deployment it must carry the base.
|
package/src/uwx/site-project.js
CHANGED
|
@@ -155,6 +155,23 @@ const INFO_TO_SITE_YML = {
|
|
|
155
155
|
seo: 'seo',
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// ── `config` Section → site.yml ───────────────────────────────────────────────
|
|
159
|
+
//
|
|
160
|
+
// The `config` Section (see `site.js::configNested`) carries authored
|
|
161
|
+
// configuration that does not belong on `info`. Each key maps to a
|
|
162
|
+
// top-level `site.yml` key of the same name, verbatim, so the author's file
|
|
163
|
+
// round-trips unchanged.
|
|
164
|
+
//
|
|
165
|
+
// ⛔ THIS MAP IS THE HALF THAT GETS LEFT OUT. The push side tests green entirely
|
|
166
|
+
// on its own, so a missing entry here is invisible until someone pulls and finds
|
|
167
|
+
// their block gone from site.yml. Every key `configNested` emits needs a line.
|
|
168
|
+
//
|
|
169
|
+
// 📌 `theme` will belong here after the stage-2 move off `info` — it is projected
|
|
170
|
+
// to `theme.yml` (not site.yml) and so will need its own handling, not a row.
|
|
171
|
+
const CONFIG_TO_SITE_YML = {
|
|
172
|
+
placeholders: 'placeholders',
|
|
173
|
+
}
|
|
174
|
+
|
|
158
175
|
/**
|
|
159
176
|
* Project a site-content document's `info` (+ `extensions`) onto the site's
|
|
160
177
|
* config files: `site.yml`, `theme.yml`, and `head.html`. Idempotent; only the
|
|
@@ -193,6 +210,14 @@ export function siteInfoToConfig({ document, siteRoot, sourceLocale = LOCALIZED_
|
|
|
193
210
|
if (info[infoKey] !== undefined) siteChanges[ymlKey] = info[infoKey]
|
|
194
211
|
}
|
|
195
212
|
|
|
213
|
+
// The `config` Section — authored configuration that is not identity, so it is
|
|
214
|
+
// not on `info`. Same verbatim treatment as the `info` block above; a Section
|
|
215
|
+
// the document does not carry writes nothing, like every other absent key here.
|
|
216
|
+
const configSection = document?.config || {}
|
|
217
|
+
for (const [configKey, ymlKey] of Object.entries(CONFIG_TO_SITE_YML)) {
|
|
218
|
+
if (configSection[configKey] !== undefined) siteChanges[ymlKey] = configSection[configKey]
|
|
219
|
+
}
|
|
220
|
+
|
|
196
221
|
// extensions[] → site.yml::extensions. Each entry carries EITHER `ref` (a
|
|
197
222
|
// catalog ref or a local name — an extension is a foundation and is declared
|
|
198
223
|
// like one) OR `url`. Project back whichever is present so a ref survives a
|
package/src/uwx/site.js
CHANGED
|
@@ -955,6 +955,54 @@ function secretsNested(siteYml) {
|
|
|
955
955
|
)
|
|
956
956
|
}
|
|
957
957
|
|
|
958
|
+
// ── `config` — the site's authored configuration ──────────────────────────────
|
|
959
|
+
//
|
|
960
|
+
// ⭐ THE LINE IS IDENTITY vs CONFIGURATION. `info` answers *"which site is this?"*
|
|
961
|
+
// — the name/label record, and it is read far more often than it is read in full.
|
|
962
|
+
// `config` answers *"what does this site render with?"*, and it exists because that
|
|
963
|
+
// second question had no home and its answers were accumulating on `info`.
|
|
964
|
+
//
|
|
965
|
+
// A `single` Section: one record, holding each block verbatim under its own key.
|
|
966
|
+
// Verbatim is the point — the authored shape is a nested map and it comes back as
|
|
967
|
+
// one, so nothing has to be flattened on push or rebuilt on pull.
|
|
968
|
+
//
|
|
969
|
+
// ⚠️ AUTHORS NEVER SEE THIS NAME. It is a wire and Model name; `site-project.js`
|
|
970
|
+
// writes `config.placeholders` back out to `site.yml::placeholders`. So it does
|
|
971
|
+
// not have to read well in a YAML file, and it is named flatly for what it holds,
|
|
972
|
+
// like `pages` / `queries` / `records`.
|
|
973
|
+
//
|
|
974
|
+
// ⚠️ AND IT IS A SUBSET OF THE RUNTIME'S `website.config`, not the same thing —
|
|
975
|
+
// that object is all of site.yml spread whole. One word, two scopes: everything
|
|
976
|
+
// in this Section lands in `website.config`, never the reverse. (uwx-format.md →
|
|
977
|
+
// the `config` Section.)
|
|
978
|
+
//
|
|
979
|
+
// 📌 Stage 2, not done here: `info.theme` belongs in this Section by the same
|
|
980
|
+
// argument and is NOT moved, because moving it is a DROP from `info` and a drop
|
|
981
|
+
// refuses (there is no rename detection — uwx-format.md § *A rename refuses*).
|
|
982
|
+
// That is a destructive migration on live data and is priced separately with the
|
|
983
|
+
// lane that pays it. Adding this Section is additive and auto-applies; do not
|
|
984
|
+
// quietly fold `theme` in on the strength of the comment above.
|
|
985
|
+
//
|
|
986
|
+
// ⛔ NEVER EMIT `{}` — and NOT for the reason this comment first gave. It said `{}`
|
|
987
|
+
// reads as "clear the stored record", by analogy with `services` above. Backend
|
|
988
|
+
// corrected it (2026-09-08): on a `single` Section the value must be an object, so
|
|
989
|
+
// `{}` parses as ONE RECORD WITH NO FIELDS, not zero records. There is no `[]`
|
|
990
|
+
// analogue — `multi` can say "zero records", `single` cannot.
|
|
991
|
+
//
|
|
992
|
+
// ⚠️ TODAY THE TWO COINCIDE BY ACCIDENT, because `placeholders` is this Section's
|
|
993
|
+
// only field, so "a record with no fields" and "placeholders cleared" are the same
|
|
994
|
+
// state. They diverge the moment `config` gains a second field, and then `{}` means
|
|
995
|
+
// *clear every field on config* — a far wider statement than the one intended.
|
|
996
|
+
//
|
|
997
|
+
// ⇒ The behaviour below is right either way: emit only when the file declares
|
|
998
|
+
// something. If an explicit clear is ever wanted, ask backend for a real form rather
|
|
999
|
+
// than inferring one from an empty object.
|
|
1000
|
+
function configNested(siteYml) {
|
|
1001
|
+
const config = {}
|
|
1002
|
+
setIf(config, 'placeholders', siteYml.placeholders)
|
|
1003
|
+
return Object.keys(config).length > 0 ? config : undefined
|
|
1004
|
+
}
|
|
1005
|
+
|
|
958
1006
|
/**
|
|
959
1007
|
* Map a file site project to the nested `@uniweb/site-content` `$`-document
|
|
960
1008
|
* (see the lane header above). PURE — reads the project, never mints, never writes.
|
|
@@ -1124,6 +1172,13 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1124
1172
|
// The provisioned record rides the `$services` section instead (see servicesNested).
|
|
1125
1173
|
setIf(info, 'paths', siteYml.paths)
|
|
1126
1174
|
setIf(info, 'data', siteYml.data ?? siteYml.fetch)
|
|
1175
|
+
// ⛔ `placeholders` IS NOT HERE, DELIBERATELY — it rides the `config` Section
|
|
1176
|
+
// (`configNested` below). `info` carries the site's IDENTITY, and every key on
|
|
1177
|
+
// this allowlist is one WE name and the author merely fills. `placeholders` is
|
|
1178
|
+
// the first where the author invents the key set, and it is unbounded — which
|
|
1179
|
+
// puts it on the Section side of the same line `queries` / `records` / `folders`
|
|
1180
|
+
// already sit on. See `configNested` for the split.
|
|
1181
|
+
//
|
|
1127
1182
|
// ⛔ `app` IS RETIRED — do not reintroduce it, in either direction. It carried an
|
|
1128
1183
|
// opaque uuid naming a separate entity a host bound to the site; that entity is
|
|
1129
1184
|
// gone, a site's services belong to the site itself, and NOTHING replaces the key.
|
|
@@ -1177,6 +1232,9 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1177
1232
|
doc.$id = SITE_ENTITY_KEY // one site-content entity per project (stable handle)
|
|
1178
1233
|
doc.$model = SITE_MODEL_NAME
|
|
1179
1234
|
doc.info = info
|
|
1235
|
+
// Emitted only when the file declares something — see `configNested`.
|
|
1236
|
+
const config = configNested(siteYml)
|
|
1237
|
+
if (config) doc.config = config
|
|
1180
1238
|
doc.pages = pages
|
|
1181
1239
|
doc.layout_sections = layoutSections
|
|
1182
1240
|
doc.extensions = extensionsNested(siteYml)
|
|
@@ -1191,9 +1249,26 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1191
1249
|
// Emitted ONLY when the file declares the key — see the header above
|
|
1192
1250
|
// `serviceRecords`: on a replaced Section, absent and empty are different
|
|
1193
1251
|
// requests and one of them is destructive.
|
|
1194
|
-
|
|
1252
|
+
//
|
|
1253
|
+
// ⭐ AND ONLY WHEN THE CALLER SAYS THE DECLARATION IS A REQUEST.
|
|
1254
|
+
// `opts.declareServices === false` withholds both Sections for THIS push, which
|
|
1255
|
+
// is not the same as the file having no key — the file still declares one; the
|
|
1256
|
+
// caller has determined the owner is not asking for anything new by it.
|
|
1257
|
+
//
|
|
1258
|
+
// ⛔ Why this decision cannot live here: the Sections are REPLACED wholesale by
|
|
1259
|
+
// what we send (`SectionScope::DeclaredOnly`), so re-sending an unchanged block
|
|
1260
|
+
// OVERWRITES whatever the stored request has become since — including a decision
|
|
1261
|
+
// the owner made in the app, where the consent workflow's publish happens. But
|
|
1262
|
+
// "has it changed since we last agreed?" needs the last agreed state, which is
|
|
1263
|
+
// project memory (`deploy.yml`) the CLI owns and this pure mapper must not read.
|
|
1264
|
+
// ⇒ The CLI decides; this honours the decision.
|
|
1265
|
+
//
|
|
1266
|
+
// ⚖️ Default is to declare, so every existing caller is unchanged and the
|
|
1267
|
+
// withholding is opt-in.
|
|
1268
|
+
const declare = opts.declareServices !== false
|
|
1269
|
+
const services = declare ? servicesNested(siteYml) : undefined
|
|
1195
1270
|
if (services) doc.services = services
|
|
1196
|
-
const secrets = secretsNested(siteYml)
|
|
1271
|
+
const secrets = declare ? secretsNested(siteYml) : undefined
|
|
1197
1272
|
if (secrets) doc.secrets = secrets
|
|
1198
1273
|
return doc
|
|
1199
1274
|
}
|
package/src/uwx/sync-package.js
CHANGED
|
@@ -305,7 +305,12 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
305
305
|
const siteDoc = includeSite
|
|
306
306
|
? await siteProjectToDocument(siteRoot, {
|
|
307
307
|
sourceLocale,
|
|
308
|
-
...(opts.queryUuids ? { queryUuids: opts.queryUuids } : {})
|
|
308
|
+
...(opts.queryUuids ? { queryUuids: opts.queryUuids } : {}),
|
|
309
|
+
// Withhold the `$services`/`$secrets` Sections when the caller has
|
|
310
|
+
// determined the file is not asking for anything new by them. Passed
|
|
311
|
+
// through rather than decided here: the last-agreed state is project
|
|
312
|
+
// memory the CLI owns. See site.js at `declareServices`.
|
|
313
|
+
...(opts.declareServices === false ? { declareServices: false } : {})
|
|
309
314
|
})
|
|
310
315
|
: null
|
|
311
316
|
// Deploy-derived `info` fields (e.g. `data_bundle`, the static-data ball URL) are
|