refac: separate front assets into esbuild bundles

Sort ~50 Mo de libs tierces hors du repo et regroupe le code
front en 6 bundles thématiques + 2 assets statiques.

Bundles produits par esbuild :
- core.bundle.min.js   : jQuery + jQuery UI + Bootstrap 4 + validation
                          + tout le code applicatif 'core'
                          (site, signout, signin, input-lib, md-helpers,
                          audiovideoinput, parallax, google.geocode,
                          google-geoloc). Chargé par _Layout.cshtml.
- chat.bundle.min.js   : core subset (sans Bootstrap) + chat.js + comment.js
                          (SignalR client chargé séparément, voir plus bas)
- dropzone.bundle.min.js : jQuery + dropzone + yavsc-remote-fs
- datetime.bundle.min.js : jQuery + jQuery UI + eonasdan datetimepicker
                          + jquery-timepicker
- timepicker.bundle.min.js : jQuery + jQuery UI + jquery-timepicker
- quill.bundle.min.js  : Quill rich text editor

Assets statiques (non bundlés, copiés depuis node_modules) :
- signalr.min.js       : client SignalR 2.x (legacy server)
- moment-with-locales.min.js : moment + 137 locales (require dynamique
                               non résolvable statiquement par esbuild)

CSS vendor copiées comme assets statiques vers wwwroot/css/ :
bootstrap.min.css, jquery-ui.min.css, dropzone.min.css,
dropzone-basic.min.css, bootstrap-datetimepicker.min.css,
jquery.timepicker.css

Exception gitignore : jonthornton-Datepair/jquery.datepair.min.js
n'est pas sur npm, conservé comme asset statique sous
wwwroot/lib/jonthornton-Datepair/ (téléchargé depuis upstream).

Vues Razor mises à jour (15 fichiers) pour pointer vers les bundles
et les CSS vendor. Suppression de la majorité de wwwroot/lib/ (1300+
fichiers). Suppression des jquery*.js, quill.js, showdown.js,
to-markdown.js, jquery.signalR-2.2.1.js, dropzone.js et de leurs
.min.js associés.

'jquery-datepair' et 'jquery-ui-map' non publiés sur npm :
- jquery-ui-map n'est utilisé nulle part dans le repo (vérifié),
  la dépendance est purement historique.
- datepair reste en static sous wwwroot/lib/jonthornton-Datepair/.
This commit is contained in:
Paul Schneider 2026-06-14 14:11:33 +01:00
commit 952d263b9f
1091 changed files with 295 additions and 333043 deletions

View file

@ -1,109 +1,171 @@
// Yavsc front-end bundle config (esbuild).
//
// Produces a single minified bundle at
// src/Yavsc.Org/wwwroot/js/site.bundle.min.js
// that contains both our application code and the third-party libs we
// keep under npm. The bundle is committed to the repo so the .NET
// runtime does not need a node toolchain at publish time.
// Produces several thematic minified bundles under
// src/Yavsc.Org/wwwroot/js/*.bundle.min.js
// plus static copies of assets that cannot be bundled (legacy
// SignalR 2.x client, moment-with-locales with its 137 dynamic
// requires).
//
// Each bundle has a single entry file under build/<name>.entry.js
// (gitignored, not committed) that re-exports the relevant vendor and
// our code. esbuild bundles from there into one IIFE per bundle.
//
// Usage:
// npm install
// npm run build:js # one-shot
// npm run build:js:watch # rebuild on change
// npm run clean:js # remove the generated bundle
// npm run clean:js # remove generated bundles + static assets
import { build, context } from 'esbuild';
import { existsSync, mkdirSync, statSync } from 'node:fs';
import { copyFileSync, existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const isWatch = process.argv.includes('--watch');
const isClean = process.argv.includes('--clean');
const outFile = path.resolve(
__dirname,
'src/Yavsc.Org/wwwroot/js/site.bundle.min.js'
);
const jsOutDir = path.resolve(__dirname, 'src/Yavsc.Org/wwwroot/js');
const buildDir = path.resolve(__dirname, 'build');
const outDir = path.dirname(outFile);
if (!existsSync(outDir)) {
mkdirSync(outDir, { recursive: true });
// One bundle per thematic area. Each one points at a single entry file
// under build/ that re-exports the relevant vendor + our code.
const bundleNames = [
'core', // jQuery + jQuery UI + Bootstrap 4 + validation + our core code
'chat', // core subset + chat + comment (SignalR is loaded separately as a static asset)
'dropzone', // jQuery + dropzone + yavsc-remote-fs
'datetime', // jQuery + jQuery UI + eonasdan datetimepicker + jquery-timepicker
// (moment-with-locales is a static asset, see copyStaticAssets)
'timepicker', // jQuery + jQuery UI + jquery-timepicker
// (datepair is not on npm; kept as a static asset for now)
'quill', // Quill rich text editor (no jQuery dependency)
];
// Static assets that cannot be bundled (dynamic require, legacy
// server protocol, package not on npm). CSS files are also copied
// as static assets (bundling CSS is out of scope of this commit).
const staticAssets = [
{
src: 'node_modules/@aspnet/signalr/dist/browser/signalr.min.js',
dst: 'signalr.min.js',
},
{
src: 'node_modules/moment/min/moment-with-locales.min.js',
dst: 'moment-with-locales.min.js',
},
{
src: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
dst: '../css/bootstrap.min.css',
},
{
src: 'node_modules/jquery-ui/dist/themes/base/jquery-ui.min.css',
dst: '../css/jquery-ui.min.css',
},
{
src: 'node_modules/dropzone/dist/min/dropzone.min.css',
dst: '../css/dropzone.min.css',
},
{
src: 'node_modules/dropzone/dist/min/basic.min.css',
dst: '../css/dropzone-basic.min.css',
},
{
src: 'node_modules/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css',
dst: '../css/bootstrap-datetimepicker.min.css',
},
{
src: 'node_modules/jquery-timepicker/jquery.timepicker.css',
dst: '../css/jquery.timepicker.css',
},
];
function entryFor(bundleName) {
return path.resolve(buildDir, `${bundleName}.entry.js`);
}
// --- Our code (entry points) --------------------------------------------
// `site.js` is the bootstrap loaded by _Layout.cshtml. We list every
// .js we own as an entry point so they all end up in the same bundle.
const ourEntries = [
'src/Yavsc.Org/wwwroot/js/site.js',
'src/Yavsc.Org/wwwroot/js/chat.js',
'src/Yavsc.Org/wwwroot/js/comment.js',
'src/Yavsc.Org/wwwroot/js/signout-redirect.js',
'src/Yavsc.Org/wwwroot/js/input-lib.js',
'src/Yavsc.Org/wwwroot/js/google-geoloc.js',
'src/Yavsc.Org/wwwroot/js/audiovideoinput.js',
'src/Yavsc.Org/wwwroot/js/yavsc-remote-fs.js',
];
function outFor(bundleName) {
return path.join(jsOutDir, `${bundleName}.bundle.min.js`);
}
// --- Third-party code we want bundled ----------------------------------
// Listed by npm package name; resolved via node_modules.
// jQuery, jQuery UI, jQuery plugins, Bootstrap JS bundle, SignalR
// (browser), Moment, Quill, Showdown, To-markdown, Dropzone.
const vendorEntries = [
'jquery/dist/jquery.min.js',
'jquery-ui/dist/jquery-ui.min.js',
'jquery-validation/dist/jquery.validate.min.js',
'jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js',
'jquery-timepicker/jquery.timepicker.min.js',
'jquery-datepair/dist/jquery.datepair.min.js',
'bootstrap/dist/js/bootstrap.bundle.min.js',
'@microsoft/signalr/dist/browser/signalr.min.js',
'moment/min/moment.min.js',
'showdown/dist/showdown.min.js',
'to-markdown/dist/to-markdown.min.js',
'dropzone/dist/min/dropzone.min.js',
'quill/dist/quill.min.js',
// eonasdan is registered as a CommonJS package; esbuild handles it.
'eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js',
];
function buildOptionsFor(bundleName) {
return {
entryPoints: [entryFor(bundleName)],
outfile: outFor(bundleName),
bundle: true,
minify: true,
sourcemap: false,
target: ['es2018'],
format: 'iife',
legalComments: 'none',
logLevel: 'info',
nodePaths: ['node_modules'],
};
}
// Our entry points may live alongside the output, so we exclude the
// outDir from the resolver to avoid bundling the previous run.
const entryPoints = [...ourEntries, ...vendorEntries];
function ensureDir(dir) {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
const buildOptions = {
entryPoints,
outFile,
bundle: true,
minify: true,
sourcemap: false,
target: ['es2018'],
format: 'iife',
legalComments: 'none',
logLevel: 'info',
// jQuery plugins attach themselves to jQuery; we must not shim it.
alias: {},
nodePaths: ['node_modules'],
loader: {
'.js': 'js',
},
};
function copyStaticAssets() {
for (const a of staticAssets) {
const src = path.resolve(__dirname, a.src);
const dst = path.join(jsOutDir, a.dst);
if (!existsSync(src)) {
console.warn(`[static] ${a.src} not installed; skipping`);
continue;
}
copyFileSync(src, dst);
console.log(`[static] ${a.dst}${path.relative(__dirname, src)}`);
}
}
async function run() {
if (isClean) {
for (const name of bundleNames) {
const f = outFor(name);
if (existsSync(f)) {
rmSync(f);
console.log(`[clean] removed ${path.relative(__dirname, f)}`);
}
}
for (const a of staticAssets) {
const f = path.join(jsOutDir, a.dst);
if (existsSync(f)) {
rmSync(f);
console.log(`[clean] removed ${path.relative(__dirname, f)}`);
}
}
return;
}
ensureDir(jsOutDir);
if (isWatch) {
const ctx = await context(buildOptions);
await ctx.watch();
console.log(`[esbuild] watching → ${path.relative(__dirname, outFile)}`);
for (const name of bundleNames) {
const ctx = await context(buildOptionsFor(name));
await ctx.watch();
console.log(`[esbuild] watching ${name}${path.basename(outFor(name))}`);
}
copyStaticAssets();
console.log('[esbuild] watch mode active');
} else {
const result = await build(buildOptions);
if (result.errors.length) {
process.exitCode = 1;
} else {
const size = statSync(outFile).size;
for (const name of bundleNames) {
const opts = buildOptionsFor(name);
const result = await build(opts);
if (result.errors.length) {
console.error(`[esbuild] ${name} failed`);
process.exitCode = 1;
continue;
}
const size = statSync(opts.outfile).size;
console.log(
`[esbuild] wrote ${path.relative(__dirname, outFile)} (${size} bytes)`
`[esbuild] ${name} ${path.relative(__dirname, opts.outfile)} (${size} bytes)`
);
}
copyStaticAssets();
}
}