VSCodeでデバッグするようBrowserifyのソースマップを正しく出力するには

前回調べた結果、node.jsにおいては、launch.jsonのoutDirプロパティと、gulp-sourcemapsのsourceRootプロパティを適切に設定することで、.tsファイル上のブレークポイントが正しくVSCodeで働かせることができた。
今回は、TypeScriptから生成されたJavaScriptをBrowserifyする過程において、同じことをするためにどうすればいいか調べる。

やりたいこと

${workspaceRoot}/src/ts/tsconfig.json
                        Program.ts
                        Startup.ts
                        ...
                     js/Program.js       // *1
                        Program.js.map   // *3
                        Startup.js       // *1
                        Startup.js.map   // *3
                        ...
                 index.html
                 js/bundle.js              // *2
                    bundle.js.map          // *4

上記の構成とした場合に*1を使って*2を得たい。
また、*3を使って*4を得たい(*1に設定したブレークポイントが正しく働くソースマップを得たい)。

ふつうにやると

とりあえず過去の経緯で手元にあるgulpfile.js

var gulp = require('gulp');
var gulpSourceMaps = require('gulp-sourcemaps');
var browserify = require('browserify');
var vinylSourceStream = require('vinyl-source-stream');
var vinylBuffer = require('vinyl-buffer');
var fs = require('fs');
var path = require('path');



var TSCONFIG_PATH = './src/ts/tsconfig.json';
var TS_SRC_DIR_PATH = './src/ts';
var JS_SRC_DIR_PATH = './src/js';
var OUTPUT_FILE_PATH = './js/bundle.js';

/// gulp build
gulp.task('build', [ 'transpile' ], function() {

    // https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback
    fs.readdir(JS_SRC_DIR_PATH, function(err, files) {
        if(err) {
            throw err;
        }

        var filesToBeBundled = [];
        var regexJS = new RegExp('.+\.js$');
        var regexes = [];
        INPUT_EXCEPTION_PATTERNS.forEach(function(pattern) {
            regexes.push(new RegExp(pattern));
        });

        files.forEach(function(file) {
            var matched = false;
            if(regexJS.test(file)) {
                regexes.forEach(function() {
                    matched |= r.test(file);
                });
                if(!matched) {
                    filesToBeBundled.push(path.join(JS_SRC_DIR_PATH, file));
                }
            }
        });

        return browserify({
                entries: filesToBeBundled,
                debug: true
            })
            .bundle()
            .pipe(vinylSourceStream(OUTPUT_FILE_PATH))
            .pipe(vinylBuffer())
            .pipe(gulpSourceMaps.init({ loadMaps: true }))
            .pipe(gulpSourceMaps.write('.', {
                includeContent: false,
                sourceRoot: function(file) {
                    console.log(file.cwd)
                    return path.relative(OUTPUT_FILE_PATH, JS_SRC_DIR_PATH);
                }
            }))
            .pipe(gulp.dest('.'));
    });
});

生成されたbundle.js.map

ブレークポイントが正しく働かない。

{
    "version":3,
    "sources":[
        "js/node_modules/browser-pack/_prelude.js",
        "js/src/js/Program.js",
        "js/src/js/Startup.js"
    ],
    "names":[],
    "mappings":"AAAA;ACAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA",
    "file":"js/bundle.js",
    "sourceRoot":"..\\..\\src\\js"
}

手編集で以下のように書き換えると、ただしくブレークポイントが働く。

{
    "version":3,
    "sources":[
        "node_modules/browser-pack/_prelude.js",
        "src/ts/Program.ts",
        "src/ts/Startup.ts"
    ],
    "names":[],
    "mappings":"AAAA;ACAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA",
    "file":"js/bundle.js",
    "sourceRoot": "..\\"
}

修正箇所

  1. sourceRootを”..\\”(bundle.jsのある”${workspaceRoot}\\js”から”${workspaceRoot}”に変更)
  2. sourcesを${workspaceRoot}からみたパスに変更(といか、謎な”js/”がついてしまっている)
  3. sourcesを、.jsから.tsファイルに変更

さて、どう直せばいいか。

gulpfile.jsの修正

sourceRootの修正

TypeScriptの時は同階層のjsディレクトリからtsディレクトリを見る相対パスを指定していた。
Browserifyで単一JavaScriptファイル化をする場合には、最上階のnode_modules内のライブラリを参照することもあるので、やはりCWDは${workspaceRoot}を指すことが正しい。
なのでsourceRootコールバックで、出力先ディレクトリから${workspaceRoot}(=gulp実行時のCWD)を相対パスで見るよう修正。

var OUTPUT_FILE_PATH = './js/bundle.js';

// ...

            .pipe(gulpSourceMaps.write('.', {
                includeContent: false,
                sourceRoot: function(file) {
                    return path.relative(path.dirname(OUTPUT_FILE_PATH), '.');
                }
            }))

sourcesの修正


var OUTPUT_FILE_PATH = './js/bundle.js';

// ...

// @gulp.task('build')

        return browserify({
                entries: filesToBeBundled,
                debug: true
            })
            .bundle()
            .pipe(vinylSourceStream(OUTPUT_FILE_PATH))                        // *5
            .pipe(vinylBuffer())
            .pipe(gulpSourceMaps.init({ loadMaps: true }))
            .pipe(gulpSourceMaps.write('.', {
                includeContent: false,
                sourceRoot: function(file) {
                    return path.relative(path.dirname(OUTPUT_FILE_PATH), '.');
                }
            }))
            .pipe(gulp.dest('.'));                                            // *6

“js/”と指定しているところはないが、どうやらいろいろ試してみた結果、*5および*6が悪さしている様子。
(*5で、階層付きでファイル名を指定するとディレクトリとファイル名に切り分けられ、ディレクトリ部分がCWDとして内部で保持されてしまい、sourcesがCWD + fileという形になってしまっているのではなかろうか)

確実な情報ソースは確認していないが、*5を”path.basename(OUTPUT_FILE_PATH)”、*6をpath.dirname(OUTPUT_FILE_PATH)”としたら問題が修正された。


        return browserify({
                entries: filesToBeBundled,
                debug: true
            })
            .bundle()
            .pipe(vinylSourceStream(path.basename(OUTPUT_FILE_PATH)))
            .pipe(vinylBuffer())
            .pipe(gulpSourceMaps.init({ loadMaps: true }))
            .pipe(gulpSourceMaps.write('.', {
                includeContent: false,
                sourceRoot: function(file) {
                    return path.relative(path.dirname(OUTPUT_FILE_PATH), '.');
                }
            }))
            .pipe(gulp.dest(path.dirname(OUTPUT_FILE_PATH)));

sourcesを.jsファイルから.tsファイルに変更

gulp-sourcemapsのmapSources()には.js.mapを作るときにsourcesとして指定するファイルパスを処理するコールバックを指定することができる。
そこで以下の処理をすれば実現できそう。

  1. ファイルパスとして与えられる.jsファイルへのファイルパスから、その.jsファイルを生成したときに生成されたソースマップファイルを取得する
  2. 上記で得たソースマップファイルから、そのソースファイルである.tsファイルのパスを取得する
  3. 上記で得た.tsファイルのパスに置き換える
            .pipe(gulpSourceMaps.init({ loadMaps: true }))
            .pipe(gulpSourceMaps.mapSources(function(pathSource, file) {
                if((new RegExp('^(\./)*node_modules/')).test(pathSource)) {
                    return pathSource;
                }

                var pathMapSources = [];
                var map = JSON.parse(fs.readFileSync(path.join(file.cwd, (pathSource + '.map'))));
                map.sources.forEach(function(s) {
                    var pathRelative = path.join(path.dirname(pathSource), path.join(map.sourceRoot, s));
                    pathMapSources.push(pathRelative);
                });

                if(pathMapSources.length > 1) {
                    throw (new Error());
                }
                return pathMapSources[0];
            }))
            .pipe(gulpSourceMaps.write('.', { ... }))

mapSourcesはstringしか返せないので、葉のソースマップ内sourcesに複数エントリが存在した場合には適切に処理できなくなる。
そのためあまりいい案ではないが、エラーをスローし処理を中止している。

出来上がったgulpfile.js

上記をまとめたものがこちら。

var gulp = require('gulp');
var gulpTypeScript = require('gulp-typescript');
var gulpSourceMaps = require('gulp-sourcemaps');
var gulpConcat = require('gulp-concat');
var gulpUglify = require('gulp-uglify');
var browserify = require('browserify');
var vinylSourceStream = require('vinyl-source-stream');
var vinylBuffer = require('vinyl-buffer');
var fs = require('fs');
var path = require('path');


/* FIX VALUES FOR YOUR ENVIRONMENT */
var TSCONFIG_PATH = './src/ts/tsconfig.json';
var TS_SRC_DIR_PATH = './src/ts';
var JS_SRC_DIR_PATH = './src/js';
var OUTPUT_FILE_PATH = './js/bundle.js';

var INPUT_EXCEPTION_PATTERNS = [];



/// gulp transpile
gulp.task('transpile', function() {

    var project = gulpTypeScript.createProject(TSCONFIG_PATH);

    return project
        .src()
        .pipe(gulpSourceMaps.init())
        .pipe(project())
        .js
        .pipe(gulpSourceMaps.write('.', {
            includeContent: true,
            sourceRoot: function(file) {
                return path.relative(JS_SRC_DIR_PATH, TS_SRC_DIR_PATH); // Now
            }
        }))
        .pipe(gulp.dest(JS_SRC_DIR_PATH));
});



/// gulp browserify
gulp.task('browserify', [ 'transpile' ], function() {
    fs.readdir(JS_SRC_DIR_PATH, function(err, files) {
        if(err) {
            throw err;
        }

        var filesToBeBundled = [];
        var regexJS = new RegExp('.+\.js$');
        var regexes = [];
        INPUT_EXCEPTION_PATTERNS.forEach(function(pattern) {
            regexes.push(new RegExp(pattern));
        });

        files.forEach(function(file) {
            var matched = false;
            if(regexJS.test(file)) {
                regexes.forEach(function(r) {
                    matched |= r.test(file);
                });
                if(!matched) {
                    filesToBeBundled.push(path.join(JS_SRC_DIR_PATH, file));
                }
            }
        });

        return browserify({
                entries: filesToBeBundled,
                debug: true
            })
            .bundle()
            .pipe(vinylSourceStream(path.basename(OUTPUT_FILE_PATH)))
            .pipe(vinylBuffer())
            .pipe(gulpSourceMaps.init({ loadMaps: true }))
            .pipe(gulpSourceMaps.mapSources(function(pathSource, file) {
                if((new RegExp('^(\./)*node_modules/')).test(pathSource)) {
                    return pathSource;
                }

                var pathMapSources = [];
                var map = JSON.parse(fs.readFileSync(path.join(file.cwd, (pathSource + '.map'))));
                map.sources.forEach(function(s) {
                    var pathRelative = path.join(path.dirname(pathSource), path.join(map.sourceRoot, s));
                    pathMapSources.push(pathRelative);
                });

                if(pathMapSources.length > 1) {
                    throw (new Error());
                }
                return pathMapSources[0];
            }))
            .pipe(gulpSourceMaps.write('.', {
                includeContent: false,
                sourceRoot: function(file) {
                    return path.relative(path.dirname(OUTPUT_FILE_PATH), '.');
                }
            }))
            .pipe(gulp.dest(path.dirname(OUTPUT_FILE_PATH)));
    });
});



/// gulp build
gulp.task('build', [ 'browserify' ], function() { });

ただ、これでも1つ問題があり、./src/ts/以下のサブフォルダにある.tsファイルから生成した.jsファイルが、Browserifyの対象から漏れている。
fs.readdirでうまく拾い上げられていないようなので、次回はこの問題を直す。
それでも大部分の問題は解消できてよかった。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です