74 lines
3.7 KiB
JavaScript
74 lines
3.7 KiB
JavaScript
// lib/activate.js
|
|
// activateTheme -- staged, persists COMPOSITE hash, refreshes assets (ARCHITECTURE.md 7.8)
|
|
const crypto = require("crypto");
|
|
const { getState } = require("@saltcorn/data/db/state");
|
|
|
|
const { compileTheme } = require("./compile");
|
|
const themeStore = require("./themeStore");
|
|
const cssCache = require("./cssCache");
|
|
const activePointer = require("./activePointer");
|
|
const { normalizeLayoutTree } = require("./layoutTree");
|
|
const { PLUGIN_NAME } = require("./constants");
|
|
|
|
function compositeHash(entries /* CompiledEntry[] */) {
|
|
// Fold the default + every role-override entry's content hash into ONE cache-buster so that
|
|
// re-activating ANY single role changes activeHash (and thus every emitted ?v). (folded fix)
|
|
const h = crypto.createHash("sha1");
|
|
for (const e of entries.filter(Boolean)) { h.update(`${e.role ?? "_"}:${e.hash};`); }
|
|
return h.digest("hex").slice(0, 8);
|
|
}
|
|
|
|
|
|
async function activateTheme(id, role) {
|
|
// Resolve uniformly through the store so the SEAM FIX applies (getById returns a
|
|
// normalized ThemeDTO for builtin ids too) -- STAGE-1's compile probe then sees the
|
|
// same defaulted token shape STAGE-2 caches and serves.
|
|
const theme = await themeStore.getById(id);
|
|
if (!theme) { const e = new Error("theme not found"); e.stage = "compile"; throw e; }
|
|
|
|
// STAGE 1: compile (distinct failure stage -> compile_failed). Pointer untouched on throw.
|
|
let compiled;
|
|
try { compiled = compileTheme(theme.tokens, { engine: theme.engine }); }
|
|
catch (e) { e.stage = "compile"; throw e; }
|
|
|
|
// STAGE 2: prime cache for ALL pointer-implied keys (default + every activeByRole role).
|
|
const { activeThemeId, activeByRole } = activePointer.getActivePointer();
|
|
const nextByRole = role == null ? activeByRole : { ...activeByRole, [role]: id };
|
|
const nextDefault = role == null ? id : activeThemeId;
|
|
cssCache.bustAll();
|
|
const entries = [];
|
|
if (nextDefault) entries.push(await cssCache.warm(nextDefault, null));
|
|
for (const r of Object.keys(nextByRole)) entries.push(await cssCache.warm(nextByRole[r], Number(r)));
|
|
|
|
// STAGE 3: persist pointer + COMPOSITE hash, re-register, refresh assets, propagate
|
|
// (distinct failure -> activation_failed).
|
|
// activeHash folds in the default AND every role-override entry's content hash, so re-activating
|
|
// a single role CHANGES activeHash -> the per-role ?v link changes -> immutable role CSS is busted.
|
|
// (folded review fix: role-override cache-buster never changed -> stale immutable CSS)
|
|
const nextHash = compositeHash(entries);
|
|
|
|
// LAYOUT MODE: persist the activated theme's layout tree into cfg so wrap() can read
|
|
// it synchronously (and it rides backup with the pointer). No-op when layout mode is off.
|
|
const liveCfg = getState().plugin_cfgs[PLUGIN_NAME] || {};
|
|
let layoutPatch = {};
|
|
if (liveCfg.layoutMode) {
|
|
const tree = normalizeLayoutTree(theme.layoutTree);
|
|
layoutPatch = role == null
|
|
? { activeLayoutTree: tree }
|
|
: { activeLayoutTreeByRole: { ...(liveCfg.activeLayoutTreeByRole || {}), [role]: tree } };
|
|
}
|
|
|
|
try {
|
|
const patch = role == null
|
|
? { activeThemeId: id, activeByRole: nextByRole, activeHash: nextHash, ...layoutPatch }
|
|
: { activeByRole: nextByRole, activeHash: nextHash, ...layoutPatch }; // ALWAYS update activeHash, incl. role path
|
|
await activePointer.setActivePointer(patch); // upsert + loadPlugin + refresh_views + processSend
|
|
} catch (e) {
|
|
cssCache.bustTheme(id); // drop orphan cache entry (folded fix)
|
|
e.stage = "activation"; throw e;
|
|
}
|
|
return { activeThemeId: nextDefault, activeByRole: nextByRole, cssHash: nextHash };
|
|
}
|
|
|
|
|
|
module.exports = { activateTheme, compositeHash };
|