Files
auto-login/scripts/build.mjs
T

153 lines
6.4 KiB
JavaScript
Raw Normal View History

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 通信 */
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();
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) {
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(
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()) {
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();
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") });
}