@uniweb/build 1.10.4 → 1.10.5

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.5",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,11 +56,11 @@
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",
61
+ "@uniweb/semantic-parser": "~1.13.0",
63
62
  "@uniweb/projections": "~1.2.1",
63
+ "@uniweb/content-writer": "~1.3.1",
64
64
  "@uniweb/theming": "~1.2.1"
65
65
  },
66
66
  "optionalDependencies": {
@@ -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/runtime": "~1.2.3",
77
+ "@uniweb/core": "~1.4.7"
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))
@@ -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
+ }