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:
Pontoporeia
2026-06-24 13:09:44 +02:00
parent e74f9210c5
commit 20fe4b6c8c
29 changed files with 1391 additions and 46 deletions

3
.gitignore vendored
View File

@@ -49,6 +49,9 @@ Thumbs.db
/node_modules
# Build output
app/public/assets/dist/
# PHPStan cache
.phpstan.result.cache

18
TODO.md
View File

@@ -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

View File

@@ -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') {

View File

@@ -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';

View File

@@ -302,7 +302,6 @@
font-weight: 500;
white-space: nowrap;
}
}
/* ── Table ──────────────────────────────────────────────────────────────── */
/* Base table/th/td styles live in components/tables.css */

View File

@@ -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 <script> tags
*/
// Core utilities (no deps)
import "./beforeunload-guard.js";
import "./clipboard.js";
import "./smtp-error-focus.js";
// HTMX-powered features (htmx is a global from vendor script)
import "./htmx-global-setup.js";
// Admin features
import "./admin-index-bulk.js";
import "./admin-tags.js";
import "./admin-contenus-langues.js";
import "./admin-contenus-motscles.js";
import "./admin-contacts-form.js";
import "./admin-acces.js";
import "./admin-acces-sharelink.js";
import "./admin-file-access.js";
import "./admin-toc.js";
import "./admin-logs.js";
import "./sidebar-links-editor.js";

View File

@@ -0,0 +1,17 @@
/**
* Form JS entry — form-specific JS for admin form pages (add/edit).
*
* Admin baseline JS (admin.min.js) is loaded separately via the admin footer.
* This bundle only includes form-specific features.
*
* Vendor scripts (htmx, FilePond, OverType) are loaded separately via <script> tags.
*/
// Form-specific features (admin.min.js provides htmx-global-setup, beforeunload-guard, etc.)
import "./file-upload-filepond.js";
import "./pill-search.js";
import "./jury-autocomplete.js";
import "./autosave-handler.js";
import "./form-duration-toggle.js";
import "./form-jury-fields.js";
import "./form-language-asterisk.js";

View File

@@ -0,0 +1,17 @@
/**
* Partage (student share) JS entry — all JS needed on partage form pages.
*
* Vendor scripts (htmx, FilePond) are loaded separately via <script> tags.
*/
// Core utilities
import "./beforeunload-guard.js";
// Form-specific features
import "./file-upload-filepond.js";
import "./pill-search.js";
import "./jury-autocomplete.js";
import "./autosave-handler.js";
import "./form-duration-toggle.js";
import "./form-jury-fields.js";
import "./form-language-asterisk.js";

View File

@@ -0,0 +1,19 @@
/**
* Public JS entry — all JS needed on public-facing pages.
*
* Import order matters.
* Vendor scripts (htmx) are loaded separately via <script> tags.
*/
// Core utilities
import "./beforeunload-guard.js";
import "./clipboard.js";
// HTMX-powered features
import "./htmx-global-setup.js";
// Public page features
import "./repertoire-accordion.js";
import "./repertoire-student-popover.js";
import "./access-request.js";
import "./acces-password.js";

View File

@@ -55,7 +55,7 @@ class AboutController
'sidebarLinks' => $sidebarLinks,
'pageTitle' => 'À Propos XAMXAM',
'metaDescription' => "À propos de XAMXAM, le répertoire des mémoires de fin d'études de l'erg École de Recherches Graphiques de Bruxelles.",
'extraCss' => ['/assets/css/content-page.css'],
'extraCss' => ['/assets/dist/content-page.min.css'],
'bodyClass' => 'apropos-body',
];
}

View File

@@ -49,7 +49,7 @@ class CharteController
'pageTitle' => $pageTitle . ' XAMXAM',
'metaDescription' => "Charte d'utilisation de XAMXAM, le répertoire des TFE de l'erg.",
'currentNav' => 'charte',
'extraCss' => ['/assets/css/content-page.css'],
'extraCss' => ['/assets/dist/content-page.min.css'],
'bodyClass' => 'apropos-body',
];
}

View File

@@ -156,7 +156,7 @@ class HomeController
// Layout
'currentNav' => '',
'extraCss' => ['/assets/css/public.css'],
'extraCss' => ['/assets/dist/public.min.css'],
'bodyClass' => 'home-body',
];
}

View File

@@ -49,7 +49,7 @@ class LicenceController
'pageTitle' => $pageTitle . ' XAMXAM',
'metaDescription' => "Informations sur les licences d'utilisation des mémoires publiés sur XAMXAM, le répertoire des TFE de l'erg.",
'currentNav' => 'licence',
'extraCss' => ['/assets/css/content-page.css'],
'extraCss' => ['/assets/dist/content-page.min.css'],
'bodyClass' => 'apropos-body',
];
}

View File

@@ -153,7 +153,7 @@ class SearchController
'site_name' => 'XAMXAM ERG',
],
'currentNav' => 'repertoire',
'extraCss' => ['/assets/css/repertoire.css'],
'extraCss' => ['/assets/dist/repertoire.min.css'],
'bodyClass' => 'search-body',
];
}
@@ -205,7 +205,7 @@ class SearchController
'site_name' => 'XAMXAM ERG',
],
'currentNav' => 'repertoire',
'extraCss' => ['/assets/css/repertoire.css'],
'extraCss' => ['/assets/dist/repertoire.min.css'],
'bodyClass' => 'search-body',
];
}

View File

@@ -146,8 +146,8 @@ class TfeController
// Layout
'currentNav' => '',
'extraCss' => ['/assets/css/tfe.css'],
'extraJs' => ['/assets/js/app/access-request.js'],
'extraCss' => ['/assets/dist/tfe.min.css'],
'extraJs' => ['/assets/dist/public.min.js'],
'bodyClass' => 'tfe-body',
];
}

View File

@@ -16,23 +16,15 @@ class FormBootstrap
public static function adminAssetArrays(): array
{
return [
'extraCss' => ['/assets/css/form-base.css'],
'extraCssAdmin' => [
'/assets/css/form-admin.css',
'/assets/css/filepond.min.css',
'/assets/css/filepond-plugin-image-preview.min.css',
],
'extraCss' => ['/assets/dist/form.min.css'],
'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/js/app/file-upload-filepond.js',
'/assets/js/app/beforeunload-guard.js',
'/assets/js/app/pill-search.js',
'/assets/js/app/jury-autocomplete.js',
'/assets/js/app/autosave-handler.js',
'/assets/dist/form.min.js',
],
];
}

View File

@@ -17,6 +17,6 @@
<script><?= $extraJsInline ?></script>
<?php endif; ?>
<script src="/assets/js/vendor/htmx.min.js"></script>
<script src="<?= App::assetV('/assets/js/app/htmx-global-setup.js') ?>"></script>
<script src="<?= App::assetV('/assets/dist/admin.min.js') ?>"></script>
</body>
</html>

View File

@@ -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 ?? []);
}
?>
<title><?= htmlspecialchars($pageTitle ?? 'XAMXAM') ?></title>
@@ -70,7 +70,7 @@
<?php if (!empty($isAdmin) || !empty($filepondBase)): ?>
<meta name="filepond-base" content="<?= htmlspecialchars($filepondBase ?? '/admin/actions/filepond') ?>">
<?php endif; ?>
<link rel="stylesheet" href="<?= App::assetV('/assets/css/style.css') ?>">
<link rel="stylesheet" href="<?= App::assetV('/assets/dist/base.min.css') ?>">
<?php foreach ($extraCss ?? [] as $css): ?>
<link rel="stylesheet" href="<?= App::assetV($css) ?>">
<?php endforeach; ?>

View File

@@ -37,11 +37,11 @@ $filepondBase = $filepondBase ?? null;
<?php if ($filepondBase !== null): ?>
<meta name="filepond-base" content="<?= htmlspecialchars($filepondBase) ?>">
<?php endif; ?>
<link rel="stylesheet" href="<?= App::assetV('/assets/css/style.css') ?>">
<link rel="stylesheet" href="<?= App::assetV('/assets/css/form-base.css') ?>">
<link rel="stylesheet" href="<?= App::assetV('/assets/dist/base.min.css') ?>">
<?php if ($includeFilePond): ?>
<link rel="stylesheet" href="<?= App::assetV('/assets/css/filepond.min.css') ?>">
<link rel="stylesheet" href="<?= App::assetV('/assets/css/filepond-plugin-image-preview.min.css') ?>">
<link rel="stylesheet" href="<?= App::assetV('/assets/dist/partage-form.min.css') ?>">
<?php else: ?>
<link rel="stylesheet" href="<?= App::assetV('/assets/dist/form-base.min.css') ?>">
<?php endif; ?>
<?php foreach ($extraCss as $css): ?>
<link rel="stylesheet" href="<?= App::assetV($css) ?>">
@@ -52,14 +52,10 @@ $filepondBase = $filepondBase ?? null;
<script src="<?= App::assetV('/assets/js/vendor/filepond-plugin-file-validate-size.min.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/vendor/filepond-plugin-image-preview.min.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/vendor/filepond-plugin-image-exif-orientation.min.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/app/file-upload-filepond.js') ?>" defer></script>
<?php endif; ?>
<script src="<?= App::assetV('/assets/js/app/beforeunload-guard.js') ?>" defer></script>
<?php if ($includeFilePond): ?>
<script src="<?= App::assetV('/assets/js/app/pill-search.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/app/jury-autocomplete.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/app/autosave-handler.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/js/vendor/htmx.min.js') ?>" defer></script>
<script src="<?= App::assetV('/assets/dist/partage.min.js') ?>" defer></script>
<?php else: ?>
<script src="<?= App::assetV('/assets/dist/public.min.js') ?>" defer></script>
<?php endif; ?>
<?php foreach ($extraJs as $js): ?>
<script src="<?= App::assetV($js) ?>" defer></script>

View File

@@ -7,5 +7,4 @@
<div id="student-popover" class="student-popover" hidden aria-live="polite"></div>
<script src="/assets/js/vendor/htmx.min.js"></script>
<script src="<?= App::assetV('/assets/js/app/repertoire-student-popover.js') ?>"></script>
<script src="<?= App::assetV('/assets/js/app/repertoire-accordion.js') ?>"></script>
<script src="<?= App::assetV('/assets/dist/public.min.js') ?>"></script>

View File

@@ -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"]
}
}

View File

@@ -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 ""

829
package-lock.json generated Normal file
View File

@@ -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
}
}
}

15
package.json Normal file
View File

@@ -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"
}
}

43
rolldown.config.mjs Normal file
View File

@@ -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"],
},
});

152
scripts/build-css.mjs Normal file
View 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
View 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
View 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
View 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");