@ttsc/wasm

@ttsc/wasmpackages/wasm)是浏览器内 ttsc playground 的基础:一个 Go host 包(编译成 WebAssembly)加 JS 引导脚手架。它让 ttsc 的 build/check/transform/插件能在浏览器里跑,无需后端。

两半结构

packages/wasm/
├── host/           # Go: 编进 wasm 的宿主
│   ├── host.go         # Expose: 安装 globalThis[apiName]
│   ├── api.go          # build/check/transform 实现
│   ├── plugin.go       # 插件分派
│   ├── fountain.go     # snapshot/getDiagnostics/... fountain verbs
│   └── host_native.go  # 非 wasm 构建的占位
├── src/            # JS: 引导 + 桥接
│   ├── bootTtsc.ts     # 启动 wasm
│   ├── createMemFS.ts  # 内存文件系统桥
│   ├── parseResult.ts  # 解析结果信封
│   └── structures/     # 类型
└── build/          # 构建脚本 (build-wasm.cjs, pack-prepare.cjs)

JS 侧(src/index.ts)re-export boot helper、MemFS 桥、类型表面。Go 侧 host/ 随包 ship 在磁盘上但经 go build 加载,不从 JS import。

host.Expose:JS 桥接

Expose(apiName, cfg)host/host.go:54)在 globalThis[apiName] 安装标准 verb 加 fountain verb,然后让 Go 运行时永久存活:

globalThis[apiName].version()                     → 版本横幅
globalThis[apiName].build({ cwd, tsconfig })      → Promise<ITtscResult>
globalThis[apiName].check({ cwd, tsconfig })      → Promise<ITtscResult>
globalThis[apiName].transform({ cwd, tsconfig })  → Promise<ITtscResult>
globalThis[apiName].plugin({ name, command, ...}) → Promise<ITtscResult>
globalThis[apiName].plugins()                     → 注册插件名 string[]

加上 fountain verb(snapshot/getDiagnostics/getNodeAtPosition/…,fountainAPIMap()),在 snapshot handle 表上工作。

几个 wasm 特定设计:

  • 拒绝双 Exposehost.go:65):第二次调用会泄漏第一批 js.FuncOf(Go pin 住 js.Func 不 GC)、起第二个 keepalive goroutine、覆盖 ready resolver。所以用 exposed atomic.Bool CAS 守卫,失败时 console.error + JS 可见的 reject 信号(globalThis[apiName+"Failed"])而非 panic——panic 会在任何 Ready resolver 触发前终止 Go 运行时,让 bootTtscawait ready 无限挂起。
  • 永久 idle goroutinehost.go:111):for { time.Sleep(time.Hour) } 让 wasm 运行时存活而不触发 Go 死锁检测器——select {} 会在 FS 路径把请求交给 JS 那刻被误判为"所有 goroutine 睡眠"。

Promise 桥接

makePromisehost.go:261)把 Go 计算包成 JS Promise:executor 同步捕获 resolve/reject,工作本身在 goroutine 里跑,让 JS 事件循环能在 Go 收回调前驱动 fs.stat 等到完成。

stdout/stderr 捕获

wasm 里 syscall.Pipe 返回 pipe: not implemented on js——wasm 目标不支持管道。所以 runWithCapturedIOhost.go:325)用 MemFS 临时文件重定向 os.Stdout/os.Stderr:插件 Run 像原生旁车那样写 stdout/stderr,捕获后让 JS 宿主在 console 面板渲染,无需 spawn 子进程。captureMu 串行化临时替换进程全局 stdout/stderr,captureCounter 避免并发分派的临时文件名碰撞。

MemFS

createMemFS.ts + MemFSError.ts 实现内存文件系统桥,让浏览器里没有真实磁盘也能 open/write/read。这是 runWithCapturedIO 临时文件方案能工作的基础。

boot 流程

bootTtsc.ts 启动 wasm:JS 在 go.run 前注册 globalThis[${apiName}Ready] resolver,等 wasm boot(Expose 末尾 invoke ready resolver)。parseResult.ts 解析 ITtscResult 信封({ code, stdout, stderr, result }result 是 build/check/transform 的 JSON,JS 侧 JSON.parse)。

API 稳定性

README 与 host.go:35 标注:实验性,v1.0 前签名可能在 minor 版本间变。生产 playground 要 pin 精确版本。

不变量

  • Expose 每个 wasm 实例至多调一次(CAS 守卫)。
  • 永久 idle goroutine 防 Go 死锁检测器误杀。
  • stdout/stderr 经 MemFS 临时文件捕获(wasm 无管道)。
  • build/check/transform 与 plugin 分派共享 { code, stdout, stderr, result } 形状,JS 一个错误分支处理。

维护者提示

  • 别引入第二个 Expose 路径——双 Expose 是 wasm 资源泄漏。
  • 改 stdout 捕获时记住 wasm 无 os.Pipe,必须走 MemFS。
  • host_native.go 是非 wasm 构建占位,让包在非 wasm 平台也能编译。

接下来