@uniweb/build 0.73.3 → 0.74.0
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 +9 -9
- package/src/docs.js +37 -17
- package/src/runtime-schema.js +12 -5
- package/src/schema.js +95 -8
- package/src/site/asset-processor.js +33 -9
- package/src/site/assets.js +101 -75
- package/src/site/build-site-data.js +3 -3
- package/src/site/content-collector.js +7 -1
- package/src/site/file-records.js +5 -52
- package/src/site/plugin.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,15 +57,15 @@
|
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
|
-
"@uniweb/
|
|
61
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
60
|
+
"@uniweb/content-reader": "^1.2.7",
|
|
62
61
|
"@uniweb/projections": "^0.7.11",
|
|
63
|
-
"@uniweb/
|
|
64
|
-
"@uniweb/
|
|
65
|
-
"@uniweb/
|
|
62
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
63
|
+
"@uniweb/semantic-parser": "^1.5.1",
|
|
64
|
+
"@uniweb/schemas": "^0.10.0",
|
|
65
|
+
"@uniweb/theming": "^0.2.0"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.28.
|
|
68
|
+
"@uniweb/runtime": "^0.28.4"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"@tailwindcss/vite": "^4.0.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
76
76
|
"vite-plugin-svgr": "^4.0.0",
|
|
77
|
-
"@uniweb/
|
|
78
|
-
"@uniweb/
|
|
77
|
+
"@uniweb/core": "^0.35.0",
|
|
78
|
+
"@uniweb/runtime": "^0.28.4"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
package/src/docs.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
9
9
|
import { existsSync } from 'node:fs'
|
|
10
10
|
import { join, isAbsolute } from 'node:path'
|
|
11
|
+
import { describeContent } from '@uniweb/schemas/content'
|
|
11
12
|
import { buildSchema } from './schema.js'
|
|
12
13
|
import { resolveFoundationSrcPath } from './utils/foundation-source-root.js'
|
|
13
14
|
|
|
@@ -94,25 +95,30 @@ function generateComponentDocs(name, meta) {
|
|
|
94
95
|
lines.push('')
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
// Content expectations — what an author writes in the markdown
|
|
98
|
-
// `
|
|
99
|
-
// the
|
|
100
|
-
//
|
|
101
|
-
//
|
|
98
|
+
// Content expectations — what an author writes in the markdown, read through
|
|
99
|
+
// `describeContent`: a built schema holds the lowered list (`SCHEMA_FORMAT` 2), a
|
|
100
|
+
// `meta.js` the declaration as written, and both describe alike. ⛔ This read
|
|
101
|
+
// `meta.content` as a map of labels until 2026-09-29 — which the lowered list is not —
|
|
102
|
+
// and as `meta.elements`, the pre-rename name, until 2026-09-16.
|
|
102
103
|
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
const elements = Object.entries(meta.content || {})
|
|
104
|
+
// The LABEL is the point: `paragraphs` alone tells an author nothing, and the count is
|
|
105
|
+
// the part that says how much to write — shown in the bracket syntax they write.
|
|
106
|
+
const { elements } = describeContent(meta)
|
|
107
107
|
if (elements.length > 0) {
|
|
108
108
|
lines.push('### Content')
|
|
109
109
|
lines.push('')
|
|
110
110
|
|
|
111
|
-
for (const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
111
|
+
for (const el of elements) {
|
|
112
|
+
const name = el.kind === 'concept' ? `md:${el.key}` : el.element
|
|
113
|
+
const types = el.element === 'media' && el.types.length < 3 ? ` (${el.types.join(', ')})` : ''
|
|
114
|
+
const count = bracketCount(el)
|
|
115
|
+
const label = [el.label, count].filter(Boolean).join(' ')
|
|
116
|
+
lines.push(`**${name}**${types}${label ? ` — ${label}` : ''}`)
|
|
117
|
+
if (el.hint) lines.push(` ${el.hint}`)
|
|
118
|
+
if (el.except) lines.push(` Leaves out: ${el.except.join(', ')}`)
|
|
119
|
+
if (el.content) {
|
|
120
|
+
lines.push(` Each entry: ${el.content.map((c) => [c.element, c.label].filter(Boolean).join(' — ')).join(' · ')}`)
|
|
121
|
+
}
|
|
116
122
|
lines.push('')
|
|
117
123
|
}
|
|
118
124
|
}
|
|
@@ -120,6 +126,18 @@ function generateComponentDocs(name, meta) {
|
|
|
120
126
|
return lines.join('\n')
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
/** A lowered entry's count, as a developer writes it: `[1]`, `[3-6]`, `[2+]` — or ''. */
|
|
130
|
+
function bracketCount({ min, max }) {
|
|
131
|
+
if (typeof min !== 'number') return ''
|
|
132
|
+
if (typeof max !== 'number') return `[${min}+]`
|
|
133
|
+
return min === max ? `[${min}]` : `[${min}-${max}]`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The section types of a foundation schema: every key but `_self`, `_layouts` and `dataSchemas`. */
|
|
137
|
+
function sectionTypesOf(schema) {
|
|
138
|
+
return Object.keys(schema).filter((key) => !key.startsWith('_') && key !== 'dataSchemas')
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
/**
|
|
124
142
|
* Generate full markdown documentation for a foundation
|
|
125
143
|
*
|
|
@@ -146,8 +164,10 @@ export function generateDocsFromSchema(schema, options = {}) {
|
|
|
146
164
|
lines.push('---')
|
|
147
165
|
lines.push('')
|
|
148
166
|
|
|
149
|
-
// Table of contents
|
|
150
|
-
|
|
167
|
+
// Table of contents. A schema's section types are its keys but `_self`, `_layouts` and
|
|
168
|
+
// `dataSchemas` (`buildSchema`); ⛔ until 2026-09-29 only `_self` was left out, so a
|
|
169
|
+
// foundation with layouts or data schemas documented both as components.
|
|
170
|
+
const componentNames = sectionTypesOf(schema)
|
|
151
171
|
|
|
152
172
|
if (componentNames.length > 0) {
|
|
153
173
|
lines.push('## Components')
|
|
@@ -222,7 +242,7 @@ export async function generateDocs(foundationDir, options = {}) {
|
|
|
222
242
|
await writeFile(outputPath, markdown)
|
|
223
243
|
|
|
224
244
|
// Count components
|
|
225
|
-
const componentCount =
|
|
245
|
+
const componentCount = sectionTypesOf(schema).length
|
|
226
246
|
|
|
227
247
|
return { outputPath, componentCount }
|
|
228
248
|
}
|
package/src/runtime-schema.js
CHANGED
|
@@ -28,6 +28,7 @@ import { isRichSchema } from '@uniweb/core'
|
|
|
28
28
|
import { dataRefOf } from '@uniweb/core/data-keys'
|
|
29
29
|
import { briefFieldMap, wholeFieldMap } from '@uniweb/schemas/conform'
|
|
30
30
|
import { enumValues } from '@uniweb/schemas/format'
|
|
31
|
+
import { lowerData } from '@uniweb/schemas/content'
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* A `data:` entry's schema ref — a ref string, or `{ schema }` — or null for an inline
|
|
@@ -282,10 +283,16 @@ export function extractRuntimeSchema(fullMeta, dataSchemaMap = {}) {
|
|
|
282
283
|
// with their refs, because the keys ARE what the section receives, and a key's ref is
|
|
283
284
|
// what a fetch of another name fills it by. `schemas` carries field defaults for the
|
|
284
285
|
// keys that have fields. `data: false` declares nothing, as no `data:` does.
|
|
285
|
-
|
|
286
|
+
//
|
|
287
|
+
// ⭐ A concept block's key is written as its fence is — `'md:faq': 'Questions and
|
|
288
|
+
// answers'` — and lowered to the key a component reads, `faq`, with no schema: the value
|
|
289
|
+
// is the author's label, never a ref (`lowerData`; ruled 2026-09-29). A built entry is
|
|
290
|
+
// already lowered; this holds for a `meta.js` handed in as written.
|
|
291
|
+
const data = lowerData(fullMeta.data)
|
|
292
|
+
if (data === false) {
|
|
286
293
|
// declares no key
|
|
287
|
-
} else if (
|
|
288
|
-
for (const [key, value] of Object.entries(
|
|
294
|
+
} else if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
295
|
+
for (const [key, value] of Object.entries(data)) {
|
|
289
296
|
// ⛔ A value that is no schema at all declares a key by accident — it would reach the
|
|
290
297
|
// component as `null`, silently. `data: { inherit: [...] }` — the opt-in to cascaded
|
|
291
298
|
// data from 2026-01-31 until declared keys replaced it — is the shape this refuses: it
|
|
@@ -307,9 +314,9 @@ export function extractRuntimeSchema(fullMeta, dataSchemaMap = {}) {
|
|
|
307
314
|
runtime.schemas[key] = lean
|
|
308
315
|
}
|
|
309
316
|
}
|
|
310
|
-
} else if (
|
|
317
|
+
} else if (data !== undefined) {
|
|
311
318
|
throw new Error(
|
|
312
|
-
`[uniweb] Invalid 'data' in meta.js: expected false or { <key>: <schema> }, got ${JSON.stringify(
|
|
319
|
+
`[uniweb] Invalid 'data' in meta.js: expected false or { <key>: <schema> }, got ${JSON.stringify(data)}. ` +
|
|
313
320
|
"A <schema> is a named ref ('@/x'), an inline field map, or a rich-form { fields: [...] }."
|
|
314
321
|
)
|
|
315
322
|
}
|
package/src/schema.js
CHANGED
|
@@ -16,7 +16,8 @@ import { createHash } from 'node:crypto'
|
|
|
16
16
|
import { isFontVar } from '@uniweb/theming'
|
|
17
17
|
import { join, dirname, extname, basename } from 'node:path'
|
|
18
18
|
import { pathToFileURL } from 'node:url'
|
|
19
|
-
import { describeChildren, describeVisuals } from '@uniweb/schemas/component'
|
|
19
|
+
import { describeChildren, describeVisuals, lowerChildren } from '@uniweb/schemas/component'
|
|
20
|
+
import { describeContent, lowerData } from '@uniweb/schemas/content'
|
|
20
21
|
import { SECTION_PARAMS, SECTION_KEYS } from '@uniweb/schemas/section'
|
|
21
22
|
import { collectSchemaRefs, buildDataSchemaMap, ownSchemaRefs } from './resolve-data-schema.js'
|
|
22
23
|
import {
|
|
@@ -47,6 +48,20 @@ const SECTIONS_PATH = 'sections'
|
|
|
47
48
|
// The layouts path where layout components are discovered
|
|
48
49
|
export const LAYOUTS_PATH = 'layouts'
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Which form a foundation schema's entries hold — `_self.schemaFormat`.
|
|
53
|
+
*
|
|
54
|
+
* (absent) 1 — each entry is its `meta.js`, copied as written
|
|
55
|
+
* 2 `content:` and `children:` lowered (2026-09-29): `content` is the canonical
|
|
56
|
+
* list `describeContent` returns, concept blocks included, and `children` the
|
|
57
|
+
* object form `lowerChildren` returns; `data:` keys are the keys a component
|
|
58
|
+
* reads (`lowerData`). `visuals` and the `content:` element `data` are refused.
|
|
59
|
+
*
|
|
60
|
+
* ⭐ A registered version never changes, so a reader holding one learns its form here,
|
|
61
|
+
* before the form ever changes again — never by sniffing a key's shape.
|
|
62
|
+
*/
|
|
63
|
+
export const SCHEMA_FORMAT = 2
|
|
64
|
+
|
|
50
65
|
/**
|
|
51
66
|
* Load a meta.js file via dynamic import — as it is on disk now.
|
|
52
67
|
*
|
|
@@ -552,9 +567,17 @@ function createImplicitMeta() {
|
|
|
552
567
|
return {}
|
|
553
568
|
}
|
|
554
569
|
|
|
570
|
+
/**
|
|
571
|
+
* What lowering an entry could not read — said once, when the schema is built
|
|
572
|
+
* (`reportPlacementDeclarations`), rather than at every discovery: the entry is also
|
|
573
|
+
* discovered to generate the foundation's entry file.
|
|
574
|
+
*/
|
|
575
|
+
const loweringProblems = new WeakMap()
|
|
576
|
+
|
|
555
577
|
/**
|
|
556
578
|
* Build a component entry: the `meta.js` default export, plus the build's `name`
|
|
557
|
-
* and `path
|
|
579
|
+
* and `path`, with its declarations LOWERED — the form a foundation registers
|
|
580
|
+
* (`SCHEMA_FORMAT`).
|
|
558
581
|
*
|
|
559
582
|
* ⭐ `title` IS THERE ONLY WHEN THE DEVELOPER WROTE ONE, so its absence says the
|
|
560
583
|
* component is unnamed. That difference decides whether a name can be translated:
|
|
@@ -566,17 +589,73 @@ function createImplicitMeta() {
|
|
|
566
589
|
* ⛔ Until 2026-09-27 the build filled `title` from the name whenever `meta.js`
|
|
567
590
|
* declared none, and added `titleInferred: true` to restore the fact the filling-in
|
|
568
591
|
* erased. Consumers derive the title where they use it instead [Diego, 2026-09-27].
|
|
592
|
+
*
|
|
593
|
+
* ⭐ `content:` and `children:` ARE LOWERED, and `data:` with them (ruled 2026-09-29):
|
|
594
|
+
* `content` becomes the canonical list `describeContent` returns — `image:` and
|
|
595
|
+
* `videos:` as the `media` element, a count as `min` / `max`, a label only where one
|
|
596
|
+
* was written, and each `'md:<tag>'` concept block as `{ key, kind: 'concept' }` —
|
|
597
|
+
* `children` the object form `lowerChildren` returns, and a `'md:<tag>'` key in `data:`
|
|
598
|
+
* the key a component reads (`lowerData`), so the lean runtime schema delivers
|
|
599
|
+
* `content.data.<tag>`. A reader of the registered schema needs no framework grammar.
|
|
600
|
+
* ⛔ Until then every key was copied as written.
|
|
569
601
|
*/
|
|
570
602
|
function buildComponentEntry(name, relativePath, meta) {
|
|
603
|
+
refuseRetired(name, meta)
|
|
604
|
+
|
|
571
605
|
// ⭐ `name` and `path` are the build's facts, so they are set AFTER the spread: a
|
|
572
606
|
// `meta.js` declaring either cannot change them. ⛔ Until 2026-09-27 they came first,
|
|
573
607
|
// and a `meta.js` `name` became the entry's `name` — which an editor reads as the
|
|
574
608
|
// section type — while the schema stayed keyed by the folder's name.
|
|
575
|
-
|
|
609
|
+
const entry = {
|
|
576
610
|
...meta,
|
|
577
611
|
name,
|
|
578
612
|
path: relativePath,
|
|
579
613
|
}
|
|
614
|
+
|
|
615
|
+
const content = describeContent(meta)
|
|
616
|
+
if (content.elements.length > 0) entry.content = content.elements
|
|
617
|
+
else delete entry.content
|
|
618
|
+
|
|
619
|
+
const children = lowerChildren(meta)
|
|
620
|
+
if (children) entry.children = children
|
|
621
|
+
else delete entry.children
|
|
622
|
+
|
|
623
|
+
if (meta.data !== undefined) entry.data = lowerData(meta.data)
|
|
624
|
+
|
|
625
|
+
const problems = [...content.problems, ...describeChildren(meta).problems]
|
|
626
|
+
if (problems.length > 0) loweringProblems.set(entry, problems)
|
|
627
|
+
return entry
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Refuse a declaration that was retired, naming what replaces it. A refusal, not a
|
|
632
|
+
* warning: a retired key would otherwise register as if it still meant something.
|
|
633
|
+
*/
|
|
634
|
+
function refuseRetired(name, meta) {
|
|
635
|
+
if (meta.visuals !== undefined) {
|
|
636
|
+
// ⛔ Retired 2026-09-29: the slot is the `media` element of `content:`.
|
|
637
|
+
const { max, types } = describeVisuals(meta)
|
|
638
|
+
const count = max === null ? '' : ` [0-${max}]`
|
|
639
|
+
const suggestion = types
|
|
640
|
+
? `media: { label: 'Visual${count}', types: ${JSON.stringify(types).replace(/"/g, "'")} }`
|
|
641
|
+
: `media: 'Visual${count}'`
|
|
642
|
+
throw new Error(
|
|
643
|
+
`[uniweb] ${name} (meta.js): \`visuals\` is retired. Declare the slot in \`content:\` as \`media\` — ` +
|
|
644
|
+
`an image, a video, or an embedded component — here, \`${suggestion}\`.`
|
|
645
|
+
)
|
|
646
|
+
}
|
|
647
|
+
if (meta.content && typeof meta.content === 'object' && meta.content.data !== undefined) {
|
|
648
|
+
// ⛔ Retired 2026-09-29: it named no tag, and declaring it delivered nothing.
|
|
649
|
+
const declared = Object.keys(meta.data && typeof meta.data === 'object' ? meta.data : {})
|
|
650
|
+
throw new Error(
|
|
651
|
+
`[uniweb] ${name} (meta.js): the \`content:\` element \`data\` is retired — it named no tag. ` +
|
|
652
|
+
`Declare the block's tag as a key in \`data:\`: \`'md:<tag>': '<label>'\` for a concept block ` +
|
|
653
|
+
`(\`\`\`md:<tag>), \`<tag>: {}\` for a data block.` +
|
|
654
|
+
(declared.length > 0
|
|
655
|
+
? ` ${name} already declares \`data: { ${declared.join(', ')} }\`, so remove \`content.data\`.`
|
|
656
|
+
: '')
|
|
657
|
+
)
|
|
658
|
+
}
|
|
580
659
|
}
|
|
581
660
|
|
|
582
661
|
/**
|
|
@@ -932,9 +1011,9 @@ function reportSupports(srcDir, authored, derived, emitted) {
|
|
|
932
1011
|
}
|
|
933
1012
|
|
|
934
1013
|
/**
|
|
935
|
-
* Warn about `
|
|
936
|
-
* `@uniweb/schemas`, the grammar the editor reads them with. The
|
|
937
|
-
*
|
|
1014
|
+
* Warn about `content` and `children` declarations an editor cannot use — what lowering
|
|
1015
|
+
* them could not read (`@uniweb/schemas`, the grammar the editor reads them with). The
|
|
1016
|
+
* entry is emitted lowered either way; these are warnings, never refusals.
|
|
938
1017
|
*
|
|
939
1018
|
* ⭐ A `children.types` entry must name one of this foundation's section types. A hidden
|
|
940
1019
|
* one is not a section type at all, and a child of a type nothing provides renders
|
|
@@ -945,9 +1024,15 @@ function reportSupports(srcDir, authored, derived, emitted) {
|
|
|
945
1024
|
*/
|
|
946
1025
|
export function reportPlacementDeclarations(components) {
|
|
947
1026
|
for (const [name, entry] of Object.entries(components)) {
|
|
1027
|
+
// An entry built here was lowered, and kept what it could not read aside; one handed
|
|
1028
|
+
// in as written is read now. (A lowered entry reads as itself, with no problems.)
|
|
948
1029
|
const children = describeChildren(entry)
|
|
949
|
-
const
|
|
950
|
-
|
|
1030
|
+
const problems = [
|
|
1031
|
+
...(loweringProblems.get(entry) || []),
|
|
1032
|
+
...describeContent(entry).problems,
|
|
1033
|
+
...children.problems,
|
|
1034
|
+
]
|
|
1035
|
+
for (const problem of problems) {
|
|
951
1036
|
console.warn(`Warning: ${name} (meta.js): ${problem}`)
|
|
952
1037
|
}
|
|
953
1038
|
for (const type of children.types || []) {
|
|
@@ -1082,6 +1167,8 @@ export async function buildSchema(srcDir, sectionPaths, derivedSupports = null)
|
|
|
1082
1167
|
...(foundationConfig.description && { description: foundationConfig.description }),
|
|
1083
1168
|
...(foundationConfig.defaultLayout && { defaultLayout: foundationConfig.defaultLayout }),
|
|
1084
1169
|
...(isExtension && { role: 'extension' }),
|
|
1170
|
+
// The build's fact, last: which form the entries below hold.
|
|
1171
|
+
schemaFormat: SCHEMA_FORMAT,
|
|
1085
1172
|
},
|
|
1086
1173
|
// Layout metadata (full, for editor)
|
|
1087
1174
|
...(Object.keys(dataSchemas).length > 0 && { dataSchemas }),
|
|
@@ -14,6 +14,7 @@ import { existsSync } from 'node:fs'
|
|
|
14
14
|
import { join, basename, extname, dirname } from 'node:path'
|
|
15
15
|
import { createHash } from 'node:crypto'
|
|
16
16
|
import sharp from 'sharp'
|
|
17
|
+
import { mimeFor } from '@uniweb/semantic-parser'
|
|
17
18
|
|
|
18
19
|
// Image formats that can be converted to WebP
|
|
19
20
|
const CONVERTIBLE_FORMATS = ['.png', '.jpg', '.jpeg', '.gif']
|
|
@@ -157,10 +158,15 @@ export async function processAsset(asset, options = {}) {
|
|
|
157
158
|
* @param {Object} assetManifest - Asset manifest from content collector
|
|
158
159
|
* @param {Object} options - Processing options
|
|
159
160
|
* @param {string} [options.basePath='/'] - Site base path for subdirectory deployments
|
|
160
|
-
* @returns {Promise<Object>}
|
|
161
|
+
* @returns {Promise<Object>} `pathMapping` — original paths to output URLs — `results`, and
|
|
162
|
+
* `files`: what the build knows of each file it copied, by original path (`name`, `size`, `mime`)
|
|
161
163
|
*/
|
|
162
164
|
export async function processAssets(assetManifest, options = {}) {
|
|
163
165
|
const pathMapping = {}
|
|
166
|
+
// ⭐ A copied file is renamed `{name}-{hash}{ext}`, so its new address no longer names it. A
|
|
167
|
+
// document carries its file's `name`, `mime` and `size` (a file record's field names), and this
|
|
168
|
+
// is the one place that knows all three for a local file — `rewriteContentPaths` stamps them.
|
|
169
|
+
const files = {}
|
|
164
170
|
const results = {
|
|
165
171
|
processed: 0,
|
|
166
172
|
converted: 0,
|
|
@@ -175,6 +181,7 @@ export async function processAssets(assetManifest, options = {}) {
|
|
|
175
181
|
pathMapping[originalPath] = result.output
|
|
176
182
|
|
|
177
183
|
if (result.processed) {
|
|
184
|
+
files[originalPath] = { name: basename(asset.resolved), size: result.size, mime: mimeFor(result.outputPath) }
|
|
178
185
|
results.processed++
|
|
179
186
|
results.totalSize += result.size || 0
|
|
180
187
|
if (result.converted) {
|
|
@@ -185,7 +192,7 @@ export async function processAssets(assetManifest, options = {}) {
|
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
return { pathMapping, results }
|
|
195
|
+
return { pathMapping, results, files }
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
/**
|
|
@@ -220,9 +227,10 @@ function rewriteDataPaths(data, pathMapping) {
|
|
|
220
227
|
*
|
|
221
228
|
* @param {Object} content - ProseMirror document
|
|
222
229
|
* @param {Object} pathMapping - Map of original paths to new paths
|
|
230
|
+
* @param {Object} [files] - what the build knows of each file it copied (`processAssets`)
|
|
223
231
|
* @returns {Object} Content with rewritten paths
|
|
224
232
|
*/
|
|
225
|
-
export function rewriteContentPaths(content, pathMapping) {
|
|
233
|
+
export function rewriteContentPaths(content, pathMapping, files = null) {
|
|
226
234
|
if (!content) return content
|
|
227
235
|
|
|
228
236
|
// Deep clone to avoid mutating original
|
|
@@ -231,11 +239,26 @@ export function rewriteContentPaths(content, pathMapping) {
|
|
|
231
239
|
function walk(node) {
|
|
232
240
|
if (!node) return
|
|
233
241
|
|
|
234
|
-
// Rewrite image src
|
|
242
|
+
// Rewrite image src — and a video's `poster` / a document's `preview`, which
|
|
243
|
+
// the collector processes as assets of their own (`walkContentAssets`). Only
|
|
244
|
+
// `src` was rewritten until 2026-09-29, so an explicit poster was emitted to
|
|
245
|
+
// dist/assets/ while the page still pointed at its source path.
|
|
235
246
|
if (node.type === 'image' && node.attrs?.src) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
node.attrs
|
|
247
|
+
const original = node.attrs.src
|
|
248
|
+
for (const attr of ['src', 'poster', 'preview']) {
|
|
249
|
+
const newPath = node.attrs[attr] && pathMapping[node.attrs[attr]]
|
|
250
|
+
if (newPath) {
|
|
251
|
+
node.attrs[attr] = newPath
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// A document whose file the build copied: the file's name as the author gave it — the
|
|
255
|
+
// address now holds a hashed one — its size, and its type (2026-09-29). Only on the built
|
|
256
|
+
// output, never on what an author stored; a value written on the node stays.
|
|
257
|
+
const file = node.attrs.role === 'pdf' ? files?.[original] : null
|
|
258
|
+
if (file) {
|
|
259
|
+
for (const key of ['name', 'mime', 'size']) {
|
|
260
|
+
if (node.attrs[key] === undefined && file[key] !== undefined) node.attrs[key] = file[key]
|
|
261
|
+
}
|
|
239
262
|
}
|
|
240
263
|
}
|
|
241
264
|
|
|
@@ -305,15 +328,16 @@ export function rewriteParamPaths(params, pathMapping) {
|
|
|
305
328
|
*
|
|
306
329
|
* @param {Object} siteContent - Full site content object
|
|
307
330
|
* @param {Object} pathMapping - Map of original paths to new paths
|
|
331
|
+
* @param {Object} [files] - what the build knows of each file it copied (`processAssets`)
|
|
308
332
|
* @returns {Object} Site content with rewritten paths
|
|
309
333
|
*/
|
|
310
|
-
export function rewriteSiteContentPaths(siteContent, pathMapping) {
|
|
334
|
+
export function rewriteSiteContentPaths(siteContent, pathMapping, files = null) {
|
|
311
335
|
// Deep clone
|
|
312
336
|
const result = JSON.parse(JSON.stringify(siteContent))
|
|
313
337
|
|
|
314
338
|
function processSection(section) {
|
|
315
339
|
if (section.content) {
|
|
316
|
-
section.content = rewriteContentPaths(section.content, pathMapping)
|
|
340
|
+
section.content = rewriteContentPaths(section.content, pathMapping, files)
|
|
317
341
|
}
|
|
318
342
|
if (section.params) {
|
|
319
343
|
section.params = rewriteParamPaths(section.params, pathMapping)
|
package/src/site/assets.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* act as implicit imports and should be processed/optimized during build.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { join, dirname, isAbsolute, normalize } from 'node:path'
|
|
11
|
+
import { join, dirname, isAbsolute, normalize, relative, sep } from 'node:path'
|
|
12
12
|
import { existsSync } from 'node:fs'
|
|
13
13
|
|
|
14
14
|
// Image extensions we should process
|
|
@@ -107,21 +107,23 @@ export function isMediaFieldReference(value) {
|
|
|
107
107
|
* @param {any} data - Parsed JSON/YAML data
|
|
108
108
|
* @param {Function} visitor - Callback for each asset path: (path) => void
|
|
109
109
|
*/
|
|
110
|
-
function walkDataAssets(data, visitor) {
|
|
110
|
+
function walkDataAssets(data, visitor, set) {
|
|
111
111
|
if (typeof data === 'string') {
|
|
112
112
|
if (isLocalAssetPath(data)) {
|
|
113
|
-
visitor(data)
|
|
113
|
+
visitor(data, set)
|
|
114
114
|
}
|
|
115
115
|
return
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
if (Array.isArray(data)) {
|
|
119
|
-
data.forEach(item => walkDataAssets(item, visitor))
|
|
119
|
+
data.forEach((item, i) => walkDataAssets(item, visitor, (v) => { data[i] = v }))
|
|
120
120
|
return
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
if (data && typeof data === 'object') {
|
|
124
|
-
|
|
124
|
+
for (const [key, value] of Object.entries(data)) {
|
|
125
|
+
walkDataAssets(value, visitor, (v) => { data[key] = v })
|
|
126
|
+
}
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -137,7 +139,8 @@ function walkDataBlockAssets(doc, visitor) {
|
|
|
137
139
|
|
|
138
140
|
// dataBlock nodes have pre-parsed data in attrs.data
|
|
139
141
|
if (doc.type === 'dataBlock' && doc.attrs?.data) {
|
|
140
|
-
|
|
142
|
+
const attrs = doc.attrs
|
|
143
|
+
walkDataAssets(attrs.data, visitor, (v) => { attrs.data = v })
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
// Recurse into content
|
|
@@ -203,8 +206,9 @@ export function resolveAssetPath(src, contextPath, siteRoot) {
|
|
|
203
206
|
* Walk a ProseMirror document and collect all asset references
|
|
204
207
|
*
|
|
205
208
|
* @param {Object} doc - ProseMirror document
|
|
206
|
-
* @param {Function} visitor - Callback for each asset: (node, path, attrName) => void
|
|
207
|
-
* attrName is 'src', 'poster', or 'preview'
|
|
209
|
+
* @param {Function} visitor - Callback for each asset: (node, path, attrName, holder) => void
|
|
210
|
+
* attrName is 'src', 'poster', or 'preview'; `holder` is
|
|
211
|
+
* the attrs object the reference lives on, at `holder[attrName]`
|
|
208
212
|
* @param {string} [path=''] - Current path in document (for debugging)
|
|
209
213
|
*/
|
|
210
214
|
export function walkContentAssets(doc, visitor, path = '') {
|
|
@@ -212,14 +216,14 @@ export function walkContentAssets(doc, visitor, path = '') {
|
|
|
212
216
|
|
|
213
217
|
// Check for image nodes
|
|
214
218
|
if (doc.type === 'image' && doc.attrs?.src) {
|
|
215
|
-
visitor(doc, path, 'src')
|
|
219
|
+
visitor(doc, path, 'src', doc.attrs)
|
|
216
220
|
|
|
217
221
|
// Also collect explicit poster/preview attributes as assets
|
|
218
222
|
if (doc.attrs.poster && !isExternalUrl(doc.attrs.poster)) {
|
|
219
|
-
visitor({ type: 'image', attrs: { src: doc.attrs.poster } }, path, 'poster')
|
|
223
|
+
visitor({ type: 'image', attrs: { src: doc.attrs.poster } }, path, 'poster', doc.attrs)
|
|
220
224
|
}
|
|
221
225
|
if (doc.attrs.preview && !isExternalUrl(doc.attrs.preview)) {
|
|
222
|
-
visitor({ type: 'image', attrs: { src: doc.attrs.preview } }, path, 'preview')
|
|
226
|
+
visitor({ type: 'image', attrs: { src: doc.attrs.preview } }, path, 'preview', doc.attrs)
|
|
223
227
|
}
|
|
224
228
|
}
|
|
225
229
|
|
|
@@ -234,7 +238,7 @@ export function walkContentAssets(doc, visitor, path = '') {
|
|
|
234
238
|
if (doc.marks && Array.isArray(doc.marks)) {
|
|
235
239
|
doc.marks.forEach((mark, index) => {
|
|
236
240
|
if (mark.attrs?.src) {
|
|
237
|
-
visitor(mark, `${path}/marks[${index}]`, 'src')
|
|
241
|
+
visitor(mark, `${path}/marks[${index}]`, 'src', mark.attrs)
|
|
238
242
|
}
|
|
239
243
|
})
|
|
240
244
|
}
|
|
@@ -250,42 +254,45 @@ export function walkContentAssets(doc, visitor, path = '') {
|
|
|
250
254
|
* - assets: Asset manifest mapping original paths to resolved info
|
|
251
255
|
* - hasExplicitPoster: Set of video src paths that have explicit poster attributes
|
|
252
256
|
* - hasExplicitPreview: Set of PDF src paths that have explicit preview attributes
|
|
257
|
+
* - uses: every co-located reference, with the file it resolved to and a setter
|
|
258
|
+
* for the place it is written — what `disambiguateAssetRefs` needs to rename
|
|
259
|
+
* one. In memory only; never part of the site content.
|
|
253
260
|
*/
|
|
254
261
|
export function collectSectionAssets(section, markdownPath, siteRoot) {
|
|
255
262
|
const assets = {}
|
|
256
263
|
const hasExplicitPoster = new Set()
|
|
257
264
|
const hasExplicitPreview = new Set()
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
265
|
+
const uses = []
|
|
266
|
+
|
|
267
|
+
// One entry per reference. `set` writes a new reference where this one stands;
|
|
268
|
+
// `flags` carries the explicit-poster/preview marks a renamed video/PDF must keep.
|
|
269
|
+
const add = (ref, set, flags = null) => {
|
|
270
|
+
const result = resolveAssetPath(ref, markdownPath, siteRoot)
|
|
271
|
+
if (result.external || !result.resolved) return
|
|
272
|
+
assets[ref] = {
|
|
273
|
+
original: ref,
|
|
274
|
+
resolved: result.resolved,
|
|
275
|
+
isImage: result.isImage,
|
|
276
|
+
isVideo: result.isVideo,
|
|
277
|
+
isPdf: result.isPdf
|
|
278
|
+
}
|
|
279
|
+
// Only a co-located ref depends on WHERE it is written, so only it can name
|
|
280
|
+
// two different files with one string. A site-root ref (`/x.png`) cannot.
|
|
281
|
+
if (!ref.startsWith('/') && set) uses.push({ ref, resolved: result.resolved, set, flags })
|
|
282
|
+
}
|
|
261
283
|
|
|
262
284
|
// Collect from ProseMirror content
|
|
263
285
|
if (section.content) {
|
|
264
|
-
walkContentAssets(section.content, (node, path, attrName) => {
|
|
265
|
-
const
|
|
266
|
-
|
|
286
|
+
walkContentAssets(section.content, (node, path, attrName, holder) => {
|
|
287
|
+
const ref = node.attrs.src
|
|
288
|
+
let flags = null
|
|
267
289
|
if (attrName === 'src') {
|
|
268
|
-
// Main src attribute - track it for potential poster/preview
|
|
269
|
-
currentImageSrc = node.attrs.src
|
|
270
|
-
|
|
271
290
|
// Check if this image has explicit poster/preview
|
|
272
|
-
if (node.attrs.poster)
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
if (node.attrs.preview) {
|
|
276
|
-
hasExplicitPreview.add(node.attrs.src)
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (!result.external && result.resolved) {
|
|
281
|
-
assets[node.attrs.src] = {
|
|
282
|
-
original: node.attrs.src,
|
|
283
|
-
resolved: result.resolved,
|
|
284
|
-
isImage: result.isImage,
|
|
285
|
-
isVideo: result.isVideo,
|
|
286
|
-
isPdf: result.isPdf
|
|
287
|
-
}
|
|
291
|
+
if (node.attrs.poster) hasExplicitPoster.add(ref)
|
|
292
|
+
if (node.attrs.preview) hasExplicitPreview.add(ref)
|
|
293
|
+
flags = { poster: !!node.attrs.poster, preview: !!node.attrs.preview }
|
|
288
294
|
}
|
|
295
|
+
add(ref, holder ? (v) => { holder[attrName] = v } : null, flags)
|
|
289
296
|
})
|
|
290
297
|
}
|
|
291
298
|
|
|
@@ -301,16 +308,7 @@ export function collectSectionAssets(section, markdownPath, siteRoot) {
|
|
|
301
308
|
// Only resolve values that are actually asset references. A dual-use field
|
|
302
309
|
// like `background: gray` (a CSS color) must not be treated as a file path.
|
|
303
310
|
if (isMediaFieldReference(value)) {
|
|
304
|
-
|
|
305
|
-
if (!result.external && result.resolved) {
|
|
306
|
-
assets[value] = {
|
|
307
|
-
original: value,
|
|
308
|
-
resolved: result.resolved,
|
|
309
|
-
isImage: result.isImage,
|
|
310
|
-
isVideo: result.isVideo,
|
|
311
|
-
isPdf: result.isPdf
|
|
312
|
-
}
|
|
313
|
-
}
|
|
311
|
+
add(value, (v) => { section.params[field] = v })
|
|
314
312
|
}
|
|
315
313
|
}
|
|
316
314
|
|
|
@@ -318,40 +316,66 @@ export function collectSectionAssets(section, markdownPath, siteRoot) {
|
|
|
318
316
|
// Background can be { image: { src }, video: { src } } with nested asset paths
|
|
319
317
|
const bg = section.params?.background
|
|
320
318
|
if (bg && typeof bg === 'object') {
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
if (typeof src === 'string') {
|
|
324
|
-
const result = resolveAssetPath(src, markdownPath, siteRoot)
|
|
325
|
-
if (!result.external && result.resolved) {
|
|
326
|
-
assets[src] = {
|
|
327
|
-
original: src,
|
|
328
|
-
resolved: result.resolved,
|
|
329
|
-
isImage: result.isImage,
|
|
330
|
-
isVideo: result.isVideo,
|
|
331
|
-
isPdf: result.isPdf
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
319
|
+
for (const media of [bg.image, bg.video]) {
|
|
320
|
+
if (typeof media?.src === 'string') add(media.src, (v) => { media.src = v })
|
|
335
321
|
}
|
|
336
322
|
}
|
|
337
323
|
|
|
338
324
|
// Collect from tagged code blocks (JSON/YAML data)
|
|
339
325
|
if (section.content) {
|
|
340
|
-
walkDataBlockAssets(section.content, (assetPath) =>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
326
|
+
walkDataBlockAssets(section.content, (assetPath, set) => add(assetPath, set))
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { assets, hasExplicitPoster, hasExplicitPreview, uses }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Give every co-located reference that names more than one file its own key.
|
|
334
|
+
*
|
|
335
|
+
* The manifest is keyed by the reference AS WRITTEN, site-wide, and a co-located
|
|
336
|
+
* ref means a different file in every folder — so two pages that both wrote
|
|
337
|
+
* `./media/shot.png` shared one entry: the last one collected won, and both pages
|
|
338
|
+
* rendered its image. Each colliding use is renamed to its file's path from the
|
|
339
|
+
* site root (`./pages/alpha/media/shot.png`, or `../…` for a mounted folder
|
|
340
|
+
* outside it), which is one key per file by construction and keeps the extension
|
|
341
|
+
* the later steps classify by. A ref that names one file keeps its key, so a
|
|
342
|
+
* site without a collision is unchanged.
|
|
343
|
+
*
|
|
344
|
+
* Call it on the merged page + layout collection, before the site config's assets
|
|
345
|
+
* (which have no written place to rename) are merged in.
|
|
346
|
+
*
|
|
347
|
+
* @param {Object} collection - merged asset collection (mutated in place)
|
|
348
|
+
* @param {string} siteRoot - Site root directory
|
|
349
|
+
* @returns {number} how many references were renamed
|
|
350
|
+
*/
|
|
351
|
+
export function disambiguateAssetRefs(collection, siteRoot) {
|
|
352
|
+
const uses = collection.uses || []
|
|
353
|
+
const filesByRef = new Map()
|
|
354
|
+
for (const use of uses) {
|
|
355
|
+
if (!filesByRef.has(use.ref)) filesByRef.set(use.ref, new Set())
|
|
356
|
+
filesByRef.get(use.ref).add(use.resolved)
|
|
352
357
|
}
|
|
353
358
|
|
|
354
|
-
|
|
359
|
+
let renamed = 0
|
|
360
|
+
for (const use of uses) {
|
|
361
|
+
if (filesByRef.get(use.ref).size < 2) continue
|
|
362
|
+
const fromRoot = relative(siteRoot, use.resolved).split(sep).join('/')
|
|
363
|
+
const key = fromRoot.startsWith('../') ? fromRoot : `./${fromRoot}`
|
|
364
|
+
const entry = collection.assets[use.ref]
|
|
365
|
+
collection.assets[key] = { ...entry, original: key, resolved: use.resolved }
|
|
366
|
+
if (use.flags?.poster) collection.hasExplicitPoster?.add(key)
|
|
367
|
+
if (use.flags?.preview) collection.hasExplicitPreview?.add(key)
|
|
368
|
+
use.set(key)
|
|
369
|
+
renamed++
|
|
370
|
+
}
|
|
371
|
+
// The shared key no longer names anything written in the content.
|
|
372
|
+
for (const [ref, files] of filesByRef) {
|
|
373
|
+
if (files.size < 2) continue
|
|
374
|
+
delete collection.assets[ref]
|
|
375
|
+
collection.hasExplicitPoster?.delete(ref)
|
|
376
|
+
collection.hasExplicitPreview?.delete(ref)
|
|
377
|
+
}
|
|
378
|
+
return renamed
|
|
355
379
|
}
|
|
356
380
|
|
|
357
381
|
/**
|
|
@@ -426,13 +450,15 @@ export function mergeAssetCollections(...collections) {
|
|
|
426
450
|
const merged = {
|
|
427
451
|
assets: {},
|
|
428
452
|
hasExplicitPoster: new Set(),
|
|
429
|
-
hasExplicitPreview: new Set()
|
|
453
|
+
hasExplicitPreview: new Set(),
|
|
454
|
+
uses: []
|
|
430
455
|
}
|
|
431
456
|
|
|
432
457
|
for (const collection of collections) {
|
|
433
458
|
// Handle both old format (plain object) and new format (with sets)
|
|
434
459
|
if (collection.assets) {
|
|
435
460
|
Object.assign(merged.assets, collection.assets)
|
|
461
|
+
if (collection.uses) merged.uses.push(...collection.uses)
|
|
436
462
|
collection.hasExplicitPoster?.forEach(p => merged.hasExplicitPoster.add(p))
|
|
437
463
|
collection.hasExplicitPreview?.forEach(p => merged.hasExplicitPreview.add(p))
|
|
438
464
|
} else {
|
|
@@ -173,7 +173,7 @@ export async function buildSiteData({
|
|
|
173
173
|
if (assetsOpts.process && siteContent?.assets) {
|
|
174
174
|
const assetCount = Object.keys(siteContent.assets).length
|
|
175
175
|
if (assetCount > 0) {
|
|
176
|
-
const { pathMapping } = await processAssets(siteContent.assets, {
|
|
176
|
+
const { pathMapping, files } = await processAssets(siteContent.assets, {
|
|
177
177
|
outputDir: resolvedDistDir,
|
|
178
178
|
assetsSubdir: assetsOpts.outputDir,
|
|
179
179
|
convertToWebp: assetsOpts.convertToWebp,
|
|
@@ -197,7 +197,7 @@ export async function buildSiteData({
|
|
|
197
197
|
}
|
|
198
198
|
)
|
|
199
199
|
|
|
200
|
-
finalContent = rewriteSiteContentPaths(siteContent, pathMapping)
|
|
200
|
+
finalContent = rewriteSiteContentPaths(siteContent, pathMapping, files)
|
|
201
201
|
if (Object.keys(posterMapping).length > 0 || Object.keys(thumbnailMapping).length > 0) {
|
|
202
202
|
finalContent._assetMeta = {
|
|
203
203
|
posters: posterMapping,
|
|
@@ -205,7 +205,7 @@ export async function buildSiteData({
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
} else {
|
|
208
|
-
finalContent = rewriteSiteContentPaths(siteContent, pathMapping)
|
|
208
|
+
finalContent = rewriteSiteContentPaths(siteContent, pathMapping, files)
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
} else {
|
|
@@ -31,7 +31,7 @@ import { join, parse, relative, resolve, sep } from 'node:path'
|
|
|
31
31
|
import { existsSync, statSync, realpathSync, readdirSync } from 'node:fs'
|
|
32
32
|
import yaml from 'js-yaml'
|
|
33
33
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
34
|
-
import { collectSectionAssets, mergeAssetCollections, collectConfigAssets } from './assets.js'
|
|
34
|
+
import { collectSectionAssets, mergeAssetCollections, collectConfigAssets, disambiguateAssetRefs } from './assets.js'
|
|
35
35
|
import { collectSectionIcons, mergeIconCollections, buildIconManifest } from './icons.js'
|
|
36
36
|
import { normalizeHideIn, dropUnpublishedPages, dropHiddenSections } from './nav-visibility.js'
|
|
37
37
|
import { parseFetchConfig, toFetchList } from './data-fetcher.js'
|
|
@@ -2592,6 +2592,12 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2592
2592
|
assetCollection = mergeAssetCollections(assetCollection, layoutAssets)
|
|
2593
2593
|
iconCollection = mergeIconCollections(iconCollection, layoutIcons)
|
|
2594
2594
|
|
|
2595
|
+
// One manifest key per file: a co-located ref written the same way in two
|
|
2596
|
+
// folders (`./media/shot.png`) names two files, and would otherwise share one
|
|
2597
|
+
// entry — every page rendering the last one collected. Before the config
|
|
2598
|
+
// assets, which have no written place to rename.
|
|
2599
|
+
disambiguateAssetRefs(assetCollection, sitePath)
|
|
2600
|
+
|
|
2595
2601
|
// Merge top-level config assets (e.g. document.yml's book.covers.front,
|
|
2596
2602
|
// banner images, logos) into the manifest. The compile pipeline reads
|
|
2597
2603
|
// website.assets[<original src>] to resolve these to filesystem paths
|
package/src/site/file-records.js
CHANGED
|
@@ -17,63 +17,16 @@
|
|
|
17
17
|
|
|
18
18
|
import { statSync } from 'node:fs'
|
|
19
19
|
import { extname } from 'node:path'
|
|
20
|
+
import { mimeFor } from '@uniweb/semantic-parser'
|
|
20
21
|
import { generatePdfThumbnail } from './advanced-processors.js'
|
|
21
22
|
|
|
22
23
|
export const FILE_MODEL = '@uniweb/file'
|
|
23
24
|
|
|
24
25
|
// What a file is, by its extension — for the `mime` a file record carries and the type its upload
|
|
25
|
-
// declares.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
30
|
-
xls: 'application/vnd.ms-excel',
|
|
31
|
-
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
32
|
-
ppt: 'application/vnd.ms-powerpoint',
|
|
33
|
-
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
34
|
-
odt: 'application/vnd.oasis.opendocument.text',
|
|
35
|
-
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
36
|
-
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
37
|
-
rtf: 'application/rtf',
|
|
38
|
-
epub: 'application/epub+zip',
|
|
39
|
-
zip: 'application/zip',
|
|
40
|
-
gz: 'application/gzip',
|
|
41
|
-
tar: 'application/x-tar',
|
|
42
|
-
csv: 'text/csv',
|
|
43
|
-
tsv: 'text/tab-separated-values',
|
|
44
|
-
txt: 'text/plain',
|
|
45
|
-
md: 'text/markdown',
|
|
46
|
-
json: 'application/json',
|
|
47
|
-
xml: 'application/xml',
|
|
48
|
-
yml: 'application/yaml',
|
|
49
|
-
yaml: 'application/yaml',
|
|
50
|
-
html: 'text/html',
|
|
51
|
-
png: 'image/png',
|
|
52
|
-
jpg: 'image/jpeg',
|
|
53
|
-
jpeg: 'image/jpeg',
|
|
54
|
-
gif: 'image/gif',
|
|
55
|
-
svg: 'image/svg+xml',
|
|
56
|
-
webp: 'image/webp',
|
|
57
|
-
avif: 'image/avif',
|
|
58
|
-
mp3: 'audio/mpeg',
|
|
59
|
-
wav: 'audio/wav',
|
|
60
|
-
ogg: 'audio/ogg',
|
|
61
|
-
m4a: 'audio/mp4',
|
|
62
|
-
mp4: 'video/mp4',
|
|
63
|
-
webm: 'video/webm',
|
|
64
|
-
mov: 'video/quicktime',
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The media type of a file, by its extension.
|
|
69
|
-
*
|
|
70
|
-
* @param {string} name - a file name or path
|
|
71
|
-
* @returns {string}
|
|
72
|
-
*/
|
|
73
|
-
export function mimeFor(name) {
|
|
74
|
-
const ext = extname(String(name || '')).slice(1).toLowerCase()
|
|
75
|
-
return MIME[ext] || 'application/octet-stream'
|
|
76
|
-
}
|
|
26
|
+
// declares. ⭐ The table is `@uniweb/semantic-parser`'s: the parser reads it for a document's `mime`,
|
|
27
|
+
// and could not reach one here — it has no dependencies. One table; anything not in it is
|
|
28
|
+
// `application/octet-stream`.
|
|
29
|
+
export { mimeFor }
|
|
77
30
|
|
|
78
31
|
/** Is this file a PDF — the one type a preview is made for? */
|
|
79
32
|
export function isPdfFile(name) {
|
package/src/site/plugin.js
CHANGED
|
@@ -1496,7 +1496,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
1496
1496
|
console.log(`[site-content] Processing ${assetCount} assets...`)
|
|
1497
1497
|
|
|
1498
1498
|
// Process standard assets (images)
|
|
1499
|
-
const { pathMapping, results } = await processAssets(siteContent.assets, {
|
|
1499
|
+
const { pathMapping, results, files } = await processAssets(siteContent.assets, {
|
|
1500
1500
|
outputDir: resolvedOutDir,
|
|
1501
1501
|
assetsSubdir: assetsOptions.outputDir,
|
|
1502
1502
|
convertToWebp: assetsOptions.convertToWebp,
|
|
@@ -1542,7 +1542,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
1542
1542
|
// Merge poster and thumbnail mappings into the path mapping
|
|
1543
1543
|
// Videos: add a .poster property to the content (not replace the src)
|
|
1544
1544
|
// PDFs: add a .thumbnail property to the content
|
|
1545
|
-
finalContent = rewriteSiteContentPaths(siteContent, pathMapping)
|
|
1545
|
+
finalContent = rewriteSiteContentPaths(siteContent, pathMapping, files)
|
|
1546
1546
|
|
|
1547
1547
|
// Add poster and thumbnail metadata to the site content
|
|
1548
1548
|
if (Object.keys(posterMapping).length > 0 || Object.keys(thumbnailMapping).length > 0) {
|
|
@@ -1553,7 +1553,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
1553
1553
|
}
|
|
1554
1554
|
} else {
|
|
1555
1555
|
// Rewrite paths in content
|
|
1556
|
-
finalContent = rewriteSiteContentPaths(siteContent, pathMapping)
|
|
1556
|
+
finalContent = rewriteSiteContentPaths(siteContent, pathMapping, files)
|
|
1557
1557
|
}
|
|
1558
1558
|
|
|
1559
1559
|
// Log results
|