frontend module conversion) ui-state was the smallest, most isolated module: 35 lines, zero legacy- global dependencies, single concern (localStorage helpers under the 'ped_ui/' namespace). Ideal first migration to validate the strangler- fig pattern end-to-end. Changes: - New: client/ui-state.ts — exports get/set/del + UIState bundle. Keeps (window as any).UIState = UIState assignment so the 10+ legacy consumers (sub-nav.js, peGuide.js, app.js, etc.) keep working without modification. As each consumer is migrated, it will replace the window.UIState read with an explicit import. - client/main.ts: import './ui-state' so it gets bundled into main.js. - scripts/build-client.mjs: explicit ENTRIES list (just 'main.ts' for now) — was scanning every top-level .ts as an entry, which produced duplicate ui-state.js bundle. - public/index.html: <script defer src="/js/ui-state.js"> removed; the TS bundle (/dist/main.js) now provides window.UIState. - public/js/ui-state.js: deleted (now lives in client/). Verification: - npm run typecheck:client → 0 errors - npm run build:client → 349-byte main.js bundle (gzipped will be ~200B) - 46/46 unit tests pass - The 10 cross-file consumers of UIState.get/set/del still work because the window assignment lives in the bundle. Pattern documented for next migrations: each new client/X.ts is imported as a side-effect from main.ts. The window-bridge stays until every consumer also moves to ES imports.
70 lines
2.2 KiB
JavaScript
70 lines
2.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// ============================================================
|
|
// build-client.mjs — Compiles client/**/*.ts to public/dist/.
|
|
//
|
|
// Strangler-fig pattern: TS modules live under client/, legacy
|
|
// IIFE files stay under public/js/ until each is migrated.
|
|
// Each top-level .ts file in client/ becomes one ES-module bundle
|
|
// in public/dist/ that index.html can load via:
|
|
// <script type="module" src="/dist/<name>.js"></script>
|
|
//
|
|
// Run:
|
|
// npm run build:client # one-shot
|
|
// npm run build:client -- --watch # rebuild on change
|
|
// ============================================================
|
|
|
|
import { build, context } from 'esbuild';
|
|
import { readdirSync, mkdirSync, existsSync } from 'node:fs';
|
|
import { resolve, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
const SRC = resolve(PROJECT_ROOT, 'client');
|
|
const OUT = resolve(PROJECT_ROOT, 'public/dist');
|
|
|
|
const watch = process.argv.includes('--watch');
|
|
|
|
if (!existsSync(SRC)) {
|
|
console.error(`[build-client] Source dir does not exist: ${SRC}`);
|
|
process.exit(1);
|
|
}
|
|
mkdirSync(OUT, { recursive: true });
|
|
|
|
// Explicit entry list — every other .ts file in client/ is a sub-module
|
|
// imported by an entry. Add new entries here when a feature truly needs
|
|
// its own separate bundle (different DOM lifecycle, lazy-loaded route,
|
|
// etc.). For most modernization work, just import into main.ts.
|
|
const ENTRIES = [
|
|
'main.ts',
|
|
];
|
|
|
|
const entryFiles = ENTRIES.map(name => {
|
|
const p = resolve(SRC, name);
|
|
if (!existsSync(p)) {
|
|
console.error(`[build-client] Entry file does not exist: ${p}`);
|
|
process.exit(1);
|
|
}
|
|
return p;
|
|
});
|
|
|
|
const buildOpts = {
|
|
entryPoints: entryFiles,
|
|
outdir: OUT,
|
|
bundle: true,
|
|
format: 'esm',
|
|
target: 'es2022',
|
|
platform: 'browser',
|
|
sourcemap: true,
|
|
minify: !watch, // unminified in watch mode for easier debugging
|
|
logLevel: 'info',
|
|
};
|
|
|
|
if (watch) {
|
|
const ctx = await context(buildOpts);
|
|
await ctx.watch();
|
|
console.log('[build-client] Watching for changes…');
|
|
} else {
|
|
await build(buildOpts);
|
|
console.log(`[build-client] Built ${entryFiles.length} entry point(s) → ${OUT}`);
|
|
}
|