mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Add biome + rolldown + lightningcss build pipeline for JS/CSS bundling & minification
- package.json with biome, rolldown, lightningcss devDependencies
- biome.json: add CSS formatter support
- scripts/build-css.mjs: lightningcss resolves @import chain, bundles/minifies CSS
- scripts/build-js.mjs: rolldown per-entry JS bundling (no code splitting)
- scripts/build.mjs: orchestrator for both CSS + JS
- scripts/check-build.mjs: staleness checker for CI/deploy guard
- justfile: add build, build-css, build-js, build-install, build-check recipes
- justfile: deploy recipe now runs build before deploy-code
- head.php + form-page.php: use dist/base.min.css instead of style.css
- All controllers + FormBootstrap: reference dist/*.min.{css,js}
- admin footer: load admin.min.js for all admin pages
- repertoire: use public.min.js instead of individual app JS files
- Fix stray '}' syntax error in admin.css line 305
- .gitignore: add app/public/assets/dist/
This commit is contained in:
152
scripts/build-css.mjs
Normal file
152
scripts/build-css.mjs
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build CSS bundles with Lightning CSS.
|
||||
*
|
||||
* - base.min.css: resolves the @import chain in style.css into a single minified file.
|
||||
* This eliminates ~17 sequential @import requests on every page.
|
||||
* - admin.min.css: minifies admin.css
|
||||
* - form.min.css: bundles form-base.css + form-admin.css + filepond vendor CSS
|
||||
*
|
||||
* All other page-specific CSS files (public.css, tfe.css, repertoire.css, etc.)
|
||||
* are minified in-place as individual files.
|
||||
*
|
||||
* Output: app/public/assets/dist/
|
||||
*/
|
||||
|
||||
import { bundleAsync } from "lightningcss";
|
||||
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { resolve, dirname, basename } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, "..");
|
||||
const cssDir = resolve(root, "app/public/assets/css");
|
||||
const distDir = resolve(root, "app/public/assets/dist");
|
||||
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
|
||||
const targets = {
|
||||
chrome: 115 << 16,
|
||||
firefox: 115 << 16,
|
||||
safari: 16 << 16,
|
||||
};
|
||||
|
||||
function makeResolver() {
|
||||
return {
|
||||
resolve(specifier, from) {
|
||||
return resolve(dirname(from), specifier);
|
||||
},
|
||||
read(filePath) {
|
||||
return readFileSync(filePath, "utf8");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function bundleCss(filename, outName) {
|
||||
const entryPath = resolve(cssDir, filename);
|
||||
const result = await bundleAsync({
|
||||
filename: entryPath,
|
||||
resolver: makeResolver(),
|
||||
minify: true,
|
||||
sourceMap: false,
|
||||
targets,
|
||||
});
|
||||
const outPath = resolve(distDir, outName);
|
||||
writeFileSync(outPath, result.code);
|
||||
const size = Buffer.byteLength(result.code, "utf8");
|
||||
console.log(` ✓ ${outName} (${size.toLocaleString()} bytes)`);
|
||||
return size;
|
||||
}
|
||||
|
||||
async function concatBundle(filenames, outName) {
|
||||
const parts = [];
|
||||
for (const f of filenames) {
|
||||
const fp = resolve(cssDir, f);
|
||||
parts.push(readFileSync(fp, "utf8"));
|
||||
}
|
||||
const combined = parts.join("\n");
|
||||
|
||||
// Use lightningcss to minify the combined content
|
||||
const result = await bundleAsync({
|
||||
filename: resolve(cssDir, filenames[0]),
|
||||
resolver: {
|
||||
resolve() {
|
||||
throw new Error("unexpected @import in concat bundle");
|
||||
},
|
||||
read(fp) {
|
||||
if (fp === resolve(cssDir, filenames[0])) {
|
||||
return combined;
|
||||
}
|
||||
// Allow vendor CSS @imports within the combined content
|
||||
throw new Error(`unexpected file in concat: ${fp}`);
|
||||
},
|
||||
},
|
||||
minify: true,
|
||||
sourceMap: false,
|
||||
targets,
|
||||
});
|
||||
|
||||
const outPath = resolve(distDir, outName);
|
||||
writeFileSync(outPath, result.code);
|
||||
const size = Buffer.byteLength(result.code, "utf8");
|
||||
console.log(` ✓ ${outName} (${size.toLocaleString()} bytes)`);
|
||||
return size;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("🎨 Building CSS bundles…\n");
|
||||
|
||||
let total = 0;
|
||||
|
||||
// 1. Base bundle: resolve style.css @import chain
|
||||
total += await bundleCss("style.css", "base.min.css");
|
||||
|
||||
// 2. Admin bundle: just minify admin.css (standalone)
|
||||
total += await bundleCss("admin.css", "admin.min.css");
|
||||
|
||||
// 3. Form bundle: concat + minify form-base.css + form-admin.css + filepond vendor CSS
|
||||
total += await concatBundle(
|
||||
[
|
||||
"form-base.css",
|
||||
"form-admin.css",
|
||||
"filepond.min.css",
|
||||
"filepond-plugin-image-preview.min.css",
|
||||
],
|
||||
"form.min.css"
|
||||
);
|
||||
|
||||
// 4. Individual page-specific CSS files: minify each
|
||||
const individualFiles = [
|
||||
"public.css",
|
||||
"tfe.css",
|
||||
"repertoire.css",
|
||||
"content-page.css",
|
||||
"system.css",
|
||||
"file-access.css",
|
||||
];
|
||||
|
||||
for (const f of individualFiles) {
|
||||
const outName = f.replace(/\.css$/, ".min.css");
|
||||
total += await bundleCss(f, outName);
|
||||
}
|
||||
|
||||
// 5. Form-base standalone (for partage pages without FilePond)
|
||||
total += await bundleCss("form-base.css", "form-base.min.css");
|
||||
|
||||
// 6. Partage bundle: form-base + filepond (for partage pages with FilePond)
|
||||
total += await concatBundle(
|
||||
[
|
||||
"form-base.css",
|
||||
"filepond.min.css",
|
||||
"filepond-plugin-image-preview.min.css",
|
||||
],
|
||||
"partage-form.min.css"
|
||||
);
|
||||
|
||||
console.log(`\n✅ CSS bundles done — ${total.toLocaleString()} bytes total\n`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ CSS build failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
64
scripts/build-js.mjs
Normal file
64
scripts/build-js.mjs
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build JS bundles: one self-contained file per entry point.
|
||||
*
|
||||
* Each bundle includes all its dependencies inline (no code splitting).
|
||||
* Output: app/public/assets/dist/{admin,public,form,partage}.min.js
|
||||
*/
|
||||
|
||||
import { rolldown } from "rolldown";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, "..");
|
||||
const jsDir = resolve(root, "app/public/assets/js/app");
|
||||
const distDir = resolve(root, "app/public/assets/dist");
|
||||
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
|
||||
const entries = {
|
||||
admin: resolve(jsDir, "admin-entry.js"),
|
||||
public: resolve(jsDir, "public-entry.js"),
|
||||
form: resolve(jsDir, "form-entry.js"),
|
||||
partage: resolve(jsDir, "partage-entry.js"),
|
||||
};
|
||||
|
||||
async function buildEntry(name, input) {
|
||||
const bundle = await rolldown({
|
||||
input,
|
||||
resolve: { extensions: [".js"] },
|
||||
output: {
|
||||
format: "esm",
|
||||
minify: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { output } = await bundle.generate();
|
||||
// output is an array of OutputChunks; we want the entry chunk
|
||||
const entryChunk = output.find((c) => c.isEntry);
|
||||
if (!entryChunk) {
|
||||
console.error(` ✗ ${name}.min.js — no entry chunk`);
|
||||
return;
|
||||
}
|
||||
|
||||
const outPath = resolve(distDir, `${name}.min.js`);
|
||||
writeFileSync(outPath, entryChunk.code);
|
||||
|
||||
const size = Buffer.byteLength(entryChunk.code, "utf8");
|
||||
console.log(` ✓ ${name}.min.js (${size.toLocaleString()} bytes)`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("📦 Building JS bundles…\n");
|
||||
for (const [name, input] of Object.entries(entries)) {
|
||||
await buildEntry(name, input);
|
||||
}
|
||||
console.log("\n✅ JS bundles done\n");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("❌ JS build failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
48
scripts/build.mjs
Normal file
48
scripts/build.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build all frontend assets: CSS + JS.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/build.mjs # build everything
|
||||
* node scripts/build.mjs --css # CSS only
|
||||
* node scripts/build.mjs --js # JS only
|
||||
*
|
||||
* Requires: npm install (lightningcss-cli, rolldown)
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, "..");
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const onlyCss = args.includes("--css");
|
||||
const onlyJs = args.includes("--js");
|
||||
const buildAll = !onlyCss && !onlyJs;
|
||||
|
||||
function run(label, cmd) {
|
||||
console.log(`\n📦 ${label}…`);
|
||||
execSync(cmd, {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure node_modules exist
|
||||
if (!existsSync(resolve(root, "node_modules"))) {
|
||||
console.log("📥 Installing dependencies (npm ci)…");
|
||||
execSync("npm ci", { cwd: root, stdio: "inherit" });
|
||||
}
|
||||
|
||||
if (buildAll || onlyCss) {
|
||||
run("Building CSS bundles", "node scripts/build-css.mjs");
|
||||
}
|
||||
|
||||
if (buildAll || onlyJs) {
|
||||
run("Building JS bundles", "node scripts/build-js.mjs");
|
||||
}
|
||||
|
||||
console.log("\n✅ Build complete\n");
|
||||
82
scripts/check-build.mjs
Normal file
82
scripts/check-build.mjs
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Quick check: are dist files present and fresh?
|
||||
* Exits 0 if ok, 1 if missing or stale.
|
||||
*
|
||||
* Staleness: any source file newer than the oldest dist file.
|
||||
*/
|
||||
|
||||
import { statSync, existsSync, readdirSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, "..");
|
||||
const distDir = resolve(root, "app/public/assets/dist");
|
||||
|
||||
const distFiles = readdirSync(distDir).filter(
|
||||
(f) => f.endsWith(".min.css") || f.endsWith(".min.js")
|
||||
);
|
||||
|
||||
if (distFiles.length === 0) {
|
||||
console.error("❌ No dist files found. Run: just build");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check each dist file exists
|
||||
for (const f of distFiles) {
|
||||
if (!existsSync(resolve(distDir, f))) {
|
||||
console.error(`❌ Missing: dist/${f}. Run: just build`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check staleness: any source newer than the oldest dist?
|
||||
function walkDir(dir, ext) {
|
||||
const files = [];
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
const full = resolve(dir, e.name);
|
||||
if (e.isDirectory() && e.name !== "components") {
|
||||
files.push(...walkDir(full, ext));
|
||||
} else if (e.isFile() && full.endsWith(ext)) {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const cssSrcFiles = walkDir(
|
||||
resolve(root, "app/public/assets/css"),
|
||||
".css"
|
||||
).filter((f) => !f.includes("filepond") && !f.includes("modern-normalize"));
|
||||
|
||||
const jsSrcFiles = walkDir(
|
||||
resolve(root, "app/public/assets/js/app"),
|
||||
".js"
|
||||
);
|
||||
|
||||
const srcFiles = [...cssSrcFiles, ...jsSrcFiles];
|
||||
|
||||
const oldestDist = Math.min(
|
||||
...distFiles.map((f) => statSync(resolve(distDir, f)).mtimeMs)
|
||||
);
|
||||
|
||||
let stale = false;
|
||||
for (const f of srcFiles) {
|
||||
try {
|
||||
if (statSync(f).mtimeMs > oldestDist) {
|
||||
console.error(`❌ Stale dist (source newer than output): ${f}`);
|
||||
stale = true;
|
||||
}
|
||||
} catch {
|
||||
// file may not exist (e.g. entry files), skip
|
||||
}
|
||||
}
|
||||
|
||||
if (stale) {
|
||||
console.error("❌ Run: just build");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("✅ Build output is up to date");
|
||||
Reference in New Issue
Block a user