[Feature] ScriptCat 应能注入 <script type="module">#1487
Draft
cyfung1031 wants to merge 6 commits into
Draft
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
cyfung1031
force-pushed
the
feat/script-module
branch
from
July 17, 2026 12:08
a873957 to
25ba263
Compare
Collaborator
Author
|
这个可以之后再处理 |
单元测试覆盖 isScriptModule 判断逻辑(含与 @inject-into content 互斥) 及 compileInjectionCode 对 script-module 脚本的编译路径(含 early-start 组合场景);新增 e2e 用例通过实际浏览器验证注入的确是真正的 <script type="module">(顶层 this 为 undefined、document.currentScript 为 null、顶层声明不挂载到 window),且 GM.* API 与既有沙盒 window/ unsafeWindow 语义保持一致。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
新增 mock server 路由 /module-lib.js 提供一个真实的可 export 模块;
script_module_e2e_test.js 在脚本顶层用静态 `import { addNumbers,
MODULE_LIB_MARKER } from "/module-lib.js"` 引入并调用/校验其值。
普通(非 module)<script> 遇到 import 语句会直接抛出 SyntaxError 导致
整份脚本无法执行,因此该用例能确认注入的确是可被浏览器当作 ES module
解析执行的 <script type="module">,而不只是语法特征匹配。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
review 指出 @script-module 曾把 GM/window/unsafeWindow 等经由页面共享的 document.<随机属性> 传给注入的 <script type="module">,页面可 hook document.createElement/appendChild 在临时属性被删除前窃取具备权限的 GM 对象。在浏览器能力支持 module 复用沙盒 Proxy 之前,改为: - isScriptModule() 在代码层面强制要求 @grant none,并排除 @require(module 无法访问外层 classic-script 的词法变量)与 early-start(GM_info 的 isIncognito/sandboxMode/userAgentData 可能尚未补齐,且无法像沙盒 Proxy 那样以同一对象引用事后回填),而不仅在文档中说明限制。 - wrapScriptModuleCode() 不再写入 document/window 等页面共享对象,仅把 序列化后的 GM.info(纯 JSON 数据,无 GM_* 权限方法)内嵌进 module 源码, 并为 import 解析失败/被页面 CSP 拦截等场景挂上可观测的 error 处理。 - restoreJSCodeFromCompiledResource() 补上同一层 module 包装:此前扩展重启 或重新注册脚本后,走缓存恢复路径会退回 classic-script 编译,静态 import/export 随之失效。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
上一 commit 把 @script-module 限制为仅 @grant none 并只暴露序列化的 GM.info,这会让该功能失去意义——ESM + GM.* 同时可用本来就是这个功能的核心 诉求,而非仅支持静态 import。 恢复原有的 document.<临时属性> 转交机制:module 拿到的 GM 就是按 @grant 实际授予的能力对象(本项目按设计只暴露 GM 而非旧式 GM_* 全局,二者本无需 在 ESM 模式下都支持),window/unsafeWindow/top/parent/globalThis 同上。 early-start 组合同样恢复支持(GM_info 与外层沙盒共享同一对象引用,后续 补齐 isIncognito 等字段时 module 侧能同步看到)。 仅保留两处改动: - @require 与 @script-module 互斥仍然拒绝——这是 module 无法访问外层 classic-script 词法变量的客观限制,与本次回退的权限范围无关。 - wrapScriptModuleCode() 新增的 error 事件监听保留,用于在 import 解析 失败或被页面 CSP 拦截时输出可定位到脚本名的 console.error。 restoreJSCodeFromCompiledResource() 的缓存恢复路径修复(此前该路径完全 跳过 module 包装)不受本次回退影响,继续生效。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collaborator
Author
|
补充说明:上一条评论中提到的「首版强制要求 现在恢复为: 保留的两处改动:
关于 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist / 检查清单
@script-moduleES Module injection (GM/TM/VM parity)背景
用户脚本目前始终以 classic script 方式注入,无法使用静态
import/export(例如直接import { chunk } from "https://cdn.jsdelivr.net/npm/lodash-es/+esm"),只能退回@require或手动fetch+eval等变通方式。原生 ES Module(
<script type="module">)支持静态import/export、顶层await、独立的模块作用域(顶层this为undefined,顶层声明不挂载到window),但chrome.userScriptsAPI 目前无法直接以type="module"注册用户脚本。参考:
Close #1486
本次改动
@script-modulemetadata:脚本声明后,编译阶段改为生成一段会在页面 MAIN world 内动态创建<script type="module">的注入代码(wrapScriptModuleCode()),而不是直接把脚本代码当作 classic script 执行。document.<随机属性>转交拿到GM(按脚本@grant实际授予的方法)、window、unsafeWindow、top、parent、globalThis,读取后立即删除该临时属性。不提供旧式GM_*全局或GM_info/CAT_*——ESM 模式下按设计只暴露GM命名空间对象。isScriptModule()判定规则(@script-module为空值或"true"时生效):@inject-into content(USER_SCRIPT 隔离世界)互斥,二者同时声明时@script-module不生效。@require互斥:@require的内容在外层 classic script 作用域执行,module 无法访问那些词法变量,因此同时声明时@script-module不生效,回退普通编译路径。@unwrap、@early-start可以组合:@unwrap优先于@script-module(有@unwrap时走 scriptlet 编译路径,module 包装不生效);@early-start下 module 包装仍然生效,转交的GM与外层沙盒共享同一对象引用,事后补齐isIncognito/sandboxMode/userAgentData时 module 侧能同步看到。restoreJSCodeFromCompiledResource()(扩展重启或重新注册脚本后的缓存恢复路径)现在与首次编译路径(compileInjectionCode())共用同一个wrapScriptModuleCode(),保证两条路径生成一致的注入代码。error事件监听,在 import 解析失败或被页面 CSP 拦截时输出可定位到脚本名的console.error,而非静默失败。isScriptModule、wrapScriptModuleCode、compileInjectionCode、restoreJSCodeFromCompiledResource的各组合分支)与 e2e 测试(example/tests/script_module_e2e_test.js+e2e/gm-api.spec.ts),在真实浏览器中验证:module 顶层this为undefined、document.currentScript为null、顶层声明不挂载到window、window/unsafeWindow与既有沙盒脚本语义一致、静态import真实可用(区别于语法特征匹配)、GM.setValue/GM.getValue正常工作。实现考虑
1. 为什么用临时
document属性转交GM/window等对象<script type="module">是独立解析、独立作用域的顶层脚本,无法像普通函数闭包那样直接共享外层 classic script 的变量。要把已经按@grant构建好的GM对象(以及window/unsafeWindow等沙盒变量)交给它,只能通过两者都能访问的共享对象。当前实现选择在 MAIN world 的
document上写入一个随机命名的临时属性,由 module 在自己的第一行代码里读取后立即delete。这个方案的时间窗口很短(属性写入到 module 读取之间只隔一次appendChild+ 浏览器调度 module 执行),但由于<script type="module">的执行本身是异步的(不像普通内联<script>),如果页面提前 hook 了document.createElement/appendChild或主动枚举document的新增属性,理论上存在在属性被删除前读取到它的窗口。已评估的更强隔离方案:改为用
import()动态加载一个 Blob URL 模块,把GM等对象作为普通函数参数传给模块导出的函数,从而完全不经过任何页面可见对象。但这要求把脚本代码包在一个函数里,会牺牲用户脚本顶层import语句的自由摆放位置以及真正的顶层await语义。本次维持现有document属性转交方案,把这个取舍留给后续按需评估。2.
@require与@script-module的互斥@require引入的代码在外层 classic script 的词法作用域里执行,@script-module生成的 module 是完全独立的作用域,无法访问外层的@require变量。与其让两者组合后静默产生"require 的内容在 module 里访问不到"的错误行为,isScriptModule()在检测到@require时直接判定@script-module不生效,回退到普通编译路径(@require正常工作,只是不再支持静态import)。3. 缓存恢复路径与首次编译路径的一致性
compileInjectionCode()(首次编译/重新构建)与restoreJSCodeFromCompiledResource()(扩展重启或重新注册脚本后,从CompiledResourceDAO缓存恢复)此前是两套独立实现,后者遗漏了 module 包装逻辑,导致缓存恢复后@script-module脚本会静默退化为 classic script(import/export报语法错误)。现在两条路径统一调用wrapScriptModuleCode(),避免因为走了哪条路径而产生行为差异。已知限制
GM等对象通过临时document属性转交,理论上存在页面主动 hook DOM API 提前读取的时间窗口(见"实现考虑"第 1 点);已知晓,本次未改为更强隔离的方案。@require与@script-module不能同时生效(详见上文)。@inject-into content(USER_SCRIPT 隔离世界)与@script-module不能同时生效,@script-module目前只支持默认的 MAIN world 注入。<script type="module">会受页面自身 CSP(script-src)限制;拦截时会有console.error提示,但不提供绕过手段,也不在扩展 UI 里做额外的诊断展示。<base>的解析规则完全由浏览器按当前页面上下文处理,ScriptCat 不做任何改写或特殊解析。GM(按@grant授予的方法),不提供旧式GM_*全局、GM_info或CAT_*;需要这些的脚本请勿使用@script-module。sourceType仍固定为"script",未按@script-module动态切换为"module",模块顶层import/export语法在编辑器里可能被误报为语法错误(不影响实际运行)。chrome.userScriptsworld/realm 行为上的潜在差异做浏览器矩阵验证。建议审查重点
isScriptModule()的判定条件(@inject-into content互斥、@require互斥、@unwrap/@early-start组合行为)是否符合预期;wrapScriptModuleCode()生成的临时document属性转交时序,以及"实现考虑"第 1 点描述的残余风险是否可以接受;restoreJSCodeFromCompiledResource()与compileInjectionCode()在各种 metadata 组合下是否始终生成一致的注入代码;console.error提示是否清晰可定位到具体脚本;@grant授予的GM方法在 module 内的行为是否与外层沙盒脚本一致。验证
pnpm exec vitest run --no-coverage:3382 个测试全部通过。pnpm exec tsc --noEmit:无类型错误。pnpm exec eslint .:无报错(example/tests/*.js按项目现有配置被 lint 忽略,属预期)。pnpm exec prettier --check:格式检查通过。e2e/gm-api.spec.ts的@script-module tests用例 +example/tests/script_module_e2e_test.js)此前已在本地全部通过(见下方评论历史),本轮未重新执行浏览器测试。