安全与权限: - 站点访问权限改为 optional_host_permissions,保存配置时逐域名授权;移除 host_permissions 与 tabs,安装提示不再出现全站数据访问与浏览记录 - 主口令不再写入 storage.session,改用 IndexedDB 中不可导出的 CryptoKey 句柄恢复解锁,锁定即丢弃 - content script 注入范围只注册已授权域名,并随权限变化即时收敛 自动登录: - 支持 iframe 内的登录表单:脚本注入所有帧,逐帧按自身地址匹配配置 - 手动填充改为逐帧探测,定向发送到真正含密码框的帧 - 限流修正:只在真正触发提交后计数;判定登录成功后立即清零;达到上限时页面给出可见提示;重置判断只看启用中的配置 - SPA 重试改为 DOM 变更门控 + 退避,并单独监听已发现的 Shadow Root 工程化与文档: - 引入 ESLint(扁平配置)与 Prettier,CI 增加 lint 与 format:check - 信息类日志改为 debugLog(默认静默,chrome.storage.local.debugLog 开关) - 测试 123 → 128 用例(新增权限、iframe、限流相关用例) - README / PRIVACY / CHANGELOG 同步
153 lines
6.4 KiB
JavaScript
153 lines
6.4 KiB
JavaScript
#!/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"];
|
||
/** 独立入口:各自成文件,不做跨文件打包 */
|
||
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);
|
||
};
|
||
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 可同步)`,
|
||
);
|
||
}
|
||
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,
|
||
}),
|
||
),
|
||
);
|
||
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,
|
||
});
|
||
}
|
||
|
||
const refs = await verifyReferences();
|
||
console.log(
|
||
`[build] v${version} ${release ? "release" : "dev"} 构建完成:${entryPoints().length} 个入口,校验 ${refs.length} 处引用`,
|
||
);
|
||
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") });
|
||
}
|