Ctrl K
ComunidadGuías…

scripts/package.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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.");