Merge pull request #6215 from mempool/mononaut/hashed-themes

build & use content-hashed theme bundles
This commit is contained in:
wiz 2026-01-16 10:19:34 +09:00 committed by GitHub
commit 1e21ab0673
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 102 additions and 27 deletions

2
frontend/.gitignore vendored
View file

@ -5,6 +5,8 @@
/tmp
/out-tsc
server.run.js
.theme-build
theme-manifest.json
# docker
Dockerfile

View file

@ -171,25 +171,15 @@
"src/robots.txt",
"src/config.js",
"src/customize.js",
"src/config.template.js"
"src/config.template.js",
{
"glob": "*.css",
"input": ".theme-build",
"output": "/"
}
],
"styles": [
"src/styles.scss",
{
"input": "src/theme-contrast.scss",
"bundleName": "contrast",
"inject": false
},
{
"input": "src/theme-softsimon.scss",
"bundleName": "softsimon",
"inject": false
},
{
"input": "src/theme-bukele.scss",
"bundleName": "bukele",
"inject": false
},
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
],
"vendorChunk": true,

View file

@ -46,6 +46,22 @@ try {
throw new Error(e);
}
// Inject theme manifest if it exists (created by generate-themes.js)
const THEME_MANIFEST_FILE = 'theme-manifest.json';
try {
const themeManifest = fs.readFileSync(THEME_MANIFEST_FILE, 'utf-8');
const themeFiles = JSON.parse(themeManifest);
let indexHtml = fs.readFileSync('src/index.html', 'utf-8');
const script = ` <script>window.__env=window.__env||{};window.__env.THEME_FILES=${JSON.stringify(themeFiles)};</script>`;
indexHtml = indexHtml.replace('</head>', `${script}\n</head>`);
fs.writeFileSync('src/index.html', indexHtml);
console.log('Injected theme manifest into src/index.html:', themeFiles);
} catch (e) {
if (e.code !== 'ENOENT') {
console.log('Warning: Could not inject theme manifest:', e.message);
}
}
try {
const packageJson = fs.readFileSync('package.json');
packetJsonVersion = JSON.parse(packageJson).version;

View file

@ -0,0 +1,57 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const THEMES = ['contrast', 'softsimon', 'bukele'];
const STAGING_DIR = path.join(__dirname, '.theme-build');
const DIST_DIR = path.join(__dirname, 'dist/mempool/browser');
const MANIFEST_FILE = path.join(__dirname, 'theme-manifest.json');
const command = process.argv[2];
if (command === 'copy') {
const themeFiles = fs.readdirSync(STAGING_DIR).filter(f => f.endsWith('.css'));
for (const dir of fs.readdirSync(DIST_DIR, { withFileTypes: true })) {
if (dir.isDirectory()) {
for (const file of themeFiles) {
fs.copyFileSync(path.join(STAGING_DIR, file), path.join(DIST_DIR, dir.name, file));
}
}
}
console.log(`Copied ${themeFiles.length} theme files to all locale directories`);
} else {
fs.rmSync(STAGING_DIR, { recursive: true, force: true });
fs.mkdirSync(STAGING_DIR, { recursive: true });
const manifest = {};
for (const theme of THEMES) {
const inputFile = path.join(__dirname, `src/theme-${theme}.scss`);
const tempOutput = path.join(STAGING_DIR, `${theme}.tmp.css`);
try {
execSync(`npx sass --style=compressed --no-source-map "${inputFile}" "${tempOutput}"`, {
stdio: 'pipe'
});
} catch (e) {
console.error(`Failed to compile theme-${theme}.scss:`, e.message);
process.exit(1);
}
const css = fs.readFileSync(tempOutput);
const hash = crypto.createHash('md5').update(css).digest('hex').slice(0, 16);
const nonHashedFilename = `${theme}.css`;
fs.copyFileSync(tempOutput, path.join(STAGING_DIR, nonHashedFilename));
const hashedFilename = `${theme}.${hash}.css`;
fs.renameSync(tempOutput, path.join(STAGING_DIR, hashedFilename));
manifest[theme] = hashedFilename;
console.log(`Built ${nonHashedFilename} and ${hashedFilename}`);
}
fs.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
console.log('Theme manifest written to theme-manifest.json');
}

View file

@ -24,16 +24,18 @@
"tsc": "./node_modules/typescript/bin/tsc",
"i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf",
"i18n-pull-from-transifex": "tx pull -a --minimum-perc 1 --force",
"serve": "npm run generate-config && npm run ng -- serve -c local",
"serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod",
"serve:parameterized": "npm run generate-config && npm run ng -- serve -c parameterized",
"start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local",
"start:parameterized": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c parameterized",
"start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora",
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets",
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
"serve": "npm run generate-themes && npm run generate-config && npm run ng -- serve -c local",
"serve:local-prod": "npm run generate-themes && npm run generate-config && npm run ng -- serve -c local-prod",
"serve:parameterized": "npm run generate-themes && npm run generate-config && npm run ng -- serve -c parameterized",
"start": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local",
"start:parameterized": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c parameterized",
"start:local-esplora": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora",
"start:local-prod": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
"start:mixed": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
"build": "npm run generate-themes && npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets",
"generate-themes": "node generate-themes.js",
"copy-themes": "node generate-themes.js copy",
"sync-assets": "npm run copy-themes && rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
"sync-assets-dev": "node sync-assets.js 'src/resources/'",
"generate-config": "node generate-config.js",
"test": "npm run ng -- test",

View file

@ -68,7 +68,7 @@ export class ThemeService {
this.themeState$.next({ theme, loading: false });
};
this.style.onerror = () => this.apply('default');
this.style.href = `${theme}.css`;
this.style.href = this.getThemeFile(theme);
if (!this.stateService.env.customize?.theme) {
this.storageService.setValue('theme-preference', theme);
@ -78,4 +78,12 @@ export class ThemeService {
this.apply('default');
}
}
private getThemeFile(theme: string): string {
const themeFiles = (window as any).__env?.THEME_FILES;
if (themeFiles?.[theme]) {
return themeFiles[theme];
}
return `${theme}.css`;
}
}