@uniweb/build 0.25.1 → 0.25.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,17 +59,17 @@
59
59
  "js-yaml": "^4.1.0",
60
60
  "sharp": "^0.35.3",
61
61
  "yaml": "^2.5.0",
62
- "@uniweb/schemas": "^0.2.10",
62
+ "@uniweb/semantic-parser": "^1.3.0",
63
63
  "@uniweb/theming": "^0.1.15",
64
- "@uniweb/projections": "^0.3.4",
65
64
  "@uniweb/content-writer": "^0.3.4",
66
- "@uniweb/semantic-parser": "^1.3.0"
65
+ "@uniweb/projections": "^0.3.4",
66
+ "@uniweb/schemas": "^0.2.10"
67
67
  },
68
68
  "optionalDependencies": {
69
69
  "@uniweb/semantic-parser": "^1.3.0",
70
+ "@uniweb/content-reader": "^1.2.4",
70
71
  "@uniweb/schemas": "^0.2.10",
71
- "@uniweb/runtime": "^0.12.9",
72
- "@uniweb/content-reader": "^1.2.4"
72
+ "@uniweb/runtime": "^0.12.9"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -78,7 +78,7 @@
78
78
  "@tailwindcss/vite": "^4.0.0",
79
79
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
80
80
  "vite-plugin-svgr": "^4.0.0",
81
- "@uniweb/core": "^0.11.2"
81
+ "@uniweb/core": "^0.11.3"
82
82
  },
83
83
  "peerDependenciesMeta": {
84
84
  "vite": {
package/src/schema.js CHANGED
@@ -156,6 +156,24 @@ const DEFAULT_ONLY_CAPABILITIES = [
156
156
  * every `[#id]` rendered as its own literal text — indistinguishable from a
157
157
  * foundation that had never opted in. Found only by reading generate-entry.js.
158
158
  *
159
+ * ⚠️ THAT DIAGNOSIS WAS CORRECT AND INCOMPLETE, and the rest took three weeks
160
+ * to find (2026-08-23). The same commit that moved those keys onto the default
161
+ * export also added `import … from '@uniweb/kit/xref'`, whose graph reaches
162
+ * `Ref.jsx`. `loadFoundationConfig` could not parse that, so the config never
163
+ * loaded at all — the identical symptom, from an unrelated cause, arriving on
164
+ * the same day the correct fix was applied. The fix looked inert because it
165
+ * was: where the keys sat could not matter while nothing was read.
166
+ *
167
+ * ⛔ AND IT DISABLED THIS WARNING. `warnMisplacedCapabilities` runs AFTER the
168
+ * import; a config that throws never reaches it. The guard added to catch that
169
+ * incident was, from that day, unable to fire on the project it was written
170
+ * for. A check downstream of the thing most likely to fail is not a check.
171
+ *
172
+ * To tell the two apart: a misplaced named export loses only that capability.
173
+ * A config that fails to load loses everything the file declares — `vars`
174
+ * included, so the site also renders with no theme variables at all. If the
175
+ * theme is gone too, stop looking at export placement.
176
+ *
159
177
  * A warning rather than an error: a foundation may legitimately export a name
160
178
  * that collides for its own use, and failing someone's build over a naming
161
179
  * coincidence is worse than telling them what we ignored.
@@ -134,12 +134,38 @@ function parseCollectionConfig(name, config) {
134
134
  }
135
135
 
136
136
  /**
137
- * Parse YAML frontmatter from markdown content
137
+ * Parse YAML frontmatter from markdown content.
138
+ *
139
+ * Two cases, and keeping them apart is the whole point:
140
+ *
141
+ * NO frontmatter — the file does not open with `---`, or never closes the
142
+ * block. Legitimate: a record can be pure body. Returns {}.
143
+ *
144
+ * DECLARED frontmatter that does not parse — an error, because every field
145
+ * is gone at once. Not just the one with the typo: title, slug, date, image,
146
+ * category, all of it. The record still builds, still ships, and lands at a
147
+ * filename-derived slug with no title and no cover.
148
+ *
149
+ * ⛔ THIS USED TO WARN AND CONTINUE, and the warning could not be found.
150
+ * Measured 2026-08-24 on a real post: an unquoted colon inside a description
151
+ * ("...on a website framework: everything hard about docs...") voided six
152
+ * fields and moved the page from /blog/docs-sites to /blog/11_docs_sites. The
153
+ * only trace was
154
+ *
155
+ * [collection-processor] YAML parse error: bad indentation of a mapping entry (4:72)
156
+ *
157
+ * on line 16 of 857 lines of build output, naming no file, nine lines above
158
+ * "Processed articles: 6 items" — a success line that reads as everything
159
+ * being fine. The build exited 0 and the broken record shipped.
160
+ *
161
+ * A parse error now names the file and says what it costs, because "which of
162
+ * my 200 records is (4:72) in?" is the question the old message left you with.
138
163
  *
139
164
  * @param {string} raw - Raw file content
165
+ * @param {string} [filepath] - Path to the file, for the error message
140
166
  * @returns {{ frontmatter: Object, body: string }}
141
167
  */
142
- function parseFrontmatter(raw) {
168
+ function parseFrontmatter(raw, filepath) {
143
169
  if (!raw.trim().startsWith('---')) {
144
170
  return { frontmatter: {}, body: raw }
145
171
  }
@@ -154,8 +180,18 @@ function parseFrontmatter(raw) {
154
180
  const body = parts.slice(2).join('---\n')
155
181
  return { frontmatter, body }
156
182
  } catch (err) {
157
- console.warn('[collection-processor] YAML parse error:', err.message)
158
- return { frontmatter: {}, body: raw }
183
+ const where = filepath ? `${filepath}: ` : ''
184
+ throw new Error(
185
+ `${where}frontmatter is not valid YAML — ${err.message}\n` +
186
+ ` The file opens with \`---\`, so it is declaring frontmatter. Since the block does not\n` +
187
+ ` parse, EVERY field in it is lost — title, slug, date, image, category — and the record\n` +
188
+ ` would build as an untitled entry at a slug derived from its filename.\n` +
189
+ ` A common cause is an unquoted value containing a colon followed by a space:\n` +
190
+ ` description: Building on a framework: everything hard is a website problem\n` +
191
+ ` Quote the value and it parses:\n` +
192
+ ` description: "Building on a framework: everything hard is a website problem"`,
193
+ { cause: err },
194
+ )
159
195
  }
160
196
  }
161
197
 
@@ -508,7 +544,7 @@ async function processContentItem(dir, filename, config, siteRoot, basePath) {
508
544
  const slug = basename(filename, extname(filename))
509
545
 
510
546
  // Parse frontmatter and body
511
- const { frontmatter, body } = parseFrontmatter(raw)
547
+ const { frontmatter, body } = parseFrontmatter(raw, filepath)
512
548
 
513
549
  // Skip unpublished items by default
514
550
  if (frontmatter.published === false) {
@@ -26,10 +26,31 @@ import { existsSync } from 'node:fs'
26
26
  import { join } from 'node:path'
27
27
  import { Document, parseDocument, isMap } from 'yaml'
28
28
 
29
+ // The header every generated deploy.yml carries. It has to answer the question
30
+ // a reader has while looking AT the file — who wrote this, and may I edit it —
31
+ // because that is where the question gets asked, not in the docs.
32
+ //
33
+ // The previous wording opened "operational config ... edit `targets:` freely",
34
+ // which reads as a file you are expected to author with one auto-managed block
35
+ // inside it. It cost a real reader an afternoon: they went looking for what to
36
+ // write under `targets:` for a site that had never deployed, when the answer is
37
+ // that the first deploy writes it.
29
38
  const SCAFFOLD_HEADER = [
30
- ' deploy.yml — operational config and last-deploy memory for this site.',
31
- ' Safe to commit. The `lastDeploy:` block is auto-managed by `uniweb deploy`;',
32
- ' edit `targets:` freely.',
39
+ ' deploy.yml — written by `uniweb deploy` / `uniweb publish`.',
40
+ '',
41
+ ' You do not create this file. The first successful deploy does, recording the',
42
+ ' target you picked and what happened. Later deploys rewrite only `lastDeploy:`',
43
+ ' and leave the rest — including your comments — alone.',
44
+ '',
45
+ ' Safe to commit; no credentials live here. Host credentials come from the',
46
+ ' environment.',
47
+ '',
48
+ ' default: which target is used when none is named',
49
+ ' targets: where this site ships. Edit to change the destination, or add',
50
+ ' a target and pick it with `--target <name>`',
51
+ ' autoSave: `lastDeploy` to keep the record below, `off` to stop writing it',
52
+ ' lastDeploy: what the last deploy did. A record, not a setting — nothing',
53
+ ' reads it back, so a stale one is safe to delete',
33
54
  ].join('\n')
34
55
 
35
56
  /**
package/src/uwx/index.js CHANGED
@@ -44,6 +44,7 @@ export {
44
44
  emitSiteSyncPackage,
45
45
  writeSiteEntityUuid,
46
46
  writeSiteOrg,
47
+ writeSiteBackend,
47
48
  extensionDeclaration,
48
49
  isExtensionUrl,
49
50
  isSiteRelativeExtensionUrl,
package/src/uwx/site.js CHANGED
@@ -947,3 +947,42 @@ export function writeSiteEntityUuid(siteRoot, uuid) {
947
947
  export function writeSiteOrg(siteRoot, handle) {
948
948
  return upsertYamlScalar(join(siteRoot, 'site.yml'), '$org', handle)
949
949
  }
950
+
951
+ /**
952
+ * Record the backend this project SYNCS WITH (`site.yml::$backend`), beside `$uuid`/`$org`.
953
+ *
954
+ * ⭐ It is not a tag on the site uuid — it is the project's **sync scope**. Four surfaces
955
+ * hold backend-minted identity, and this one fact is what makes all of them meaningful:
956
+ * `site.yml::$uuid`, every collection record's `$uuid` in its own source file,
957
+ * `assets.json`, and `.uniweb/sync-cache.json`. Change the backend and every one of them
958
+ * is foreign at once — which is why a mismatch is a stop, not a fallback
959
+ * (`assertSiteBackendScope` in the CLI).
960
+ *
961
+ * ⛔ **Written ONLY for a non-default backend.** The 98% case stays out of the file, and an
962
+ * absent value reads as the default — correct both for a project written before this key
963
+ * existed and for one synced against the default. Callers decide; this writer does not know
964
+ * the CLI's default. See `recordSiteBackend` (cli/src/backend/site-sync.js), which is where
965
+ * the "is it the default?" test lives.
966
+ *
967
+ * ⚠️ **The name is load-bearing. Five alternatives were considered and rejected (2026-08-24);
968
+ * do not re-propose one.** `origin` is wrong twice over — a site HAS an origin (its own
969
+ * served domain), and this lane is git-modeled, where `origin` names a remote rather than a
970
+ * URL. `remote` implies a multiplicity this design closes. `hosting` and `platform` name the
971
+ * hosting edge, which this package has no client for. `host` collides with
972
+ * `deploy.yml::targets.<n>.host` (the deploy adapter). A `uuid: <id>@<backend>` suffix was
973
+ * rejected too: it would make "this never reaches the backend" a discipline at every read
974
+ * site instead of a property, and `siteProjectToDocument` assigns `$uuid` straight onto the
975
+ * wire document.
976
+ *
977
+ * Safe as a plain YAML scalar: a URL's `:` is followed by `/` or a digit, never a space, so
978
+ * it needs no quoting (verified against js-yaml for apex, `localhost:8080` and host:port+path
979
+ * forms, 2026-08-24). This is the same class of hazard as `$org`'s leading `@`, which is
980
+ * stripped for exactly that reason — but it lands on the safe side.
981
+ *
982
+ * @param {string} siteRoot
983
+ * @param {string} origin - a bare origin, no trailing slash
984
+ * @returns {boolean} true if site.yml changed
985
+ */
986
+ export function writeSiteBackend(siteRoot, origin) {
987
+ return upsertYamlScalar(join(siteRoot, 'site.yml'), '$backend', origin)
988
+ }