#!/usr/bin/env node
/**
* Publish the extension design pack into ../../pack (served by the backend at
* /api/v1/extension/pack*). Sources: src/content.css, src/popup/popup.css,
* pack-src/tables.json and pack-src/config.json (optional, hand-edited —
* generate snapshots with --emit-tables / --emit-config).
*
* Content that differs from the currently published pack auto-bumps
* packVersion; identical content is a no-op. Files land in v<version>/ first
* and pack.json is written last (via a .tmp rename), so a client never sees a
* pack.json pointing at files that are not served yet.
*
* Usage: node tools/publish-design-pack.js [--version N] [--min-extension X.Y.Z]
*/
const fs = require("fs");
const path = require("path");
const EXTENSION_DIR = path.resolve(__dirname, "..");
const PACK_DIR = path.resolve(EXTENSION_DIR, "..", "pack");
const PACK_JSON = path.join(PACK_DIR, "pack.json");
const PACK_FILES = ["content.css", "popup.css", "tables.json", "config.json"];
function args() {
const opts = {};
const argv = process.argv.slice(2);
for (let i = 0; i < argv.length; i++) {
if (argv[i] === "--version") opts.version = Number(argv[++i]);
else if (argv[i] === "--min-extension") opts.minExtension = argv[++i];
else if (argv[i] === "--emit-tables") opts.emitTables = true;
else if (argv[i] === "--emit-config") opts.emitConfig = true;
else {
console.error(`unknown argument: ${argv[i]}`);
process.exit(1);
}
}
return opts;
}
function ownVersion() {
const makefile = fs.readFileSync(path.join(EXTENSION_DIR, "Makefile"), "utf8");
const match = /^VERSION\s*:?=\s*(.+)$/m.exec(makefile);
return match ? match[1].trim() : "0.0.0";
}
function readPackJson() {
try {
return JSON.parse(fs.readFileSync(PACK_JSON, "utf8"));
} catch {
return null;
}
}
function hashAndSize(text) {
const crypto = require("crypto");
const bytes = Buffer.from(text, "utf8");
return { sha256: crypto.createHash("sha256").update(bytes).digest("hex"), size: bytes.length };
}
function emitSnapshots() {
const opts = args();
const srcDir = path.join(EXTENSION_DIR, "pack-src");
fs.mkdirSync(srcDir, { recursive: true });
if (opts.emitTables) {
const snapshot = require(path.join(EXTENSION_DIR, "src", "field-detect.js")).keywordTables;
fs.writeFileSync(
path.join(srcDir, "tables.json"),
JSON.stringify(snapshot, null, "\t") + "\n"
);
console.log("wrote pack-src/tables.json");
}
if (opts.emitConfig) {
const packShared = require(path.join(EXTENSION_DIR, "src", "pack-shared.js"));
fs.writeFileSync(
path.join(srcDir, "config.json"),
JSON.stringify(packShared.DEFAULT_CONFIG, null, "\t") + "\n"
);
console.log("wrote pack-src/config.json");
}
if (opts.emitTables || opts.emitConfig) return;
}
function publish() {
const opts = args();
if (opts.emitTables || opts.emitConfig) return emitSnapshots();
const current = readPackJson();
const sources = {
"content.css": fs.readFileSync(path.join(EXTENSION_DIR, "src", "content.css"), "utf8"),
"popup.css": fs.readFileSync(path.join(EXTENSION_DIR, "src", "popup", "popup.css"), "utf8"),
};
for (const name of ["tables.json", "config.json"]) {
const file = path.join(EXTENSION_DIR, "pack-src", name);
if (fs.existsSync(file)) sources[name] = fs.readFileSync(file, "utf8");
}
const unchanged =
current !== null &&
PACK_FILES.every((name) => {
const dir = path.join(PACK_DIR, `v${current.packVersion}`);
const published = path.join(dir, name);
return sources[name] === undefined
? !fs.existsSync(published)
: fs.existsSync(published) && fs.readFileSync(published, "utf8") === sources[name];
});
if (unchanged) {
console.log(`pack unchanged (v${current.packVersion}) — nothing to publish`);
return;
}
const version = opts.version || (current ? current.packVersion + 1 : 1);
if (current && version <= current.packVersion) {
console.error(`refusing packVersion ${version}: content changed, must be > ${current.packVersion}`);
process.exit(1);
}
const minExtension = opts.minExtension || ownVersion();
const versionDir = path.join(PACK_DIR, `v${version}`);
fs.mkdirSync(versionDir, { recursive: true });
const files = {};
for (const [name, text] of Object.entries(sources)) {
fs.writeFileSync(path.join(versionDir, name), text);
const { sha256, size } = hashAndSize(text);
if (size > 262144) {
console.error(`${name} exceeds the 256KB client cap`);
process.exit(1);
}
files[name] = { sha256, size };
}
const packJson = {
packVersion: version,
minExtensionVersion: minExtension,
maxExtensionVersion: null,
publishedAt: new Date().toISOString(),
files,
};
const tmp = PACK_JSON + ".tmp";
fs.writeFileSync(tmp, JSON.stringify(packJson, null, "\t") + "\n");
fs.renameSync(tmp, PACK_JSON);
for (const dir of fs.readdirSync(PACK_DIR)) {
if (dir === "v" + String(version) || dir === "pack.json") continue;
if (/^v\d+$/.test(dir))
console.log(`note: older pack dir ${dir} remains on disk (clients read storage, server serves only v${version})`);
}
console.log(`published pack v${version} (minExtensionVersion ${minExtension})`);
}
publish();