scripts/package.mjs
Rama main · réplica de lectura del repositorio original.
// Builds store-upload and manual-install archives from one fresh dist/ build.
//
// Chromium stores share the exact same package. Firefox needs an MV3 background
// script fallback, a stable Gecko ID, and data-collection declarations.
import { execFileSync } from "node:child_process";
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const DIST = join(ROOT, "dist");
const ARTIFACTS = join(ROOT, "artifacts");
const FIREFOX_ADDON_ID = "web-clipper@usememos.com";
// Firefox desktop gained built-in data consent in 140; Android gained it in 142.
// `gecko.strict_min_version` covers both unless a separate Android manifest is used.
const FIREFOX_MIN_VERSION = "142.0";
const VALID_TARGETS = new Set(["all", "chrome", "edge", "firefox"]);
const requestedTarget = process.argv[2] ?? "all";
if (!VALID_TARGETS.has(requestedTarget)) {
throw new Error(`Unknown target "${requestedTarget}". Use one of: ${[...VALID_TARGETS].join(", ")}`);
}
const packageJson = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
const requireTrustedSourceTree = () => {
if (!existsSync(join(ROOT, ".git"))) throw new Error("Packaging requires a Git checkout.");
const status = execFileSync("git", ["status", "--porcelain", "--untracked-files=normal"], {
cwd: ROOT,
encoding: "utf8",
});
if (status.trim()) throw new Error("Refusing to package a dirty working tree. Commit or stash all tracked and untracked files first.");
};
// Every archive must correspond to one reproducible tracked source tree.
requireTrustedSourceTree();
const baseManifestPath = join(DIST, "manifest.json");
if (!existsSync(baseManifestPath)) throw new Error("dist/manifest.json is missing; run `pnpm build` first.");
const baseManifest = JSON.parse(readFileSync(baseManifestPath, "utf8"));
if (baseManifest.version !== packageJson.version) {
throw new Error(`Version mismatch: package.json is ${packageJson.version}, dist manifest is ${baseManifest.version}.`);
}
if (requestedTarget === "all") rmSync(ARTIFACTS, { recursive: true, force: true });
mkdirSync(ARTIFACTS, { recursive: true });
const zipDirectory = (sourceDir, outputPath) => {
rmSync(outputPath, { force: true });
// -X omits platform-specific extended attributes; the manifest remains at ZIP root.
// Finder can drop .DS_Store into dist/ while it builds; it never belongs in a package.
execFileSync("zip", ["-X", "-qr", outputPath, ".", "-x", "*.DS_Store"], { cwd: sourceDir, stdio: "inherit" });
};
const storeManifest = (target) => {
const manifest = structuredClone(baseManifest);
// Store signing owns release IDs/keys. Keep the key only in unpacked dev builds.
delete manifest.key;
// Store-hosted packages must let their store supply the update URL.
delete manifest.update_url;
if (target === "firefox") {
const worker = manifest.background?.service_worker;
if (!worker) throw new Error("Firefox packaging needs background.service_worker in the built manifest.");
// Firefox uses the script as an event page and does not support service_worker.
manifest.background.scripts = [worker];
delete manifest.background.service_worker;
manifest.browser_specific_settings = {
gecko: {
id: FIREFOX_ADDON_ID,
strict_min_version: FIREFOX_MIN_VERSION,
// The clipper signs users in, sends the active page URL, and sends selected/page
// content to the user's Memos instance. These declarations power Firefox's
// built-in install consent and must stay aligned with the privacy policy.
data_collection_permissions: {
required: ["authenticationInfo", "browsingActivity", "websiteContent"],
},
},
};
}
return manifest;
};
const manualChromiumManifest = () => {
const manifest = structuredClone(baseManifest);
// Manual Chromium installs need the public key so chrome.identity uses the
// registered store extension ID regardless of the extracted directory.
if (!manifest.key) throw new Error("The manual Chromium package requires a manifest key to preserve its OAuth identity.");
delete manifest.update_url;
return manifest;
};
const createStage = (label, manifest) => {
const stage = mkdtempSync(join(tmpdir(), `memos-web-clipper-${label}-`));
cpSync(DIST, stage, { recursive: true });
writeFileSync(join(stage, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
return stage;
};
const createStoreStage = (target) => {
return createStage(target, storeManifest(target));
};
const packageManualChromium = () => {
const stage = createStage("chromium-manual", manualChromiumManifest());
const outputPath = join(ARTIFACTS, `memos-web-clipper-chromium-v${packageJson.version}.zip`);
try {
zipDirectory(stage, outputPath);
} finally {
rmSync(stage, { recursive: true, force: true });
}
return outputPath;
};
const packageChromium = (targets) => {
const stage = createStoreStage("chromium");
const chromePath = join(ARTIFACTS, `memos-web-clipper-chrome-v${packageJson.version}.zip`);
const edgePath = join(ARTIFACTS, `memos-web-clipper-edge-v${packageJson.version}.zip`);
const primaryPath = targets.includes("chrome") ? chromePath : edgePath;
try {
zipDirectory(stage, primaryPath);
if (targets.length === 2) {
// Chrome and Edge are code/manifest compatible; duplicate the exact tested bytes.
copyFileSync(primaryPath, edgePath);
}
} finally {
rmSync(stage, { recursive: true, force: true });
}
return targets.map((target) => (target === "chrome" ? chromePath : edgePath));
};
const packageFirefox = () => {
const stage = createStoreStage("firefox");
const firefoxPath = join(ARTIFACTS, `memos-web-clipper-firefox-v${packageJson.version}.zip`);
try {
// Mozilla's validator catches Firefox manifest and compatibility problems.
execFileSync("pnpm", ["exec", "web-ext", "lint", "--source-dir", stage, "--output", "text"], {
cwd: ROOT,
stdio: "inherit",
});
zipDirectory(stage, firefoxPath);
} finally {
rmSync(stage, { recursive: true, force: true });
}
return firefoxPath;
};
const created = [];
if (requestedTarget === "all") created.push(packageManualChromium());
if (requestedTarget === "all" || requestedTarget === "chrome" || requestedTarget === "edge") {
created.push(...packageChromium(requestedTarget === "all" ? ["chrome", "edge"] : [requestedTarget]));
}
if (requestedTarget === "all" || requestedTarget === "firefox") created.push(packageFirefox());
console.log("\nCreated artifacts:");
for (const file of created) console.log(`- artifacts/${basename(file)}`);
console.log("\ndist/ remains the unpacked development build with its stable Chrome key.");