54 lines
2.1 KiB
JavaScript
54 lines
2.1 KiB
JavaScript
|
|
// Re-point every asset reference at the site's origin, and refuse to emit anything that
|
||
|
|
// would 404. A silently-missing image is precisely how the page goes back to lying about
|
||
|
|
// its own content — which is the failure this whole projection exists to end.
|
||
|
|
import { writeFile, mkdir, copyFile } from 'node:fs/promises'
|
||
|
|
import { existsSync } from 'node:fs'
|
||
|
|
import { basename, join } from 'node:path'
|
||
|
|
import { DIST, postcss, publicBase, publicDir } from './paths.mjs'
|
||
|
|
|
||
|
|
export async function projectAssets(html, css, id, { apply = true } = {}) {
|
||
|
|
const base = `${publicBase(id)}/assets`
|
||
|
|
const wanted = new Map()
|
||
|
|
const unresolved = []
|
||
|
|
|
||
|
|
const claim = (ref) => {
|
||
|
|
const clean = ref.trim().replace(/^["']|["']$/g, '')
|
||
|
|
if (/^(data:|https?:|#|mailto:)/i.test(clean)) return null // external / inline: not ours
|
||
|
|
if (!clean.startsWith('/')) { unresolved.push({ ref: clean, why: 'referencia relativa, sin base' }); return null }
|
||
|
|
|
||
|
|
const onDisk = join(DIST, clean)
|
||
|
|
if (!existsSync(onDisk)) { unresolved.push({ ref: clean, why: `no está en dist/` }); return null }
|
||
|
|
|
||
|
|
const pub = `${base}/${basename(clean)}`
|
||
|
|
wanted.set(clean, { onDisk, pub })
|
||
|
|
return pub
|
||
|
|
}
|
||
|
|
|
||
|
|
html = html
|
||
|
|
.replace(/(<img[^>]+src=")([^"]+)(")/g, (m, a, ref, z) => { const p = claim(ref); return p ? a + p + z : m })
|
||
|
|
.replace(/(background-image:\s*url\()([^)]+)(\))/g, (m, a, ref, z) => { const p = claim(ref); return p ? a + p + z : m })
|
||
|
|
|
||
|
|
const root = postcss.parse(css)
|
||
|
|
root.walkDecls(d => {
|
||
|
|
if (!d.value.includes('url(')) return
|
||
|
|
d.value = d.value.replace(/url\(([^)]+)\)/g, (m, ref) => {
|
||
|
|
const p = claim(ref)
|
||
|
|
return p ? `url("${p}")` : m
|
||
|
|
})
|
||
|
|
})
|
||
|
|
css = root.toString()
|
||
|
|
|
||
|
|
if (unresolved.length) {
|
||
|
|
const lines = unresolved.map(u => ` ${u.ref} — ${u.why}`).join('\n')
|
||
|
|
throw new Error(`${unresolved.length} referencia(s) sin resolver; no se emite un deck con assets rotos:\n${lines}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (apply) {
|
||
|
|
const dest = join(publicDir(id), 'assets')
|
||
|
|
await mkdir(dest, { recursive: true })
|
||
|
|
for (const [, v] of wanted) await copyFile(v.onDisk, join(dest, basename(v.pub)))
|
||
|
|
}
|
||
|
|
|
||
|
|
return { html, css, copied: [...wanted.values()].map(v => v.pub) }
|
||
|
|
}
|