The page's script is TypeScript in web/src, built and minified with swc
- web/src/*.ts: the script that was inline in index.html and login.html, split along its existing sections. Still one scope, concatenated in order, not modules. - web/build.mjs strips the types, puts the script in the page and minifies it with swc; build.rs runs it into OUT_DIR and web.rs include_str!s the result. 137 KB -> 106 KB. - npx tsc -p . type-checks web/src, loosely; the handful of annotations it needed change no behaviour. - The Docker build installs node and swc (npm ci --omit=dev). - Two list requests racing no longer let the older one win, and switching tabs clears the selection it closes, which made a browser test flaky. Closes #23, #24. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
52
web/build.mjs
Normal file
52
web/build.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
// Builds the pages ipx serves: each page's TypeScript from web/src, types stripped and minified
|
||||
// by swc, put into the page in place of its <script data-src>, and the whole page minified.
|
||||
// build.rs runs it into OUT_DIR, where web.rs include_str!s the result, so the binary still
|
||||
// carries every page and nothing is served from disk.
|
||||
//
|
||||
// node web/build.mjs [out-dir] default out-dir: web/dist
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import swc from '@swc/core';
|
||||
import html from '@swc/html';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// The files are one script, concatenated in this order, not modules: they share one top-level
|
||||
// scope, as the single inline script did, and code that runs at load needs what came before it.
|
||||
const PAGES = {
|
||||
'index.html': ['util', 'feeds', 'feedpage', 'items', 'player', 'dialogs', 'events'],
|
||||
'login.html': ['login'],
|
||||
};
|
||||
|
||||
export function buildPage(name, { minify = true } = {}) {
|
||||
const src = PAGES[name].map(f => fs.readFileSync(path.join(here, 'src', f + '.ts'), 'utf8')).join('\n');
|
||||
const js = swc.transformSync(src, {
|
||||
filename: name + '.ts',
|
||||
jsc: {
|
||||
parser: { syntax: 'typescript' },
|
||||
target: 'es2022',
|
||||
// Top-level names stay as they are: markup calls some of them by name (onclick="closeModal()")
|
||||
// and the browser tests reach others (player, savePos) through page.evaluate.
|
||||
minify: minify ? { compress: { toplevel: false }, mangle: { toplevel: false } } : undefined,
|
||||
},
|
||||
isModule: false,
|
||||
minify,
|
||||
}).code;
|
||||
const page = fs.readFileSync(path.join(here, name), 'utf8');
|
||||
const marker = /<script data-src="[^"]*"><\/script>/;
|
||||
if (!marker.test(page)) throw new Error(`${name} has no <script data-src> to put its script in`);
|
||||
// A function, so a `$&` or `$1` in the script is not read as a replacement pattern.
|
||||
const out = page.replace(marker, () => `<script>${js}</script>`);
|
||||
if (!minify) return out;
|
||||
const r = html.minifySync(out, { minifyJs: false, minifyCss: true, removeComments: true });
|
||||
const bad = (r.errors || []).filter(e => e.level === 'Error');
|
||||
if (bad.length) throw new Error(`${name}: ${bad.map(e => e.message).join('; ')}`);
|
||||
return r.code;
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const out = process.argv[2] || path.join(here, 'dist');
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
for (const name of Object.keys(PAGES)) fs.writeFileSync(path.join(out, name), buildPage(name));
|
||||
}
|
||||
Reference in New Issue
Block a user