手搓JSVM第 11 篇:打包、模块与 CLI:把 VM 产物变成可运行文件

文章来源声明: 原文作者:泯泷; 来源站点:掘金; 原文链接:https://juejin.cn/post/7684195084155174921; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

为 JS VM 编译产物提供从数据到可执行文件的完整打包思路,适合学习编译器/运行时工程化、CLI 工具链与模块封装的开发者参考。

手搓JSVM第 11 篇:打包、模块与 CLI:把 VM 产物变成可运行文件 --------------------------------------

11-cover.png

源码仓库

1. 本文目标

前面几篇已经把编译器和 runtime 的核心部件搭起来了。接下来要解决的是工程化闭环:

如何把 bytecode、constantPool、函数元信息和 runtime 打包成一个可运行 JS 文件?

本篇覆盖:

  1. ProgramArtifact
  2. Runtime Source
  3. Pack
  4. IIFE / CJS / ESM 包装
  5. Module Bundling
  6. CLI
  7. Debug Info

2. 为什么需要打包

如果只生成 bytecode:

[<span>4</span>, <span>0</span>, <span>0</span>, <span>19</span>, <span>0</span>]

这还只是编译数据,用户不能把它当作完整程序直接运行。

要真正执行,还需要 runtime:

<span>function</span> <span>__jsvmpRun</span>(<span>metadata, globalObject</span>) { ... }

同时还要把 runtime 所需的 metadata 一并带上:

{
  bytecode,
  constantPool,
  functions
}

形象化比喻:游戏卡带

bytecode 像游戏关卡数据,runtime 像游戏机,metadata 像说明书。三者单独存在都不构成最终交付物;打包就是把它们装进一张卡带,让用户拿到后可以直接运行。

11-pack-cartridge.png

3. 前置知识

概念说明
Artifact编译产物数据
Runtime执行 bytecode 的解释器
Pack把 runtime 与 artifact 拼成 JS
IIFE立即执行函数表达式
CJSCommonJS 输出
ESMES Module 输出
CLI命令行入口

4. 源码中的对应位置

概念源码位置说明
`compile()``src/compiler/pipeline.ts`主编译流程
`ProgramArtifact``src/compiler/types.ts`bytecode 与元数据结构
`generateRuntimeSource()``src/compiler/runtime-gen.ts`生成运行时源码
`packArtifact()``src/compiler/pack.ts`打包 runtime + metadata
`bundle()``src/compiler/bundler.ts`模块打包
CLI`src/cli.ts`命令行参数与入口

5. 核心数据结构

ProgramArtifact

它解决什么问题

把 runtime 执行程序时需要的 bytecode、常量和函数信息集中保存。

11-program-artifact.png

教学版简化实现
<span>const</span> artifact = {
  <span>format</span>: <span>'iife'</span>,
  <span>bytecode</span>: [],
  <span>constantPool</span>: [],
  <span>functions</span>: [],
  <span>entryFunctionId</span>: <span>0</span>,
};

Runtime Source

它解决什么问题

提供真正读取并解释 bytecode 的执行函数。

教学版简化实现
<span>function</span> <span>generateRuntimeSource</span>(<span></span>) {
  <span>return</span> <span>`function __run(metadata){ return 0 }`</span>;
}

Packed Output

它解决什么问题

把 runtime 与 artifact 合到一起,让用户得到一个可直接运行的 JS 文件。

教学版简化实现
<span>const</span> output = <span>`
(function(){
  const metadata = <span>${<span>JSON</span>.stringify(artifact)}</span>;
  <span>${runtimeSource}</span>
  return __run(metadata);
})()
`</span>;

6. Mermaid 图解

flowchart LR
    A[&#34;Source File&#34;] --> B[&#34;compile()&#34;]
    B --> C[&#34;ProgramArtifact&#34;]
    C --> D[&#34;Runtime Source&#34;]
    D --> E[&#34;packArtifact()&#34;]
    C --> E
    E --> F[&#34;Output JS&#34;]

11-pack-pipeline.png

flowchart TD
    A[&#34;CLI input&#34;] --> B[&#34;resolve format&#34;]
    B --> C{&#34;bundle?&#34;}
    C -->|yes| D[&#34;bundle modules&#34;]
    C -->|no| E[&#34;compile single file&#34;]
    D --> F[&#34;parse/lower/emit&#34;]
    E --> F
    F --> G[&#34;pack&#34;]
    G --> H[&#34;write output&#34;]

11-cli-flow.png

7. 伪代码

function compile(inputFile, outputFile, options):
    source = read inputFile
    format = resolveFormat(source)

    if bundle enabled:
        source = bundle(source)

    ast = parse(source)
    ir = lower(ast)
    bytecode = emit(ir)
    runtime = generateRuntimeSource()
    code = pack(runtime, bytecode)

    write outputFile
    return code

8. 教学版实现代码

<span>function</span> <span>packArtifact</span>(<span>artifact</span>) {
  <span>const</span> metadata = <span>JSON</span>.<span>stringify</span>(artifact);

  <span>return</span> <span>`
(function () {
  function __miniRun(metadata) {
    const code = metadata.bytecode;
    const pool = metadata.constantPool;
    const regs = [];
    let pc = 0;

    while (pc < code.length) {
      const op = code[pc++];
      if (op === 4) regs[code[pc++]] = pool[code[pc++]];
      else if (op === 19) return regs[code[pc++]];
      else throw new Error('unknown opcode: ' + op);
    }
  }

  return __miniRun(<span>${metadata}</span>);
})()
`</span>.<span>trim</span>();
}

9. 示例输入与输出

Artifact

<span>const</span> artifact = {
  <span>bytecode</span>: [<span>4</span>, <span>0</span>, <span>0</span>, <span>19</span>, <span>0</span>],
  <span>constantPool</span>: [<span>42</span>],
};

Packed Output

<span>const</span> code = <span>packArtifact</span>(artifact);
<span>console</span>.<span>log</span>(<span>eval</span>(code)); <span>// 42</span>

10. 执行过程拆解

Step阶段产物
1parse/lower/emit`ProgramArtifact`
2generate runtime`__jsvmpRun` 源码
3pack自包含 JS 文件
4execute outputVM runtime 执行 bytecode
5return输出结果

11. 与原始源码的差异

主题教学版正式源码
Runtime极简同步执行支持 sync/async/generator
Format只演示 IIFE支持 IIFE/ESM/CJS
Module不支持支持 ESM/CJS bundling
Global简化注入 `__vmp_global`、require/module/exports
Debug不支持可包含 debugInfo

12. 常见问题

Q1:为什么 output 仍然是 JS?

因为这个项目做的是 JS virtualization:源码先变成 bytecode,再由自定义 JS runtime 解释,所以最终产物仍然需要交给 JS 引擎运行。

Q2:打包是不是混淆?

不是。打包只是把 runtime 和 metadata 放到一起。混淆属于额外阶段。

13. 本文小结

到这里,从“编译数据”到“可运行产物”的工程闭环就完整了。

下一篇将继续讲:

第 12 篇:完整最小 JSVM 实现与源码设计复盘