# ESM 不认 NODE_PATH，用 createRequire 引外部 node_modules

**日期**：2026-05-17
**触发**：写 `docs/templates/flow-renderer.mjs`（ESM CLI 脚本）需要 import `js-yaml`，但脚本在 `docs/templates/` 下没有 node_modules；`js-yaml` 已在 `backend/node_modules/`。
**结论**：ESM 模块解析**不读 `NODE_PATH` 环境变量**——这是 CommonJS 时代的 trick，ESM 时代被废弃。要让 ESM `.mjs` 引用别处目录的 node_modules，必须用 `createRequire` 显式指定解析起点。

---

## 现象

```bash
NODE_PATH=backend/node_modules node docs/templates/flow-renderer.mjs in.yaml out.svg
```

报错（环境变量完全失效）：

```
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'js-yaml' imported from
  /.../docs/templates/flow-renderer.mjs
Did you mean to import "js-yaml/index.js"?
```

## 元根因

- Node 的 ESM resolver（基于 `import.meta.url` 和 package.json `exports`）**不查 NODE_PATH**
- NODE_PATH 是 CommonJS `require()` 时代的 trick，Node ESM 时代显式不支持
- ESM 解析只看：①相对路径、②package.json `exports` 字段、③以 importer 文件位置为基准沿目录向上爬 node_modules

`cd backend && node ../path/script.mjs` 也救不了——Node ESM 解析以 mjs 文件位置为基准，不是 cwd。

## 解法

用 `createRequire` 创建一个 CommonJS-style require，**手动指定解析基准**为 backend 的 package.json url：

```js
import { createRequire } from 'module';

// 让 require 从 backend/package.json 位置开始解析 node_modules
const requireFromBackend = createRequire(new URL('../../backend/package.json', import.meta.url));
const yaml = requireFromBackend('js-yaml');
```

`createRequire(url)` 接受 path/URL 作为解析起点——返回的 require 会从该位置向上爬 node_modules 找包。

## 何时再用

- 任何 ESM `.mjs` 脚本在仓库外围（`scripts/` / `docs/templates/` / `tools/` 等）想用 backend/frontend 已 install 的包
- 不想给"工具脚本"目录加独立 `package.json` + 重复 npm install

## 已废弃方案

- ❌ `NODE_PATH=backend/node_modules node script.mjs` —— ESM 忽略
- ❌ `cd backend && node ../path/script.mjs` —— Node ESM 以 mjs 文件位置为基准
- ❌ `import yaml from '/abs/path/to/backend/node_modules/js-yaml/index.js'` —— 绝对路径硬编码不便携
- ❌ 在工具目录单独 `npm init -y && npm install js-yaml` —— 多个工具目录会重复 install + 版本漂移

## 反向使用建议

如果工具脚本未来要拆出成独立 npm package，反过来：把 createRequire 改回正常 `import yaml from 'js-yaml'`，工具目录自己 `package.json` 声明依赖。createRequire 是"借用别处依赖"的过渡方案。
