@uniweb/build 1.10.3 → 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.
|
|
3
|
+
"version": "1.10.5",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"sharp": "^0.35.5",
|
|
58
58
|
"yaml": "^2.5.0",
|
|
59
59
|
"@uniweb/content-reader": "~1.5.7",
|
|
60
|
-
"@uniweb/content-writer": "~1.3.1",
|
|
61
|
-
"@uniweb/projections": "~1.2.1",
|
|
62
60
|
"@uniweb/schemas": "~1.6.5",
|
|
63
61
|
"@uniweb/semantic-parser": "~1.13.0",
|
|
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/
|
|
77
|
-
"@uniweb/
|
|
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
|
-
|
|
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}`)
|
|
@@ -601,9 +618,10 @@ function mountHasContent(dirPath) {
|
|
|
601
618
|
*
|
|
602
619
|
* Split out from resolveMounts so the dev server can learn which directories a
|
|
603
620
|
* site mounts without re-running resolveMounts' validation — that would repeat
|
|
604
|
-
* its warnings on every startup, and the watcher only needs paths.
|
|
605
|
-
*
|
|
606
|
-
*
|
|
621
|
+
* its warnings on every startup, and the watcher only needs paths. Every reader —
|
|
622
|
+
* the collector, the watcher, a pull (`uwx/git-authored.js`) and `uniweb status` among
|
|
623
|
+
* them — shares this one definition of what a mount key looks like, so the prefix can never mean
|
|
624
|
+
* one thing to one of them and another to the next.
|
|
607
625
|
*
|
|
608
626
|
* @param {Object} pathsConfig - The paths: object from site.yml
|
|
609
627
|
* @returns {Array<[string, string]>} [routeSegment, relativePath] pairs
|
|
@@ -1096,6 +1114,7 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
|
|
|
1096
1114
|
const parsed = parseFrontmatter(content, filePath)
|
|
1097
1115
|
frontMatter = parsed.frontmatter
|
|
1098
1116
|
markdown = parsed.body
|
|
1117
|
+
warnCutByComment(parsed.yaml, frontMatter, filePath)
|
|
1099
1118
|
} catch (err) {
|
|
1100
1119
|
yamlFailures.push({ source: filePath, message: err.message })
|
|
1101
1120
|
console.warn(`[content-collector] ${err.message}`)
|
|
@@ -2555,6 +2574,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2555
2574
|
// still be counted.
|
|
2556
2575
|
yamlFailures = []
|
|
2557
2576
|
sectionTypes = null
|
|
2577
|
+
collectRoot = sitePath
|
|
2558
2578
|
|
|
2559
2579
|
// Read site config and raw theme config
|
|
2560
2580
|
const siteConfig = await readYamlFile(join(sitePath, configFile))
|
package/src/site/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { PREVIEW_ENV, encodePreview, readPreview } from './preview.js'
|
|
|
14
14
|
export { DEV_PORT_ENV, DEV_RECORD_ENV, runningDevServer, stopDevServer } from './dev-server.js'
|
|
15
15
|
// Where a statically built site lives, as its deploy target or author states it (`./site-address.js`).
|
|
16
16
|
export { ORIGIN_ENV, refuseUnreadableAddress } from './site-address.js'
|
|
17
|
-
export { collectSiteContent } from './content-collector.js'
|
|
17
|
+
export { collectSiteContent, mountEntriesOf } from './content-collector.js'
|
|
18
18
|
// The `agents:` vocabulary lives in @uniweb/projections (which owns the block);
|
|
19
19
|
// re-exported so the CLI can validate an author's spelling, and read an
|
|
20
20
|
// `expectedOrigins` entry by its rule, without taking a direct dependency on it.
|
package/src/utils/frontmatter.js
CHANGED
|
@@ -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
|
+
}
|