Files
陈银军 1b0c3c012c feat: 按域名授权、iframe 多帧填充、自动提交限流修正,并补齐工程规范与文档
安全与权限:
- 站点访问权限改为 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 同步
2026-09-18 00:05:20 +08:00

153 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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") });
}