2026-09-17 23:00:47 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构建管线:tsc 只做类型检查(--noEmit),esbuild 负责产出 dist/。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 产物分三类,对应三种运行环境,不能混在同一个 bundle 里:
|
|
|
|
|
|
* 1. 全局脚本(iife):utils / env-store / crypto-store
|
|
|
|
|
|
* 通过 globalThis 暴露 API,被 content script 直接读全局、被 background 用
|
|
|
|
|
|
* importScripts 加载,也必须能被 Node 测试 require,因此各自独立成文件。
|
|
|
|
|
|
* 2. 独立入口(iife):background / content
|
|
|
|
|
|
* 运行期自行 importScripts 或读上面的全局变量,esbuild 不会(也不能)跟踪它们。
|
|
|
|
|
|
* 3. 模块入口(esm,可打包):popup/main
|
|
|
|
|
|
* 由 popup.html 以 type="module" 加载,其静态依赖(i18n / strength / messaging /
|
|
|
|
|
|
* ui-utils / crypto-file / popup/*)全部内联进 dist/popup/main.js。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用法:
|
|
|
|
|
|
* node scripts/build.mjs 开发构建(带 sourcemap,不压缩)
|
|
|
|
|
|
* node scripts/build.mjs --release 发布构建(压缩,无 sourcemap)
|
|
|
|
|
|
* node scripts/build.mjs --watch 监听重建
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
|
|
|
|
import { existsSync } from "node:fs";
|
|
|
|
|
|
import { readFile, rm } from "node:fs/promises";
|
|
|
|
|
|
import path from "node:path";
|
|
|
|
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
|
|
import * as esbuild from "esbuild";
|
|
|
|
|
|
|
|
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
|
|
const DIST = path.join(ROOT, "dist");
|
|
|
|
|
|
|
|
|
|
|
|
/** 全局脚本:必须独立成文件,iife 格式,彼此通过 globalThis 通信 */
|
2026-09-18 00:05:20 +08:00
|
|
|
|
const GLOBAL_SCRIPTS = ["utils", "permissions", "env-store", "crypto-store", "key-store"];
|
2026-09-17 23:00:47 +08:00
|
|
|
|
/** 独立入口:各自成文件,不做跨文件打包 */
|
|
|
|
|
|
const STANDALONE_SCRIPTS = ["background", "content"];
|
|
|
|
|
|
/** 由 background 通过 chrome.scripting 按需注入的脚本(manifest 里不再静态声明) */
|
|
|
|
|
|
const INJECTED_ASSETS = ["dist/utils.js", "dist/content.js"];
|
|
|
|
|
|
|
|
|
|
|
|
function entryPoints() {
|
|
|
|
|
|
return [
|
|
|
|
|
|
// UMD 全局脚本:不打包、不指定 format,原样保留 module.exports 分支
|
|
|
|
|
|
...GLOBAL_SCRIPTS.map((name) => ({ in: `src/${name}.ts`, out: `dist/${name}.js`, bundle: false })),
|
|
|
|
|
|
// 独立入口:不打包(运行期靠 importScripts / 全局变量协作)
|
|
|
|
|
|
...STANDALONE_SCRIPTS.map((name) => ({ in: `src/${name}.ts`, out: `dist/${name}.js`, bundle: false })),
|
|
|
|
|
|
// popup:真正的 ESM 打包,静态依赖全部内联
|
|
|
|
|
|
{ in: "src/popup/main.ts", out: "dist/popup/main.js", format: "esm", bundle: true },
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 类型检查单独一步,确保类型错误不会被 esbuild 静默跳过 */
|
|
|
|
|
|
function typecheck() {
|
|
|
|
|
|
const tscBin = path.join(ROOT, "node_modules", "typescript", "bin", "tsc");
|
|
|
|
|
|
execFileSync(process.execPath, [tscBin, "--noEmit"], { cwd: ROOT, stdio: "inherit" });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function esbuildOptions({ release, format, bundle }) {
|
|
|
|
|
|
const options = {
|
|
|
|
|
|
bundle,
|
|
|
|
|
|
platform: "browser",
|
|
|
|
|
|
target: "chrome114",
|
|
|
|
|
|
charset: "utf8",
|
|
|
|
|
|
minify: release,
|
|
|
|
|
|
sourcemap: release ? false : "linked",
|
|
|
|
|
|
legalComments: "none",
|
|
|
|
|
|
logLevel: "warning",
|
|
|
|
|
|
};
|
|
|
|
|
|
// 只在需要时指定 format:一旦指定 iife,esbuild 会把 UMD 的 module.exports 换成
|
|
|
|
|
|
// 自己的合成模块对象,Node 测试 require 就取不到导出内容
|
|
|
|
|
|
if (format) options.format = format;
|
|
|
|
|
|
return options;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** manifest.json / popup.html 里引用到的文件必须真实存在,避免"构建成功但装不上" */
|
|
|
|
|
|
async function verifyReferences() {
|
|
|
|
|
|
const manifest = JSON.parse(await readFile(path.join(ROOT, "manifest.json"), "utf8"));
|
|
|
|
|
|
const refs = new Set();
|
|
|
|
|
|
|
2026-09-18 00:05:20 +08:00
|
|
|
|
const add = (p) => {
|
|
|
|
|
|
if (typeof p === "string") refs.add(p);
|
|
|
|
|
|
};
|
2026-09-17 23:00:47 +08:00
|
|
|
|
add(manifest.background?.service_worker);
|
|
|
|
|
|
add(manifest.side_panel?.default_path);
|
|
|
|
|
|
Object.values(manifest.icons || {}).forEach(add);
|
|
|
|
|
|
Object.values(manifest.action?.default_icon || {}).forEach(add);
|
|
|
|
|
|
(manifest.content_scripts || []).forEach((cs) => (cs.js || []).forEach(add));
|
|
|
|
|
|
// manifest 不再静态声明 content_scripts,这些文件由 background 按需注入,同样必须存在
|
|
|
|
|
|
INJECTED_ASSETS.forEach(add);
|
|
|
|
|
|
|
|
|
|
|
|
const html = await readFile(path.join(ROOT, "popup.html"), "utf8");
|
|
|
|
|
|
for (const m of html.matchAll(/(?:src|href)="([^"]+)"/g)) {
|
|
|
|
|
|
const ref = m[1];
|
|
|
|
|
|
if (/^(https?:)?\/\//.test(ref) || ref.startsWith("#") || ref.startsWith("data:")) continue;
|
|
|
|
|
|
refs.add(ref);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const missing = [...refs].filter((ref) => !existsSync(path.join(ROOT, ref)));
|
|
|
|
|
|
if (missing.length) {
|
|
|
|
|
|
throw new Error(`以下被引用的文件不存在,构建产物不完整:\n - ${missing.join("\n - ")}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return [...refs];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** manifest.json 是版本号的唯一来源,package.json 需与之保持一致 */
|
|
|
|
|
|
async function checkVersionConsistency() {
|
|
|
|
|
|
const manifest = JSON.parse(await readFile(path.join(ROOT, "manifest.json"), "utf8"));
|
|
|
|
|
|
const pkg = JSON.parse(await readFile(path.join(ROOT, "package.json"), "utf8"));
|
|
|
|
|
|
if (manifest.version !== pkg.version) {
|
2026-09-18 00:05:20 +08:00
|
|
|
|
console.warn(
|
|
|
|
|
|
`[build] 版本号不一致:manifest.json=${manifest.version} package.json=${pkg.version}(执行 npm run release 可同步)`,
|
|
|
|
|
|
);
|
2026-09-17 23:00:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
return manifest.version;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function buildAll({ release = false, watch = false } = {}) {
|
|
|
|
|
|
typecheck();
|
|
|
|
|
|
const version = await checkVersionConsistency();
|
|
|
|
|
|
|
|
|
|
|
|
if (watch) {
|
|
|
|
|
|
const contexts = await Promise.all(
|
2026-09-18 00:05:20 +08:00
|
|
|
|
entryPoints().map((ep) =>
|
|
|
|
|
|
esbuild.context({
|
|
|
|
|
|
...esbuildOptions({ release, format: ep.format, bundle: ep.bundle }),
|
|
|
|
|
|
entryPoints: [ep.in],
|
|
|
|
|
|
outfile: ep.out,
|
|
|
|
|
|
}),
|
|
|
|
|
|
),
|
2026-09-17 23:00:47 +08:00
|
|
|
|
);
|
|
|
|
|
|
await Promise.all(contexts.map((c) => c.watch()));
|
|
|
|
|
|
console.log(`[build] 监听中… v${version}(Ctrl+C 退出)`);
|
|
|
|
|
|
return { version, watching: true };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await rm(DIST, { recursive: true, force: true }); // 清掉历史产物,避免残留旧文件被打进包
|
|
|
|
|
|
for (const ep of entryPoints()) {
|
2026-09-18 00:05:20 +08:00
|
|
|
|
await esbuild.build({
|
|
|
|
|
|
...esbuildOptions({ release, format: ep.format, bundle: ep.bundle }),
|
|
|
|
|
|
entryPoints: [ep.in],
|
|
|
|
|
|
outfile: ep.out,
|
|
|
|
|
|
});
|
2026-09-17 23:00:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const refs = await verifyReferences();
|
2026-09-18 00:05:20 +08:00
|
|
|
|
console.log(
|
|
|
|
|
|
`[build] v${version} ${release ? "release" : "dev"} 构建完成:${entryPoints().length} 个入口,校验 ${refs.length} 处引用`,
|
|
|
|
|
|
);
|
2026-09-17 23:00:47 +08:00
|
|
|
|
return { version, refs };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
|
|
|
|
if (isDirectRun) {
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
|
await buildAll({ release: args.includes("--release"), watch: args.includes("--watch") });
|
|
|
|
|
|
}
|