#!/usr/bin/env node // 使用例: node map-stack.js // カレントディレクトリの error.txt を読み込み、各エラーフレームから // 対応するソースマップ(dist/assets/ 以下の *.js.map)を用いて元の位置情報を出力する import fs from "fs"; import path from "path"; import { SourceMapConsumer } from "source-map"; // 各スタックフレームにマッチする正規表現 const FRAME_REGEX = /^\s*at\s+(.*?)\s+\((.*):(\d+):(\d+)\)$/; // スタックトレースのパース関数 const parseStackTrace = (text) => { return text .split(/\r?\n/) .map((line) => { const match = line.match(FRAME_REGEX); if (match) { return { original: line, functionName: match[1], file: match[2], line: Number(match[3]), column: Number(match[4]) }; } else { return { original: line }; } }); }; // エラースタックをソースマップから逆引きする関数(位置情報のみを表示) const mapStackTrace = async () => { const errorTxtPath = path.resolve(process.cwd(), "error.txt"); const stackTraceText = fs.readFileSync(errorTxtPath, "utf8"); const frames = parseStackTrace(stackTraceText); const consumerMap = new Map(); const mappedFrames = await Promise.all(frames.map(async (frame) => { if (frame.file && frame.line && frame.column) { const relativeFile = frame.file.replace(/^\//, ""); // 例: "assets/main-Td8-sruo.js" const mapFilePath = path.resolve(process.cwd(), "dist", relativeFile + ".map"); let consumer = consumerMap.get(mapFilePath); if (!consumer) { const rawSourceMap = fs.readFileSync(mapFilePath, "utf8"); consumer = await new SourceMapConsumer(rawSourceMap); consumerMap.set(mapFilePath, consumer); } const pos = consumer.originalPositionFor({ line: frame.line, column: frame.column }); if (pos && pos.source && pos.line != null && pos.column != null) { return ` at ${frame.functionName} (${pos.source}:${pos.line}:${pos.column})`; } else { return frame.original; } } else { return frame.original; } })); consumerMap.forEach((consumer) => consumer.destroy()); return mappedFrames.join("\n"); }; mapStackTrace() .then((mapped) => { console.log("--- Mapped Stack Trace ---"); console.log(mapped); }) .catch((err) => { console.error(err); process.exit(1); });