Module Global S1
中文标题:模块全局
- 阶段: Stage 1
- 状态: 进行中
- ECMAScript 版本: —
- 同步时间: 2026年8月26日
- English original · 官方仓库
本提案解决了在同一个 realm 内以新的全局作用域评估模块的需求,避免了新 realm 的身份不连续问题。它提议切断模块环境与 realm 全局环境的联系,并用用户定义的'作用域上限'替换。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
模块全局
阶段:1
提案发起人:
- Zbyszek Tenerowicz (ZTZ),Consensys,@naugtur
- Kris Kowal (KKL),Agoric,@kriskowal
- Richard Gibson (RGN),Agoric,@gibson042
- Mark S. Miller (MM),Agoric,@erights
问题陈述
在同一种 Realm 内,以新的全局作用域为上下文来评估一个模块及其依赖项的一种方式。
切断 ModuleEnvironmentRecord 实例中与 GlobalEnvironmentRecord 的联系,并在提供 ScopeCeiling 时用其替换。
本提案承接自 HardenedJS
Compartment提案 中的 Evaluators 先前提案,并依赖于 proposal-import-hook、proposal-esm-phase-imports 和 proposal-source-phase-imports。
动机
领域特定语言
像 Mocha、Jest 和 Jasmine 这样的工具会将其领域特定语言的动词和名词安装在全局作用域中。
目前,要隔离这些更改则需要创建新的 realm,而创建新的 realm 会带来身份不连续的危险。例如,array instanceof Array 不如 Array.isArray 可靠,而且这种危险不仅限于那些已通过像 Array.isArray 或可采用的 promise Promise 等变通方法预见到此问题的内在对象。
一些工具通过使用平台现有的创建新的全局上下文的设施(尽管是 iframe 或 Node.js vm 模块)来解决这个问题。然后,它们有义务将一个 realm 的内在对象移植到另一个 realm 上,这对于在语法上不可否认的 Realm 特定内在对象(如 AsyncFunction 构造函数和原型)的情况会泄漏,并且要求实现者保持警惕,将每个内在对象从一个 realm 移植到另一个 realm。我们发现这种安排既脆弱又泄漏,而且在内存效率和开发人员时间方面成本高昂。
本提案提供了一种替代解决方案:在具有共享内在对象的独立全局作用域中评估模块或脚本。
强制执行最小权限原则
在网络上,同源策略在阻止跨站点脚本攻击方面已经足够有效,以至于攻击者被迫从同源内部发起攻击。对攻击者来说方便的是,JavaScript 库生态系统的丰富性提供了大量进入同源的向量。现代 Web 应用程序的绝大部分是其供应链,包括最终将被纳入将在同源中运行的脚本的代码,还包括生成这些脚本的工具,以及准备开发环境的工具。
同源策略保护着一种迅速恶化的虚构,即 Web 浏览器在仅两方之间调解交互:服务方和用户方。对于现代应用程序,特别是那些调解多方之间交互或具有深层供应链的平台,Web 应用程序开发者需要一种机制来隔离第三方依赖项,并最小化它们对诸如高分辨率计时器或具有网络、计算或存储能力承载接口的对象等强大对象的访问。
一些宿主,包括在 [ECMA TC53][tc53] 中代表的嵌入式系统社区,没有可用于构建同源策略的来源,并且已选择通过高级 Compartment 接口在隔离的评估器上构建其安全模型。
隔离/封装不可靠的代码
现代软件的组成方式已经削弱了这样一种假设的有效性,即每个参与的作者的意图都很好地保持一致,以利于软件正常运行。现在,随着 编码代理 和 氛围编码 的流行,一种全新的不可靠性被加入,其中创建语法有效但实际上不可预测的 JavaScript,并将其集成到现有软件中以检查它是否似乎实现了所需功能,正成为一种流行的构建软件的方式。
这并不是一个全新的关注点,因为测试运行器一直关注隔离测试用例,以避免它们依赖其他测试用例的全局副作用。现在,这成为更广泛受众关注的问题,而且风险更大。
使用 Global 构造函数,能够在软件维护者不知情的情况下,以不可靠的代码无法依赖共享全局状态的方式隔离应用程序的各个部分,而这种能力是新的。来自独立工作的代理的 AI 生成的源代码可能会为要使用的全局变量带来冲突的名称,并且可能需要单独的全局作用域才能协作或共存。同样,可以冻结新的全局中不可靠代码随后运行的部分,以防止 AI 或包作者进行误导性的内联 polyfill 尝试。使用新的全局而不是新的 Realm 避免了诸如身份不连续之类的问题,这些问题会阻碍在需要跨隔离和非隔离代码进行函数调用的软件组合。
增量或上下文内执行
有些工具目前使用更复杂且成本更高的机制(类似于 领域特定语言 中描述的机制)来提供在工具非常特定的上下文中执行 JavaScript 代码片段的能力。
这包括 REPL、编辑器中的内联代码执行结果(例如 Quokka.js)以及浏览器中 IDE 的各种用例。
在用户提供的代码片段的多次执行之间维护全局状态将受益于控制作用域的能力。
提案
允许在不访问全局上下文的情况下评估模块,方法是通过切断 ModuleEnvironmentRecord 实例中与 GlobalEnvironmentRecord 的联系,将 [[OuterEnv]] 设置为与 module.[[Realm]].[[GlobalEnv]] 不同的记录,用用户定义的全局模拟替换它,我们称之为 作用域上限。
作用域上限
16.2.1.7.3.1 InitializeEnvironment ( )
注意:
module是源文本模块记录- NewObjectEnvironment 可以被更早调用
[[ScopeCeiling]]可能会被间接访问
ModuleRecord 与 ScopeCeiling 之间的关联最终需要由中介引入——可能是引入模块映射或 importHook 的同一中介。(例如 Compartment)
我们最初考虑在
ModuleSource的构造函数中将ScopeCeiling与其关联。它不再与 esm-phase-imports 提案良好地组合。
执行上下文交互
注意:
module.[[Environment]]成为moduleContext的LexicalEnvironment
-
对
x的 IdentifierReference 进行求值。根据 §13.1.3(IdentifierReference 的运行时语义:求值),调用ResolveBinding("x")。 -
ResolveBinding从 正在运行执行上下文的LexicalEnvironment读取env。正在运行执行上下文是module.[[Context]],其LexicalEnvironment是模块的ModuleEnvironmentRecord。由于模块代码始终是严格模式,strict= true。调用GetIdentifierReference(moduleEnv, "x", true)。 -
GetIdentifierReference在ModuleEnvironmentRecord上调用moduleEnv.HasBinding("x")。模块环境仅保存模块自身的顶级var/let/const/class声明和导入的绑定。x不是这些中的任何一个,所以HasBinding返回 false。 -
GetIdentifierReference读取outer=moduleEnv.[[OuterEnv]],到达ScopeCeiling。 -
ScopeCeiling.[[OuterEnv]]为 null,因此无法继续推进到 Realm 全局进行查找。
此更改将产生一个选项,即在无法通过词法方式访问全局的上下文中运行模块。
不可否认的 将来自共享的 Realm,通过其全局名称从词法上访问内在对象的便利性将取决于提供 ScopeCeiling 的人添加它们。
交叉语义
依赖于一个总括提案来提供定义 ScopeCeiling 的入口点,因此很可能将并入 Compartment 提案或其子集。