@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.
|
|
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/
|
|
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.
|
|
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.
|
|
77
|
-
"@uniweb/runtime": "~1.2.
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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.`)
|
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
|
+
}
|