sourcemapping - 我为node社区贡献的第一个npm工具

为什么要写这个工具

发布环境的产品都经过压缩混淆,并且没有attach sourcemap。因此前端收集到的错误堆栈都是被混淆过的,文件与行号列号变量名都可能发生了变化。因此需要通过sourcemap文件映射到源码上去。如果使用typescript编译+压缩混淆生成的sourcemap文件,可以直接映射到typescript源码。因此针对前端window.onerror捕获到的错误堆栈写了这样一个工具用于解析与映射,已经在npm发布。
npm发布.png

使用方法

需要传入2个参数:

Usage: sourcemapping [options]

Options:
  -v, --version         output the version number
  -s, --stack <string>  stack string which can obtain from JSON.stringfy(Error.stack)
  -m, --map <string>    sourcemap dir path. Where to find sourcemap
  -h, --help            output usage information

For example:

sourcemapping -s "ReferenceError: exclued is not defined\n    at getParameterByName (http://localhost:7777/aabbcc/logline.min.js:1:9827)\n    at http://localhost:7777/aabbcc/index.js:15:11" -m "./test"

输出

默认输出到控制台

----Sourcemap Result----
Uncaught ReferenceError: exclued is not defined
    at Logline (../src/logline.js:62:31)
    at (index.js:15:11)
------------------------

☆Github源码☆

// sourcemapping.ts
#!/usr/bin/env node

import * as path from 'path';
import * as ErrorStackParser from 'error-stack-parser';
import * as commander from 'commander';
import { readFileSync, existsSync } from 'fs';
import { SourceMapConsumer } from 'source-map';

let raw_stack_string_array: string[];

function stackStringProcess(value: any, previous: any): string {
    // input '\n' will be translated into '\\n' and cause ErrorStackParser parsing failure
    raw_stack_string_array = value.split(String.raw`\n`);
    return raw_stack_string_array.join('\n');
}

function printToConsole(error_msg: string, stack_frames: ErrorStackParser.StackFrame[]): void {
    console.log("----Sourcemap Result----")
    console.log(error_msg);
    for (const frame of stack_frames) {
        let msg = "    at ";
        if (frame.functionName) msg += frame.functionName + " ";
        msg += "(";
        if (frame.fileName) msg += frame.fileName + ":";
        if (frame.lineNumber) msg += frame.lineNumber + ":";
        if (frame.columnNumber) msg += frame.columnNumber;
        msg += ")";
        console.log(msg);
    }
    console.log("------------------------")
}

async function loadAllConsumer(dir_path: string, stack_frame_array: ErrorStackParser.StackFrame[],
    sourcemap_map: Map<string, SourceMapConsumer>) {
    // load all sourcemap files into memory
    const sourcemap_list = new Set();
    const regExp = /.+\/(.+)$/;
    for (const frame of stack_frame_array) {
        if (frame.hasOwnProperty('fileName')) {
            const name = regExp.exec(frame.fileName)[1];
            frame.fileName = name;
            if (!sourcemap_list.has(name)) {
                sourcemap_list.add(name);
                let sourcemap_filepath = path.join(dir_path, name + '.map');
                if (existsSync(sourcemap_filepath)) {
                    let sourcemap: any;
                    try {
                        sourcemap = JSON.parse(readFileSync(sourcemap_filepath, 'utf-8'))
                    } catch (error) {
                        console.error('Read&Parse sourcemap:' + sourcemap_filepath + 'failed. ' + error.toString());
                        process.exit(0);
                    }
                    const consumer = await new SourceMapConsumer(sourcemap);
                    !sourcemap_map.has(name) && sourcemap_map.set(name, consumer);
                }
            }
        }
    }
}

const program = new commander.Command();
program.version('1.0.8', '-v, --version');
program.option('-s, --stack <string>', 'stack string which can obtain from JSON.stringfy(Error.stack)', stackStringProcess);
program.option('-m, --map <string>', 'sourcemap dir path. Where to find sourcemap');
program.parse(process.argv);

if (program.stack && program.map) {
    const msgExp = /^(.+)\n/;
    const msg = msgExp.exec(program.stack)[1];
    if (!msg) {
        console.error('Error message parsing failed, please check input stack which must contain error message. \ne.g. Uncaught ReferenceError: a is not defined\\n');
        process.exit(0);
    }
    let error_obj: Error = {
        'stack': program.stack,
        'message': msg,
        'name': msg.split(':')[0]
    }
    let stack_frame_array: ErrorStackParser.StackFrame[] = [];
    try {
        stack_frame_array = ErrorStackParser.parse(error_obj)
    } catch (error) {
        console.error('ErrorStackParser parsing failed' + error.toString());
        process.exit(0);
    }

    const sourcemap_map = new Map<string, SourceMapConsumer>();

    // load all sourcemap files
    loadAllConsumer(program.map, stack_frame_array, sourcemap_map).then(() => {
        // parse stack_frame_array
        stack_frame_array.forEach(stack_frame => {
            let name = stack_frame.fileName;
            if (sourcemap_map.has(name)) {
                let consumer = sourcemap_map.get(name);
                let origin = consumer.originalPositionFor({
                    line: stack_frame.lineNumber,
                    column: stack_frame.columnNumber
                });
                if (origin.line) stack_frame.lineNumber = origin.line;
                if (origin.column) stack_frame.columnNumber = origin.column;
                if (origin.source) stack_frame.fileName = origin.source;
                if (origin.name) stack_frame.functionName = origin.name;
            }
        });

        // print result
        printToConsole(program.msg, stack_frame_array);
    });

    // destroy all consumers
    for (let consumer of Array.from(sourcemap_map.values())) {
        consumer.destroy();
    }
} else {
    console.error("No error stack string OR error msg string OR sourcemap dir found. Please Check input.");
}