@uniweb/build 1.10.4 → 1.10.6

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": "1.10.4",
3
+ "version": "1.10.6",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,15 +56,15 @@
56
56
  "js-yaml": "^4.3.2",
57
57
  "sharp": "^0.35.5",
58
58
  "yaml": "^2.5.0",
59
- "@uniweb/semantic-parser": "~1.13.0",
60
59
  "@uniweb/content-reader": "~1.5.7",
61
- "@uniweb/content-writer": "~1.3.1",
62
60
  "@uniweb/schemas": "~1.6.5",
63
- "@uniweb/projections": "~1.2.1",
64
- "@uniweb/theming": "~1.2.1"
61
+ "@uniweb/semantic-parser": "~1.13.0",
62
+ "@uniweb/theming": "~1.2.1",
63
+ "@uniweb/projections": "~1.2.2",
64
+ "@uniweb/content-writer": "~1.3.1"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@uniweb/runtime": "~1.2.3"
67
+ "@uniweb/runtime": "~1.2.4"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -73,8 +73,8 @@
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": "~1.4.7",
77
- "@uniweb/runtime": "~1.2.3"
76
+ "@uniweb/core": "~1.4.8",
77
+ "@uniweb/runtime": "~1.2.4"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "vite": {
@@ -40,6 +40,7 @@ import { resolveDefaultLocale, resolvePublishableLocales, validateLanguageConfig
40
40
  import { authoredRedirectTarget } from '@uniweb/core/resolve-route'
41
41
  import { parentRouteOf } from '@uniweb/core/route-match'
42
42
  import { parseFrontmatter } from '../utils/frontmatter.js'
43
+ import { valuesCutByComment } from '../utils/yaml-comment-trap.js'
43
44
  import { parseGrid } from '@uniweb/schemas/grid'
44
45
  import { runtimeServicesConfig, refuseRetiredServiceKeys, refuseUnreadableServices } from '../uwx/services-request.js'
45
46
  import { refuseUnreadableAddress } from './site-address.js'
@@ -438,6 +439,20 @@ let yamlFailures = []
438
439
  */
439
440
  let sectionTypes = null
440
441
 
442
+ /** The site being collected, so a warning names a file as its author sees it. Reset per collect, as `yamlFailures` is. */
443
+ let collectRoot = null
444
+
445
+ /**
446
+ * Say each value the file's YAML cut short at a `#` its author meant as part of it — a
447
+ * gradient, a color (`../utils/yaml-comment-trap.js`). A warning, not a failure: the YAML is
448
+ * valid, and the site builds without the value, as it did before anything said so.
449
+ */
450
+ function warnCutByComment(text, parsed, file) {
451
+ for (const { message } of valuesCutByComment(text, parsed)) {
452
+ console.warn(`[content-collector] ${collectRoot ? relative(collectRoot, file) : file}: ${message}`)
453
+ }
454
+ }
455
+
441
456
  /**
442
457
  * Parse YAML, recording a failure against the file it came from.
443
458
  *
@@ -449,7 +464,9 @@ let sectionTypes = null
449
464
  */
450
465
  function parseYaml(yamlString, source = '<unknown>') {
451
466
  try {
452
- return yaml.load(yamlString, YAML_OPTIONS) || {}
467
+ const parsed = yaml.load(yamlString, YAML_OPTIONS) || {}
468
+ warnCutByComment(yamlString, parsed, source)
469
+ return parsed
453
470
  } catch (err) {
454
471
  yamlFailures.push({ source, message: err.message })
455
472
  console.warn(`[content-collector] YAML parse error in ${source}: ${err.message}`)
@@ -1097,6 +1114,7 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
1097
1114
  const parsed = parseFrontmatter(content, filePath)
1098
1115
  frontMatter = parsed.frontmatter
1099
1116
  markdown = parsed.body
1117
+ warnCutByComment(parsed.yaml, frontMatter, filePath)
1100
1118
  } catch (err) {
1101
1119
  yamlFailures.push({ source: filePath, message: err.message })
1102
1120
  console.warn(`[content-collector] ${err.message}`)
@@ -2556,6 +2574,7 @@ export async function collectSiteContent(sitePath, options = {}) {
2556
2574
  // still be counted.
2557
2575
  yamlFailures = []
2558
2576
  sectionTypes = null
2577
+ collectRoot = sitePath
2559
2578
 
2560
2579
  // Read site config and raw theme config
2561
2580
  const siteConfig = await readYamlFile(join(sitePath, configFile))
@@ -308,11 +308,14 @@ export async function forgetDeployYml(siteDir) {
308
308
  *
309
309
  * A target's record moves to `deploys:` when that target has none there; when it has, the
310
310
  * one in `deploys:` is newer, and the old is dropped. `autoSave: off` becomes
311
- * `saveDeploys: false`; any other value is the default, and goes.
311
+ * `saveDeploys: false`; any other value is the default, and goes. And `saveDeploys:` holding
312
+ * one of `autoSave:`'s values — the key renamed by hand, its value kept — is read the same way:
313
+ * `off` is `false`, `lastDeploy` the default.
312
314
  *
313
315
  * @param {string} siteDir
314
- * @returns {Promise<{ moved: string[], dropped: string[], saveDeploys: boolean|null } | null>}
315
- * null when the file has neither key, or there is no file
316
+ * @returns {Promise<{ moved: string[], dropped: string[], saveDeploys: boolean|null, renamed: string|null } | null>}
317
+ * `renamed` is the old value found under `saveDeploys:`; null when the file has none of
318
+ * these, or there is no file
316
319
  */
317
320
  export async function retireDeployYmlKeys(siteDir) {
318
321
  const path = join(siteDir, 'deploy.yml')
@@ -321,7 +324,9 @@ export async function retireDeployYmlKeys(siteDir) {
321
324
  if (doc.errors.length) throw new Error(`${path}: ${doc.errors[0].message}`)
322
325
  const old = doc.get('lastDeploy')
323
326
  const autoSave = doc.get('autoSave')
324
- if (old === undefined && autoSave === undefined) return null
327
+ const renamed = doc.get('saveDeploys')
328
+ const kept = renamed === 'off' || renamed === 'lastDeploy' ? renamed : undefined
329
+ if (old === undefined && autoSave === undefined && kept === undefined) return null
325
330
  const moved = []
326
331
  const dropped = []
327
332
  if (isMap(old)) {
@@ -338,6 +343,14 @@ export async function retireDeployYmlKeys(siteDir) {
338
343
  }
339
344
  doc.delete('lastDeploy')
340
345
  let saveDeploys = null
346
+ if (kept !== undefined) {
347
+ if (kept === 'off') {
348
+ doc.set('saveDeploys', false)
349
+ saveDeploys = false
350
+ } else {
351
+ doc.delete('saveDeploys')
352
+ }
353
+ }
341
354
  if (autoSave !== undefined) {
342
355
  if (String(autoSave) === 'off' && !doc.has('saveDeploys')) {
343
356
  doc.set('saveDeploys', false)
@@ -346,5 +359,5 @@ export async function retireDeployYmlKeys(siteDir) {
346
359
  doc.delete('autoSave')
347
360
  }
348
361
  await writeFile(path, String(doc))
349
- return { moved, dropped, saveDeploys }
362
+ return { moved, dropped, saveDeploys, renamed: kept ?? null }
350
363
  }
@@ -186,7 +186,15 @@ function validate(doc, path) {
186
186
  // now: two targets naming DIFFERENT backends are two sites, and two naming the SAME
187
187
  // backend are one site shipped twice. The reason is gone, so the rejection is too.
188
188
  if (doc.saveDeploys !== undefined && typeof doc.saveDeploys !== 'boolean') {
189
- throw new Error(`${path}: \`saveDeploys\` must be true or false.`)
189
+ // `lastDeploy` and `off` were the values of `autoSave:`, which `saveDeploys:` replaced: the
190
+ // key renamed by hand, its value kept (measured 2026-10-26 on a copied project).
191
+ const retired = doc.saveDeploys === 'lastDeploy' || doc.saveDeploys === 'off'
192
+ throw new Error(
193
+ `${path}: \`saveDeploys\` must be true or false` +
194
+ (retired
195
+ ? ` — \`${doc.saveDeploys}\` is a value of the retired \`autoSave:\`. \`uniweb doctor --fix deploy-yml-retired-key\` writes what it meant.`
196
+ : '.')
197
+ )
190
198
  }
191
199
  if (doc.default !== undefined && typeof doc.default !== 'string') {
192
200
  throw new Error(`${path}: \`default\` must be a string.`)
@@ -51,24 +51,25 @@ import { YAML_OPTIONS } from './yaml-schema.js'
51
51
  * @param {string} [filepath] - where it came from. Optional only because a
52
52
  * caller may genuinely not have one; pass it whenever you do, since the file
53
53
  * name is the difference between a fixable error and a hunt.
54
- * @returns {{ frontmatter: object, body: string }}
54
+ * @returns {{ frontmatter: object, body: string, yaml: string }} `yaml` is the block as
55
+ * written, for a caller that reads what the parse cannot say (`./yaml-comment-trap.js`)
55
56
  * @throws {Error} when a `---` block is present and does not parse as YAML
56
57
  */
57
58
  export function parseFrontmatter(raw, filepath) {
58
59
  const text = raw ?? ''
59
60
  if (!text.trimStart().startsWith('---')) {
60
- return { frontmatter: {}, body: text }
61
+ return { frontmatter: {}, body: text, yaml: '' }
61
62
  }
62
63
 
63
64
  const parts = text.split('---\n')
64
65
  if (parts.length < 3) {
65
- return { frontmatter: {}, body: text }
66
+ return { frontmatter: {}, body: text, yaml: '' }
66
67
  }
67
68
 
68
69
  try {
69
70
  const frontmatter = yaml.load(parts[1], YAML_OPTIONS) || {}
70
71
  const body = parts.slice(2).join('---\n')
71
- return { frontmatter, body }
72
+ return { frontmatter, body, yaml: parts[1] }
72
73
  } catch (err) {
73
74
  const where = filepath ? `${filepath}: ` : ''
74
75
  throw new Error(
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Values YAML cut short at a `#` the author meant as part of them.
3
+ *
4
+ * YAML reads a `#` with a space before it as the start of a comment, so in a plain (unquoted)
5
+ * value it ends the value there: `gradient: linear-gradient(165deg, #0c1226 0%, #05060f 100%)`
6
+ * is `linear-gradient(165deg,` — an invalid value, which the browser drops — and
7
+ * `primary: #3b82f6` is no value at all. YAML is right; nothing in the file is malformed, so
8
+ * nothing fails, and the color or the gradient is just not there.
9
+ *
10
+ * Said only where the file shows it, so a comment meant as one is never questioned:
11
+ * - **cut** — a value left with a `(` it never closes, followed in the file by ` #`;
12
+ * - **empty** — a key left with no value, followed in the file by a color: `#` and 3 to 8
13
+ * hex digits.
14
+ *
15
+ * ⭐ Quoting the value is the fix, and the one the message gives:
16
+ * `gradient: "linear-gradient(165deg, #0c1226 0%, #05060f 100%)"`. Measured 2026-10-26: a book
17
+ * page's gradient had not shown since the page was written, in either renderer, and nothing
18
+ * said so.
19
+ *
20
+ * @module @uniweb/build/utils/yaml-comment-trap
21
+ */
22
+
23
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
24
+
25
+ /** More `(` than `)`: a function the value opens and never closes. */
26
+ function unclosed(value) {
27
+ let depth = 0
28
+ for (const ch of value) {
29
+ if (ch === '(') depth++
30
+ else if (ch === ')' && depth > 0) depth--
31
+ }
32
+ return depth > 0
33
+ }
34
+
35
+ /**
36
+ * @param {string} text - the YAML as written
37
+ * @param {*} parsed - what it parsed to
38
+ * @returns {{ path: string, kind: 'cut'|'empty', message: string }[]}
39
+ */
40
+ export function valuesCutByComment(text, parsed) {
41
+ if (!text || !text.includes('#') || !parsed || typeof parsed !== 'object') return []
42
+ const found = []
43
+ const walk = (value, path, key) => {
44
+ if (Array.isArray(value)) {
45
+ value.forEach((v, i) => walk(v, `${path}[${i}]`, null))
46
+ return
47
+ }
48
+ if (value && typeof value === 'object' && !(value instanceof Date)) {
49
+ for (const [k, v] of Object.entries(value)) walk(v, path ? `${path}.${k}` : k, k)
50
+ return
51
+ }
52
+ if (typeof value === 'string' && unclosed(value)) {
53
+ const m = text.match(new RegExp(`${escapeRe(value)}[ \\t]+(#.*?)[ \\t]*$`, 'm'))
54
+ if (!m) return
55
+ const meant = `${value} ${m[1]}`
56
+ found.push({
57
+ path,
58
+ kind: 'cut',
59
+ message:
60
+ `\`${path}\` is cut short at \`${value}\` — YAML reads a \`#\` after a space as a comment. ` +
61
+ `Quote the value: ${key ?? path}: "${meant.replace(/"/g, '\\"')}"`
62
+ })
63
+ return
64
+ }
65
+ if (value === null && key) {
66
+ const m = text.match(new RegExp(`^[ \\t]*(?:-[ \\t]+)?${escapeRe(key)}[ \\t]*:[ \\t]+(#[0-9A-Fa-f]{3,8})\\b`, 'm'))
67
+ if (!m) return
68
+ found.push({
69
+ path,
70
+ kind: 'empty',
71
+ message: `\`${path}\` is empty — YAML reads \`${m[1]}\` as a comment. Quote the value: ${key}: "${m[1]}"`
72
+ })
73
+ }
74
+ }
75
+ walk(parsed, '', null)
76
+ return found
77
+ }