Three merge-fallout bugs that combined to leave the services grid empty
and most UI inert:
1. error-handler.js was bundled into onboarding.js (loaded 3rd), but
globals.js in core.js (loaded 1st) does `const errorHandler = new
ErrorHandler()` at top level. ErrorHandler was undefined when core.js
ran -> ReferenceError -> globals.js stopped, so window.APPS,
_showTotpOverlay, loadServices, etc. were never set, and init.js
blew up on every call into core's exports.
Moved error-handler.js to the start of the core.js bundle so the
class is on window before any other script touches it.
2. setup-wizard.js also declared `const errorHandler = new ErrorHandler()`
at top level. Classic scripts share the document's top-level lexical
environment, so this collided with globals.js's declaration ->
redeclaration SyntaxError in features.js. Removed setup-wizard.js's
copy; it picks up the global one.
3. tooltip-definitions.js closed its `(function(window){...})(window);`
IIFE at line ~171 ("Validation module loaded"), then the TOOLTIP_
DEFINITIONS array, getter helpers, window.TooltipDefinitions export,
and final `debug(...)` log all sat at top level — outside the IIFE,
where `debug` was no longer in scope. Removed the early close and
added one at EOF so the whole file is in one IIFE.
196 lines
6.3 KiB
JavaScript
196 lines
6.3 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const esbuild = require('esbuild');
|
|
|
|
const JS = (...parts) => path.join(__dirname, 'js', ...parts);
|
|
const DIST = path.join(__dirname, 'dist');
|
|
const INDEX_HTML = path.join(__dirname, 'index.html');
|
|
const SW_JS = path.join(__dirname, 'sw.js');
|
|
|
|
// Bundle definitions — files are concatenated in order, then minified
|
|
const bundles = {
|
|
'core.js': [
|
|
// error-handler.js MUST be first — globals.js below does
|
|
// `const errorHandler = new ErrorHandler()` at top level, which throws
|
|
// ReferenceError if the ErrorHandler class isn't already on `window`.
|
|
JS('error-handler.js'),
|
|
JS('globals.js'),
|
|
JS('skeleton-loader.js'),
|
|
JS('theme.js'),
|
|
JS('totp-auth.js'),
|
|
JS('service-credentials.js'),
|
|
JS('totp-settings.js'),
|
|
JS('core', 'credentials.js'),
|
|
JS('core', 'grid.js'),
|
|
JS('core', 'dns.js'),
|
|
JS('core', 'logs.js'),
|
|
JS('core', 'service-modals.js'),
|
|
JS('core', 'service-infrastructure.js'),
|
|
JS('core', 'service-crud.js'),
|
|
JS('core', 'service-create.js'),
|
|
JS('live-events.js'),
|
|
JS('service-filter.js'),
|
|
JS('batch-operations.js'),
|
|
],
|
|
'features.js': [
|
|
JS('logo-customization.js'),
|
|
JS('setup-wizard.js'),
|
|
JS('app-selector.js'),
|
|
JS('recipes.js'),
|
|
JS('import-export.js'),
|
|
JS('error-logs.js'),
|
|
JS('container-logs.js'),
|
|
JS('snapshot.js'),
|
|
JS('smart-arr-connect.js'),
|
|
JS('notification-settings.js'),
|
|
JS('panel-tabs.js'),
|
|
JS('backup-restore.js'),
|
|
JS('resource-monitor.js'),
|
|
JS('health-check.js'),
|
|
JS('update-management.js'),
|
|
JS('docker-resources.js'),
|
|
JS('compose-import.js'),
|
|
JS('container-exec.js'),
|
|
JS('audit-log.js'),
|
|
JS('weather.js'),
|
|
JS('clock.js'),
|
|
JS('card-badges.js'),
|
|
JS('theme-builder.js'),
|
|
JS('license.js'),
|
|
],
|
|
'onboarding.js': [
|
|
JS('driver.min.js'),
|
|
// error-handler.js moved to core.js bundle; window.ErrorHandler is already
|
|
// set before this bundle runs.
|
|
JS('progress-tracker.js'),
|
|
JS('theme-adapter.js'),
|
|
JS('tooltip-definitions.js'),
|
|
JS('dns-template-selector.js'),
|
|
JS('tour-manager.js'),
|
|
JS('onboarding.js'),
|
|
],
|
|
'init.js': [
|
|
JS('core', 'init.js'),
|
|
JS('keyboard-shortcuts.js'),
|
|
],
|
|
};
|
|
|
|
function updateInlineScriptCspHash() {
|
|
const html = fs.readFileSync(INDEX_HTML, 'utf8');
|
|
const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)];
|
|
const target = scripts.find(match => {
|
|
const block = match[1] || '';
|
|
return block.includes("license-topbar-version") || block.includes("openVersionInfo") || block.includes("widget-");
|
|
});
|
|
|
|
if (!target) {
|
|
throw new Error('Could not find inline dashboard bootstrap script in index.html');
|
|
}
|
|
|
|
const scriptContent = target[1];
|
|
const hash = crypto.createHash('sha256').update(scriptContent).digest('base64');
|
|
const updatedHtml = html.replace(
|
|
/script-src 'self' 'sha256-[^']+';/,
|
|
`script-src 'self' 'sha256-${hash}';`
|
|
);
|
|
|
|
if (updatedHtml !== html) {
|
|
fs.writeFileSync(INDEX_HTML, updatedHtml);
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
async function build() {
|
|
// Ensure dist/ exists
|
|
if (!fs.existsSync(DIST)) fs.mkdirSync(DIST);
|
|
|
|
const results = {};
|
|
|
|
for (const [outName, files] of Object.entries(bundles)) {
|
|
// Read and concatenate
|
|
const parts = [];
|
|
for (const file of files) {
|
|
if (!fs.existsSync(file)) {
|
|
console.warn(` WARN: ${path.relative(__dirname, file)} not found, skipping`);
|
|
continue;
|
|
}
|
|
parts.push(fs.readFileSync(file, 'utf8'));
|
|
}
|
|
const concatenated = parts.join(';\n');
|
|
|
|
// Minify with esbuild (safe to re-minify already-minified code like driver.min.js)
|
|
const { code } = await esbuild.transform(concatenated, {
|
|
minify: true,
|
|
target: 'es2020',
|
|
});
|
|
|
|
const outPath = path.join(DIST, outName);
|
|
fs.writeFileSync(outPath, code);
|
|
|
|
const rawSize = (Buffer.byteLength(concatenated) / 1024).toFixed(1);
|
|
const minSize = (Buffer.byteLength(code) / 1024).toFixed(1);
|
|
results[outName] = { rawSize, minSize, fileCount: files.length };
|
|
}
|
|
|
|
const cspHash = updateInlineScriptCspHash();
|
|
const cacheTag = updateServiceWorkerCache();
|
|
|
|
// Summary
|
|
console.log('\n DashCaddy Frontend Build\n');
|
|
console.log(' Bundle Files Raw Min');
|
|
console.log(' ─────────────────────────────────────────');
|
|
let totalRaw = 0, totalMin = 0;
|
|
for (const [name, r] of Object.entries(results)) {
|
|
console.log(` ${name.padEnd(18)} ${String(r.fileCount).padStart(3)} ${r.rawSize.padStart(6)} KB ${r.minSize.padStart(6)} KB`);
|
|
totalRaw += parseFloat(r.rawSize);
|
|
totalMin += parseFloat(r.minSize);
|
|
}
|
|
console.log(' ─────────────────────────────────────────');
|
|
console.log(` ${'Total'.padEnd(18)} ${totalRaw.toFixed(1).padStart(6)} KB ${totalMin.toFixed(1).padStart(6)} KB`);
|
|
console.log(`\n Output: ${DIST}`);
|
|
console.log(` CSP Hash: sha256-${cspHash}`);
|
|
console.log(` SW Cache: dashcaddy-shell-${cacheTag}\n`);
|
|
}
|
|
|
|
// Rewrites the CACHE constant in sw.js to a tag derived from the bundle
|
|
// contents. Every change in dist/ produces a new cache name; on next load
|
|
// the SW's activate handler wipes all older caches, so users never get
|
|
// stuck on stale precached bundles after a release.
|
|
function updateServiceWorkerCache() {
|
|
const sw = fs.readFileSync(SW_JS, 'utf8');
|
|
const hash = crypto.createHash('sha256');
|
|
for (const name of Object.keys(bundles)) {
|
|
hash.update(fs.readFileSync(path.join(DIST, name)));
|
|
}
|
|
const tag = hash.digest('hex').slice(0, 10);
|
|
const updated = sw.replace(
|
|
/const CACHE = 'dashcaddy-shell-[^']+';/,
|
|
`const CACHE = 'dashcaddy-shell-${tag}';`
|
|
);
|
|
if (updated !== sw) {
|
|
fs.writeFileSync(SW_JS, updated);
|
|
}
|
|
return tag;
|
|
}
|
|
|
|
// Watch mode
|
|
if (process.argv.includes('--watch')) {
|
|
console.log(' Watching for changes...\n');
|
|
build();
|
|
|
|
const jsDir = path.join(__dirname, 'js');
|
|
let debounce = null;
|
|
fs.watch(jsDir, { recursive: true }, (event, filename) => {
|
|
if (!filename || !filename.endsWith('.js')) return;
|
|
clearTimeout(debounce);
|
|
debounce = setTimeout(() => {
|
|
console.log(` Changed: ${filename}`);
|
|
build();
|
|
}, 200);
|
|
});
|
|
} else {
|
|
build();
|
|
}
|