mirror of https://github.com/requarks/wiki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
192 lines
7.0 KiB
192 lines
7.0 KiB
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
import summary from 'rollup-plugin-summary'
|
|
import terser from '@rollup/plugin-terser'
|
|
import resolve from '@rollup/plugin-node-resolve'
|
|
import commonjs from '@rollup/plugin-commonjs'
|
|
|
|
import * as glob from 'glob'
|
|
|
|
/**
|
|
* Turn an ESTree literal node into a plain JS value.
|
|
*
|
|
* Only literals, arrays and objects of literals are supported — a block definition is metadata, so
|
|
* anything computed is a mistake worth failing the build over.
|
|
*/
|
|
function literalToValue (node, blockDir) {
|
|
switch (node.type) {
|
|
case 'Literal':
|
|
return node.value
|
|
// A backtick string with nothing interpolated is still a plain value, and the readable way to
|
|
// write the multi-line ones -- a starter body for a block, say.
|
|
case 'TemplateLiteral':
|
|
if (node.expressions.length > 0) {
|
|
throw new Error(`${blockDir}: "static definition" must contain only plain literals, got an interpolated template.`)
|
|
}
|
|
return node.quasis[0].value.cooked
|
|
case 'ArrayExpression':
|
|
return node.elements.map(el => literalToValue(el, blockDir))
|
|
case 'ObjectExpression':
|
|
return Object.fromEntries(node.properties.map(prop => [
|
|
prop.key.name ?? prop.key.value,
|
|
literalToValue(prop.value, blockDir)
|
|
]))
|
|
default:
|
|
throw new Error(`${blockDir}: "static definition" must contain only plain literals, got ${node.type}.`)
|
|
}
|
|
}
|
|
|
|
const ASSET_MIME_TYPES = {
|
|
'.gif': 'image/gif',
|
|
'.jpeg': 'image/jpeg',
|
|
'.jpg': 'image/jpeg',
|
|
'.otf': 'font/otf',
|
|
'.png': 'image/png',
|
|
'.svg': 'image/svg+xml',
|
|
'.ttf': 'font/ttf',
|
|
'.webp': 'image/webp',
|
|
'.woff': 'font/woff',
|
|
'.woff2': 'font/woff2'
|
|
}
|
|
|
|
/**
|
|
* Loads a `.css` import as a string, with the files it points at inlined as data URIs.
|
|
*
|
|
* A block styles itself from inside its shadow root, which a `<link>` in the page cannot reach — so a
|
|
* library's stylesheet has to be part of the component. Rollup has no notion of CSS on its own.
|
|
*
|
|
* The inlining is what makes that stylesheet's own assets — leaflet's control sprites, KaTeX's font
|
|
* files — arrive with it. A relative `url()` in a stylesheet resolves against the document, not
|
|
* against the file it was written in, so once the CSS is a string inside a bundle those paths point
|
|
* at whatever wiki page happens to be showing the block. There is nowhere to put the files that would
|
|
* fix that: a block is one file served from /_blocks and mounted at a path it does not know.
|
|
*
|
|
* A `@font-face` offering several formats is cut down to its woff2, when it has one. Otherwise the
|
|
* same face arrives three times over — woff2, woff and ttf are the same glyphs at ~1.5x, ~2x and ~4x
|
|
* the bytes — and every browser that can run a block reads woff2.
|
|
*/
|
|
function cssAsString () {
|
|
return {
|
|
name: 'css-as-string',
|
|
transform (code, id) {
|
|
if (!id.endsWith('.css')) {
|
|
return null
|
|
}
|
|
const baseDir = path.dirname(id)
|
|
// -> Before the inlining, while a `src` list is still short enough to read: a data URI holds
|
|
// commas of its own, which is exactly what splits the list here.
|
|
const css = code
|
|
.replace(/src\s*:\s*([^;}]+)/g, (declaration, sources) => {
|
|
const parts = sources.split(/,(?![^(]*\))/)
|
|
const woff2 = parts.filter(part =>
|
|
/\.woff2\b|format\(\s*['"]?woff2['"]?\s*\)/.test(part)
|
|
)
|
|
return woff2.length > 0 && woff2.length < parts.length
|
|
? `src:${woff2.join(',')}`
|
|
: declaration
|
|
})
|
|
.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/g, (reference, _quote, target) => {
|
|
// -> Anything already addressable is left alone, `url(#default#VML)` among them: leaflet
|
|
// writes that one to turn on VML in IE, and it names no file at all.
|
|
if (/^(data:|https?:|\/\/|#|\/)/.test(target)) {
|
|
return reference
|
|
}
|
|
const assetPath = path.resolve(baseDir, target.split(/[?#]/)[0])
|
|
const mimeType = ASSET_MIME_TYPES[path.extname(assetPath).toLowerCase()]
|
|
if (!mimeType || !fs.existsSync(assetPath)) {
|
|
this.warn(`${id}: cannot inline ${target} — no such file, or not a known asset type.`)
|
|
return reference
|
|
}
|
|
this.addWatchFile(assetPath)
|
|
return `url("data:${mimeType};base64,${fs.readFileSync(assetPath).toString('base64')}")`
|
|
})
|
|
return { code: `export default ${JSON.stringify(css)}`, map: { mappings: '' } }
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collects each block's `static definition` into `compiled/blocks.manifest.json`.
|
|
*
|
|
* The definitions are read from the AST rather than by importing the modules, since a component
|
|
* registers itself with `customElements` on load and so cannot be imported outside a browser.
|
|
*/
|
|
function blocksManifest () {
|
|
const definitions = new Map()
|
|
return {
|
|
name: 'blocks-manifest',
|
|
buildStart () {
|
|
definitions.clear()
|
|
},
|
|
transform (code, id) {
|
|
if (!id.endsWith('/component.js')) {
|
|
return null
|
|
}
|
|
const blockDir = id.split('/').at(-2)
|
|
const ast = this.parse(code)
|
|
for (const node of ast.body) {
|
|
const classNode = node.type === 'ExportNamedDeclaration' ? node.declaration : node
|
|
if (classNode?.type !== 'ClassDeclaration') {
|
|
continue
|
|
}
|
|
const definitionNode = classNode.body.body.find(member =>
|
|
member.type === 'PropertyDefinition' && member.static && member.key.name === 'definition'
|
|
)
|
|
if (definitionNode) {
|
|
definitions.set(blockDir, literalToValue(definitionNode.value, blockDir))
|
|
}
|
|
}
|
|
if (!definitions.has(blockDir)) {
|
|
this.warn(`${blockDir} has no "static definition" — it will not appear in the admin area.`)
|
|
}
|
|
return null
|
|
},
|
|
generateBundle () {
|
|
this.emitFile({
|
|
type: 'asset',
|
|
fileName: 'blocks.manifest.json',
|
|
source: JSON.stringify([...definitions.values()], null, 2) + '\n'
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
export default {
|
|
input: Object.fromEntries(
|
|
glob.sync('@(block-*)/component.js', {
|
|
ignore: [
|
|
'dist/**',
|
|
'node_modules/**'
|
|
]
|
|
}).map(file => {
|
|
const fileParts = file.split('/')
|
|
return [
|
|
fileParts[0],
|
|
file
|
|
]
|
|
})
|
|
),
|
|
output: {
|
|
dir: 'compiled',
|
|
format: 'es'
|
|
},
|
|
plugins: [
|
|
blocksManifest(),
|
|
cssAsString(),
|
|
// -> `production` is stated rather than left to be inferred: since v16 the plugin picks the
|
|
// `development` or `production` export condition off `process.env.NODE_ENV`, and this build
|
|
// runs from a bare `npm run build` with no NODE_ENV set. Unstated, lit resolves to its
|
|
// development entry and every block ships the dev-mode warnings and asserts.
|
|
resolve({ exportConditions: ['production'] }),
|
|
// -> A block's own code is ESM, but a library it pulls in need not be: mermaid reaches for dayjs,
|
|
// which ships as UMD, and rollup has no notion of `module.exports` without this
|
|
commonjs(),
|
|
terser({
|
|
ecma: 2019,
|
|
module: true
|
|
}),
|
|
summary()
|
|
]
|
|
}
|