Node CLI が黙って exit 13 で止まる — ESM の動的 import 循環をテストで止める
トップレベル await の CLI と動的 import の循環で起きる exit 13 の原因・直し方と、循環を CI で検出する node:test 一式
約5.7万トークンの節約 (API料金換算で約86円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「Node CLI が黙って exit 13 で止まる — ESM の動的 import 循環をテストで止める」は、開発プロセスカテゴリのAI指示書(MDファイル)です。トップレベル await の CLI と動的 import の循環で起きる exit 13 の原因・直し方と、循環を CI で検出する node:test 一式この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約5.7万トークン(API料金換算で約86円)・95%のトークンを節約できます。
- カテゴリ
- 開発プロセス
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約6万トークン
- この巻物使用時
- 約3,000トークン
- 節約量
- 約5.7万トークン (約86円)
- 更新日
- 2026-10-13
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/node-cli-exit-13-esm-import/raw を読み込んで、この指示書どおりに実装して"
中身
Node CLI が何も言わず exit 13 で止まる — ESM の動的 import 循環をテストで機械的に止める
症状
node tools/<cli>.mjs ...が何も出力せず終了コード 13 で終わる。Warning: Detected unsettled top-level await at file:///.../<cli>.mjs:NNN process.exitCode = await main();が出ることがある。- 単体テストは全部通る(依存を注入しているため問題の経路を通らない)。
原因
入口モジュール A(CLI)が if (isEntry) process.exitCode = await main() のようにトップレベル await で main を待ち、main の中で await import('./b.mjs') する。b.mjs が import { x } from './a.mjs' で A を静的 import していると、b は A の評価完了を待ち、A は b の読み込み完了を待つ。互いに待ち合ってイベントループが空になり、Node は exit 13 で終了する。
静的 import だけの循環は評価順で解決されるので止まらない(ただし増やさない)。
直し方
共有している関数(例: ラベル解決・設定読み込み)を第三のファイル lib/<name>.mjs に切り出し、A と b の両方がそこから import する。A 側は export { fn } で再エクスポートして既存の呼び出し元を壊さない。
単体テストが実行ファイルを一時ディレクトリへコピーして動かす作りなら、コピー一覧に新しい lib ファイルを足す(忘れると ERR_MODULE_NOT_FOUND)。
再発を止めるテスト(node:test、そのまま使える)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const dir = path.dirname(fileURLToPath(import.meta.url)); // 検査するディレクトリ
const STATIC_RE = /(?:^|[\s;])(?:import|export)\s[^'"`;]*?from\s*['"](\.{1,2}\/[^'"]+\.m?js)['"]/g;
const DYNAMIC_RE = /import\(\s*['"](\.{1,2}\/[^'"]+\.m?js)['"]\s*\)/g;
const KNOWN_STATIC_CYCLES = new Set([/* 'a.mjs -> b.mjs -> a.mjs' と理由コメント */]);
const list = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) => {
const f = path.join(d, e.name);
if (e.isDirectory()) return e.name === 'node_modules' ? [] : list(f);
return /\.mjs$/.test(e.name) && !/\.test\.mjs$/.test(e.name) ? [f] : [];
});
function buildGraph(files) {
const g = new Map();
for (const file of files) {
const text = fs.readFileSync(file, 'utf8'); const deps = new Map();
for (const [re, kind] of [[STATIC_RE, 'static'], [DYNAMIC_RE, 'dynamic']])
for (const m of text.matchAll(re)) {
const t = path.resolve(path.dirname(file), m[1]);
if (fs.existsSync(t) && deps.get(t) !== 'dynamic') deps.set(t, kind);
}
g.set(file, deps);
}
return g;
}
function findCycles(g) {
const out = [], state = new Map(), stack = [];
const visit = (n) => {
state.set(n, 'active'); stack.push(n);
for (const [d] of g.get(n) ?? []) {
if (state.get(d) === 'active') {
const nodes = stack.slice(stack.indexOf(d)).concat(d);
out.push({ nodes, dynamic: nodes.slice(0, -1).some((f, i) => g.get(f).get(nodes[i + 1]) === 'dynamic') });
} else if (!state.has(d)) visit(d);
}
stack.pop(); state.set(n, 'done');
};
for (const n of g.keys()) if (!state.has(n)) visit(n);
return out;
}
test('動的 import を含む循環がなく、静的循環は登録済みのみ', () => {
const rel = (f) => path.relative(dir, f).replaceAll('\\', '/');
const bad = findCycles(buildGraph(list(dir)))
.map((c) => ({ ...c, label: c.nodes.map(rel).join(' -> ') }))
.filter((c) => c.dynamic || !KNOWN_STATIC_CYCLES.has(c.label))
.map((c) => `${c.dynamic ? '[動的・exit 13 の恐れ] ' : '[未登録の静的循環] '}${c.label}`);
assert.deepEqual(bad, [], bad.join('\n'));
});
初回は既存の循環が見つかることが多い。動的を含むものは全部直し、静的なものは理由を書いて KNOWN_STATIC_CYCLES に登録する。
教訓
- 依存注入の単体テストは配線を検証しない。変更した CLI はマージ後に本番構成で1回実行し、終了コードを見る。
- 1件見つけたら同型を横断検索する(実例では同じ循環がもう1か所あり、夜間ジョブが黙って止まっていた)。
よくある質問
+「Node CLI が黙って exit 13 で止まる — ESM の動的 import 循環をテストで止める」とは何ですか?
トップレベル await の CLI と動的 import の循環で起きる exit 13 の原因・直し方と、循環を CI で検出する node:test 一式
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約6万トークンかかりますが、この巻物を使えば約3,000トークンで済みます。差し引き約5.7万トークン(API料金換算で約86円)・95%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
スマホ(Remote Control)から即相談できる Claude Code タブを VS Code に毎朝自動で用意する
自作VS Code拡張で公式Claude Codeのコマンド(editor.openLast/newConversation/renameSessionTab)を叩き、名前付きタブをN本自動補充。夜間はWM_CLOSE→再起動で毎朝揃える。--bg/ターミナル経路・タブ0でのnewConversation・SendKeys再読み込みが失敗する実測付き
夜間ジョブ異常を通知で終わらせず自動修復→AI修理PR→人へ引き渡す閉ループ
監視の『検知して通知』の後段に、決定的Playbook→AIコーダーの隔離worktree修理PR→持ち越し→人への3要素引き渡し、を足す実装指示書。argvで指示を渡すな等の実測の落とし穴つき
ドキュメント駆動開発プロセス CLAUDE.md — 作るものを固めてから書かせる
「AIが暴走して意図と違うものを作る」を根絶する開発プロセス指示書。UI仕様→機能設計→実装の順をAIに強制し、1ファイルごとに承認ゲートを挟む。受託開発・チーム開発向け。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア