diff --git a/.gitignore b/.gitignore index 3a8d141..ca408e7 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,9 @@ Thumbs.db /node_modules +# Build output +app/public/assets/dist/ + # PHPStan cache .phpstan.result.cache diff --git a/TODO.md b/TODO.md index c7a3cde..b556239 100644 --- a/TODO.md +++ b/TODO.md @@ -1,14 +1,25 @@ # TODO > Last updated: 2026-06-24 -> Context: Inline JS/CSS + gzip analysis (see docs/ANALYSIS_INLINE_JS_CSS_MINIFY.md) +> Context: Setup biome + rolldown + lightningcss build pipeline for JS/CSS bundling & minification + +## Completed +- [x] #build-pipeline Setup biome + rolldown + lightningcss build pipeline ✓ + - [x] #build-packagejson Create package.json with devDependencies ✓ + - [x] #build-biomecss Update biome.json to handle CSS formatting ✓ + - [x] #build-rolldown-config Create rolldown.config.mjs + build-js.mjs for JS bundling ✓ + - [x] #build-lightningcss Add lightningcss CSS bundling (resolve @import chain) ✓ + - [x] #build-justfile Add just build/deploy recipes + integrate build into deploy ✓ + - [x] #build-head Update head.php + form-page.php + controllers to use bundled assets ✓ + - [x] #build-gitignore Add dist/ to .gitignore ✓ + - [x] #build-cssfix Fix stray `}` syntax error in admin.css line 305 ✓ ## Pending - [ ] #rep-student-touch Replace hover student popover with tap-to-open drawer for mobile `(repertoire.php, repertoire.css)` - [ ] #rep-polish Polish: scroll-position memory on HTMX swap, animation tuning `(repertoire.css)` - [ ] #icon-color-verify Verify icon colors render correctly across all pages (header, admin tables, forms, dialogs, cleanup modal) -## Completed +## Completed (before this session) - [x] #gzip-nginx Enable gzip compression in nginx config `(nginx/xamxam.conf)` ✓ - [x] #extract-inline-js Move inline JS to external files across 17 templates → 15 new JS files created `(app/public/assets/js/app/*.js)` ✓ - [x] #inline-icon-helper Create `icon()` PHP helper + auto-load in bootstrap `(src/icon.php, bootstrap.php)` ✓ @@ -63,7 +74,4 @@ - [x] #extra-css-admin Update `head.php` to support `$extraCssAdmin` for admin-only stylesheets `(head.php)` ✓ ## Deferred / Blocked -- [ ] #minify-js Minify custom JS files (post-extraction, ~1,763 lines across 9 files) -- [ ] #bundle-css Bundle CSS to eliminate @import waterfall (18 files, ~6,200 lines) -- [ ] #build-step Add build step (justfile commands) for JS minification + CSS bundling - [ ] #tighten-csp Tighten CSP to remove 'unsafe-inline' after inline JS extraction diff --git a/app/public/admin/index.php b/app/public/admin/index.php index dff438f..4064de9 100644 --- a/app/public/admin/index.php +++ b/app/public/admin/index.php @@ -514,8 +514,8 @@ if ($isHtmx) { include APP_ROOT . '/templates/admin/index-table.php'; } } else { - $extraCssAdmin = ['/assets/css/filepond.min.css', '/assets/css/filepond-plugin-image-preview.min.css']; - $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/js/app/file-upload-filepond.js']; + $extraCssAdmin = []; + $extraJs = ['/assets/js/vendor/filepond.min.js', '/assets/js/vendor/filepond-plugin-file-validate-type.min.js', '/assets/js/vendor/filepond-plugin-file-validate-size.min.js', '/assets/js/vendor/filepond-plugin-image-preview.min.js', '/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js', '/assets/dist/admin.min.js']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; if ($tab === 'trash') { diff --git a/app/public/admin/parametres.php b/app/public/admin/parametres.php index 8b0bb47..583eaa4 100644 --- a/app/public/admin/parametres.php +++ b/app/public/admin/parametres.php @@ -74,7 +74,7 @@ if (empty($_SESSION['csrf_token'])) { } $isAdmin = true; $bodyClass = 'admin-body'; -$extraCssAdmin = ['/assets/css/system.css']; +$extraCssAdmin = ['/assets/dist/system.min.css']; require_once APP_ROOT . '/templates/head.php'; include APP_ROOT . '/templates/header.php'; include APP_ROOT . '/templates/admin/parametres.php'; diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index 6d131b1..8c94190 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -302,7 +302,6 @@ font-weight: 500; white-space: nowrap; } -} /* ── Table ──────────────────────────────────────────────────────────────── */ /* Base table/th/td styles live in components/tables.css */ diff --git a/app/public/assets/js/app/admin-entry.js b/app/public/assets/js/app/admin-entry.js new file mode 100644 index 0000000..ccadb85 --- /dev/null +++ b/app/public/assets/js/app/admin-entry.js @@ -0,0 +1,28 @@ +/** + * Admin JS entry — all JS needed on admin pages. + * + * Import order matters for dependencies: + * - htmx-global-setup must come first (HTMX event listeners) + * - Vendor scripts (htmx, FilePond) are loaded separately via - + diff --git a/app/templates/head.php b/app/templates/head.php index e0bea35..50cb5e7 100644 --- a/app/templates/head.php +++ b/app/templates/head.php @@ -7,7 +7,7 @@ // Admin: append suffix to title and prepend admin.css if (!empty($isAdmin)) { $pageTitle = isset($pageTitle) ? $pageTitle . ' – Admin' : 'Admin'; - $extraCss = array_merge(['/assets/css/admin.css'], $extraCssAdmin ?? [], $extraCss ?? []); + $extraCss = array_merge(['/assets/dist/admin.min.css'], $extraCssAdmin ?? [], $extraCss ?? []); } ?> <?= htmlspecialchars($pageTitle ?? 'XAMXAM') ?> @@ -70,7 +70,7 @@ - + diff --git a/app/templates/partage/form-page.php b/app/templates/partage/form-page.php index 4c19898..861a6e0 100644 --- a/app/templates/partage/form-page.php +++ b/app/templates/partage/form-page.php @@ -37,11 +37,11 @@ $filepondBase = $filepondBase ?? null; - - + - - + + + @@ -52,14 +52,10 @@ $filepondBase = $filepondBase ?? null; - - - - - - - + + + diff --git a/app/templates/public/repertoire.php b/app/templates/public/repertoire.php index fe9794b..63a9745 100644 --- a/app/templates/public/repertoire.php +++ b/app/templates/public/repertoire.php @@ -7,5 +7,4 @@ - - + diff --git a/biome.json b/biome.json index 4fd8b25..933bdc6 100644 --- a/biome.json +++ b/biome.json @@ -10,6 +10,14 @@ "!app/public/assets/js/vendor/**" ] }, + "css": { + "formatter": { + "enabled": true + }, + "linter": { + "enabled": false + } + }, "linter": { "enabled": true, "rules": { @@ -17,6 +25,7 @@ } }, "formatter": { - "enabled": true + "enabled": true, + "includes": ["**/*.js", "**/*.css"] } } diff --git a/justfile b/justfile index 0465e6f..5d66542 100644 --- a/justfile +++ b/justfile @@ -32,12 +32,37 @@ stop: logs: @tail -n 20 error.log 2>/dev/null || echo "no error log" +# ============================================================================ +# Build (JS/CSS bundling & minification) +# ============================================================================ + +[group('build')] +build: + @node scripts/build.mjs + +[group('build')] +build-css: + @node scripts/build-css.mjs + +[group('build')] +build-js: + @node scripts/build-js.mjs + +[group('build')] +build-install: + @npm ci + +[group('build')] +build-check: + @echo "Checking if build output is up to date…" + @node scripts/check-build.mjs + # ============================================================================ # Deploy # ============================================================================ [group('deploy')] -deploy: deploy-code deploy-deps deploy-migrate +deploy: build deploy-code deploy-deps deploy-migrate @just deploy-env @just deploy-verify-permissions @echo "" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5a85769 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,829 @@ +{ + "name": "xamxam", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xamxam", + "devDependencies": { + "@biomejs/biome": "^2.3.15", + "lightningcss": "^1.32.0", + "rolldown": "^1.1.3" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.1.tgz", + "integrity": "sha512-IXWLCxKmae+rI7LOHS1B3EbVisQ6GRAWbhN9msa6KjNCyFWrvKZWR4oUdinaNssrV852OrSHuSPa95h1GPJc7Q==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.5.1", + "@biomejs/cli-darwin-x64": "2.5.1", + "@biomejs/cli-linux-arm64": "2.5.1", + "@biomejs/cli-linux-arm64-musl": "2.5.1", + "@biomejs/cli-linux-x64": "2.5.1", + "@biomejs/cli-linux-x64-musl": "2.5.1", + "@biomejs/cli-win32-arm64": "2.5.1", + "@biomejs/cli-win32-x64": "2.5.1" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-npqDzvqv7vFaWRiNN1Te71siRgPaqS9MpqgYCdP/CrUbkJ7ApezaeaKjueKHRN/JH/6lRjJQAHi8acQDCAz22w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.1.tgz", + "integrity": "sha512-RgwTqPAM8g2tn1j+b5oRjF/DbSBX8a4gwojtuG9XuhfK7GgomvZ9+T+tqjXiVbjLEeGJOoL6VEk8mvRTVeSybw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.1.tgz", + "integrity": "sha512-yhV35CzZh38VyMvTEXi3JTjxZBs++oCKK9KG8vB6VI5+uvQvZNR3BFWEKKzuOmx9DJJj7sQpZ4LQJcmbGTs3+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-WMcvMLgByyTqVxGlq918NBBYliq9FRR9GAQVETHb+VjGVqXCZFfHlZHC1FX4ibuYY/Hg6TJE3rHU0xVrdJXNRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.1.tgz", + "integrity": "sha512-J/7uHSX7NfoYDI7HijAkd8lnQIOrRb2W7j3X+tw4R+N5ExvXGsyXFiGdQcfcxfOmNQmZVSQOCDk757fwpzqQcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-ANTowtlLmPYm5yeMckWY8Xzb9Ix+JJP3tgHR/n6xRj1VWyIzzWtfRfih9hv9VmClwadpBvZduISZIbBsIlYG3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.1.tgz", + "integrity": "sha512-zgXnKNgWPC4iPF7Y1lR3STUeCUuZRpD6IiOrC7TZTlh0Lx6FiVUT05myuMQHQ9D+1cc7uyMldi4forE6lp0ivQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.1.tgz", + "integrity": "sha512-6uxpR9hvaglANkZemeSiN/FhYgkGasrEGn267eXIWvjrjJ2LhDlk251IhjVJq6MXzkV2/bcXwLwSroLyPtqRZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/rolldown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.137.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6929242 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "xamxam", + "private": true, + "type": "module", + "scripts": { + "build": "node scripts/build.mjs", + "build:css": "node scripts/build-css.mjs", + "build:js": "node scripts/build-js.mjs" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.15", + "lightningcss": "^1.32.0", + "rolldown": "^1.1.3" + } +} diff --git a/rolldown.config.mjs b/rolldown.config.mjs new file mode 100644 index 0000000..451af2b --- /dev/null +++ b/rolldown.config.mjs @@ -0,0 +1,43 @@ +/** + * Rolldown configuration for XAMXAM JS bundling. + * + * Entry points: + * - admin.js → all JS needed on admin pages + * - public.js → all JS needed on public-facing pages + * - form.js → all JS needed on admin form pages (add/edit) + * + * Vendor scripts (htmx, FilePond plugins, OverType) are NOT bundled — + * they're already minified and served from vendor/. + * + * Output: app/public/assets/dist/[name].min.js + */ + +import { defineConfig } from "rolldown"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = __dirname; +const jsDir = resolve(root, "app/public/assets/js/app"); +const distDir = resolve(root, "app/public/assets/dist"); + +export default defineConfig({ + input: { + admin: resolve(jsDir, "admin-entry.js"), + public: resolve(jsDir, "public-entry.js"), + form: resolve(jsDir, "form-entry.js"), + partage: resolve(jsDir, "partage-entry.js"), + }, + output: { + dir: distDir, + format: "esm", + entryFileNames: "[name].min.js", + // Disable code splitting — each entry gets its own self-contained bundle. + codeSplitting: false, + // rolldown built-in minification + minify: true, + }, + resolve: { + extensions: [".js"], + }, +}); diff --git a/scripts/build-css.mjs b/scripts/build-css.mjs new file mode 100644 index 0000000..635ba86 --- /dev/null +++ b/scripts/build-css.mjs @@ -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); +}); diff --git a/scripts/build-js.mjs b/scripts/build-js.mjs new file mode 100644 index 0000000..fd5a435 --- /dev/null +++ b/scripts/build-js.mjs @@ -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); +}); diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..06a0cd7 --- /dev/null +++ b/scripts/build.mjs @@ -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"); diff --git a/scripts/check-build.mjs b/scripts/check-build.mjs new file mode 100644 index 0000000..3e1c81a --- /dev/null +++ b/scripts/check-build.mjs @@ -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");