9

I have a "medium" Typescript application (as in, not trivial, but not enterprise-level either, many thousands of lines) with dependencies on jQuery, React and SocketIO - among other smaller libraries.

My current gulpfile is this:

var gulp = require("gulp"),
    $ = require("gulp-load-plugins")(),
    _ = require("lodash"),
    tsify = require("tsify"),
    browserify = require("browserify"),
    source = require("vinyl-source-stream"),
    debowerify = require("debowerify"),
    watchify = require("watchify"),
    lr = require("tiny-lr"),
    buffer = require("vinyl-buffer");

var lrServer = lr();

var config = {
    scripts: {
        base: __dirname + "/Resources/Scripts",
        main: "Application.ts",
        output: "App.js"
    },

    styles: {
        base: __dirname + "/Resources/Styles",
        sheets: ["Application.less", "Preload.less"],
        autoprefixer: ["last 2 version", "safari 5", "ie 8", "ie 9", "opera 12.1", "ios 6", "android 4"]
    },

    publicPath: __dirname + "/wwwroot"
};

function printError(err) {
    $.util.log($.util.colors.red.bold(err.type + " " + err.name + ":"), $.util.colors.white(err.message));
    this.emit("end");
}

function buildScripts(watch, debug) {
    var bundler = browserify({
            basedir: config.scripts.base,
            debug: false,
            entries: [config.scripts.base + "/" + config.scripts.main],
            cache: {},
            packageCache: {}
        })
        .plugin(tsify, {
            module: "commonjs",
            target: "es5",
            jsx: "react"
        })
        .transform(debowerify);

    function build() {
        return bundler.bundle()
            .on("error", printError)
            .pipe(source(config.scripts.output))
            .pipe($.if(!debug, buffer()))
            .pipe($.if(!debug, $.uglify()))
            .pipe(gulp.dest(config.publicPath + "/" + "scripts"));
    }

    if (!watch)
        return build();

    bundler
        .plugin(watchify)
        .on("update", function () {
            $.util.log($.util.colors.grey("Building scripts..."));
            build();
        })
        .on("time", function (timeMs) {
            $.util.log(
                $.util.colors.grey("Finished"),
                $.util.colors.cyan("'dev.scripts.watch' after"),
                $.util.colors.magenta(timeMs.toLocaleString() + " ms"));
        });

    return build();
}

gulp.task("prod.scripts", function() {
    return buildScripts(false, false);
});

gulp.task("dev.scripts", function () {
    return buildScripts(false, true);
});

gulp.task("dev.scripts.watch", function () {
    return buildScripts(true, true);
});

gulp.task("prod.styles", function () {
    return gulp
        .src(_.map(config.styles.sheets, function (sheet) { return config.styles.base + "/" + sheet; }))
        .pipe($.less())
        .on("error", printError)
        .pipe($.autoprefixer(config.styles.autoprefixer))
        .pipe($.uglifycss())
        .pipe(gulp.dest(config.publicPath + "/styles/"));
});

gulp.task("dev.styles", function () {
    return gulp
        .src(_.map(config.styles.sheets, function (sheet) { return config.styles.base + "/" + sheet; }))
        .pipe($.sourcemaps.init())
        .pipe($.less())
        .on("error", printError)
        .pipe($.autoprefixer(config.styles.autoprefixer))
        .pipe($.sourcemaps.write())
        .pipe(gulp.dest(config.publicPath + "/styles/"));
});

gulp.task("dev.styles.watch", ["dev.styles"], function () {
    return gulp.watch(config.styles.base + "/**/*.{css,less}", ["dev.styles"]);
});

gulp.task("dev.watch", ["dev.scripts.watch", "dev.styles.watch"], function () {
    lrServer.listen(35729);

    gulp.watch(config.publicPath + "/styles/**").on("change", function(file) {
        lrServer.changed({ body: { files: [file.path] } });
    });
});

gulp.task("dev", ["dev.styles", "dev.scripts"]);
gulp.task("prod", ["prod.styles", "prod.scripts"]);

Everything works as expected, however, the build times when using the watch task are taking many seconds. The odd thing is that my task reports that the re-compilation of the scripts happen in under 500ms (the event handler on the "time" event), yet if I count in my head it doesn't finish until three to four seconds after.

Note that before I pasted in my existing TypeScript code, I was loading/bundling jQuery, React, Moment and the other libraries that I was using very quickly. Because of this, I don't think that using a separate vendor bundle would speed anything up. Also, not writing out sourcemaps doesn't seem to impact the performance either.

Before I switched to browserify, I was using gulp-typescript for compilation and requirejs for module loading. Those builds took under a second. However, requirejs was causing issues for other reasons - and either way, I want to move away from AMD to CommonJS.

For now it's not a huge concern, but as the project grows it could certainly cause issues with my development flow. With a project only this large, how much longer will it take to process anything larger?

Furthermore, it's also causing issues with Visual Studio. This is an ASP.NET 5 application, and Visual Studio apparently insists on re-loading/re-parsing the bundled JavaScript file every time it changes, causing a lag in the IDE for 1-2 seconds after every change: on top of the 3-4 seconds it takes for recompilation itself. The script is being rendered to my wwwroot folder, and there seems to be no way to "exclude" the scripts sub-folder with the ASP.NET 5 tooling.

I know that I'm missing something somewhere. A possible issue is that tsify isn't using typescript's "project" feature to implement reloading, causing the TypeScript compiler to re-process each file for every change.

Anyway, I can't be the only person who has used these tools beyond toy projects, so I'm asking here if anyone has a better solution; since besides this issue, everything is working very well.

EDIT --------------------------------

OK, gonna have to eat my own words. Builds are down to roughly a second now that I'm bundling my third party libraries into their own bundle. Here's my updated gulpfile (notice the new dev.scripts.vendor task and the .external call in the buildScripts function)

var gulp = require("gulp"),
    $ = require("gulp-load-plugins")(),
    _ = require("lodash"),
    tsify = require("tsify"),
    browserify = require("browserify"),
    source = require("vinyl-source-stream"),
    debowerify = require("debowerify"),
    watchify = require("watchify"),
    lr = require("tiny-lr"),
    buffer = require("vinyl-buffer");

var lrServer = lr();

var config = {
    scripts: {
        base: __dirname + "/Resources/Scripts",
        main: "Application.ts",
        output: "App.js",
        vendor: ["react", "jquery", "moment", "socket.io-client", "lodash", "react-dom"]
    },

    styles: {
        base: __dirname + "/Resources/Styles",
        sheets: ["Application.less", "Preload.less"],
        autoprefixer: ["last 2 version", "safari 5", "ie 8", "ie 9", "opera 12.1", "ios 6", "android 4"]
    },

    publicPath: __dirname + "/wwwroot"
};

function printError(err) {
    $.util.log($.util.colors.red.bold(err.type + " " + err.name + ":"), $.util.colors.white(err.message));
    this.emit("end");
}

function buildScripts(watch, debug) {
    var bundler = browserify({
            basedir: config.scripts.base,
            debug: false,
            entries: [config.scripts.base + "/" + config.scripts.main],
            cache: {},
            packageCache: {}
        })
        .plugin(tsify, {
            module: "commonjs",
            target: "es5",
            jsx: "react"
        });

    if (debug)
        bundler.external(config.scripts.vendor);

    function build() {
        return bundler.bundle()
            .on("error", printError)
            .pipe(source(config.scripts.output))
            .pipe($.if(!debug, buffer()))
            .pipe($.if(!debug, $.uglify()))
            .pipe(gulp.dest(config.publicPath + "/" + "scripts"));
    }

    if (!watch)
        return build();

    bundler
        .plugin(watchify)
        .on("update", function () {
            $.util.log($.util.colors.grey("Building scripts..."));
            build();
        })
        .on("time", function (timeMs) {
            $.util.log(
                $.util.colors.grey("Finished"),
                $.util.colors.cyan("'dev.scripts.watch' after"),
                $.util.colors.magenta(timeMs.toLocaleString() + " ms"));
        });

    return build();
}

gulp.task("prod.scripts", function() {
    return buildScripts(false, false);
});

gulp.task("dev.scripts", ["dev.scripts.vendor"], function () {
    return buildScripts(false, true);
});

gulp.task("dev.scripts.vendor", function() {
    return browserify({
            debug: true,
            cache: {},
            packageCache: {},
            require: config.scripts.vendor
        })
        .bundle()
        .on("error", printError)
        .pipe(source("Vendor.js"))
        .pipe(gulp.dest(config.publicPath + "/" + "scripts"));
});

gulp.task("dev.scripts.watch", ["dev.scripts.vendor"], function () {
    return buildScripts(true, true);
});

gulp.task("prod.styles", function () {
    return gulp
        .src(_.map(config.styles.sheets, function (sheet) { return config.styles.base + "/" + sheet; }))
        .pipe($.less())
        .on("error", printError)
        .pipe($.autoprefixer(config.styles.autoprefixer))
        .pipe($.uglifycss())
        .pipe(gulp.dest(config.publicPath + "/styles/"));
});

gulp.task("dev.styles", function () {
    return gulp
        .src(_.map(config.styles.sheets, function (sheet) { return config.styles.base + "/" + sheet; }))
        .pipe($.sourcemaps.init())
        .pipe($.less())
        .on("error", printError)
        .pipe($.autoprefixer(config.styles.autoprefixer))
        .pipe($.sourcemaps.write())
        .pipe(gulp.dest(config.publicPath + "/styles/"));
});

gulp.task("dev.styles.watch", ["dev.styles"], function () {
    return gulp.watch(config.styles.base + "/**/*.{css,less}", ["dev.styles"]);
});

gulp.task("dev.watch", ["dev.scripts.watch", "dev.styles.watch"], function () {
    lrServer.listen(35729);

    gulp.watch(config.publicPath + "/styles/**").on("change", function(file) {
        lrServer.changed({ body: { files: [file.path] } });
    });
});

gulp.task("dev", ["dev.styles", "dev.scripts"]);
gulp.task("prod", ["prod.styles", "prod.scripts"]);

However, I'm still getting an odd issue. With sourcemaps disabled (which seem to have an impact now as far as speed), my on("time", () => {}) callback is reporting 60-80ms for every file change, but it still hangs for about a second. A second is about all I'm willing to wait for this, so again I'm worried that as the project grows this wait could grow as well.

It'd be interesting to see what this additional second of time is being spent on, when the event is reporting something much smaller. Perhaps I'll start digging into the source a bit since it seems nobody has the answer right off the bat.

ANOTHER ISSUE this is just a sidenote, but debowerify no longer works with this. When using debowerify+bower, it will continue to render the required module in the final output, even if that module is listed in the "external" list. So currently with this setup, I can only use npm modules unless I'm OK with adding more compilation time to my app bundle.

Also, I learned that debowerify will override npm modules, and that it's based off of the directory listing of bower_components, NOT your bower config file. I had jQuery installed in npm, and only bootstrap in bower; but since bootstrap pulled down jQuery as a dependency, the bower jQuery module was being loaded preferentially over the NPM jQuery. Just a heads up for people.

  • 1
    Just a thought: This is essentially a profiling task. What about adding `var debug = require('gulp-debug');` and `.pipe(debug({title: 'your message:'}))` to find out which parts are slow? I'm surprised that sourcemaps are not slow, it slowed down a build process on my computer considerably. We use gulp-typescript and the initial compilation takes up to 10 seconds but then incremental compilation is only up to 1.5 seconds long which is quite comfortable. – MartyIX Nov 20 '15 at 11:30
  • In our big project we use ASP.NET 4 and Typescript is compiled by Visual Studio 2015. It Takes up to 1s to compile and it's totally fine. As I tried play with ASP.NET 5 it seams like it's still very buggy if using Typescript. – Andzej Maciusovic Nov 20 '15 at 14:35
  • @MartinVseticka: the bundler.bundle() is what starts the stream, so I can't put anything before it. However, it appears that the bundler.bundle() operation is what is causing the extra time spent. Also, gulp-typescript *is* fast, but since I'm using browserify for module loading, I can't use it. What do you guys use for module loading? –  Nov 20 '15 at 23:18
  • @AndzejMaciusovic: what do you use for module loading? Also, I don't want to rely on the editor at all, as I'd like this project to be cross platform. So using a watch task is basically my only way to go forward. –  Nov 20 '15 at 23:18
  • It looks like you're not the only one with this problem: https://github.com/TypeStrong/tsify/issues/54 – gustavohenke Mar 04 '16 at 05:15
  • How depressing it is to come back to this question (and the other issue too) a few months later and see that no fix is available yet :( – gustavohenke Aug 10 '16 at 12:50

1 Answers1

2

Forget this, just use latest TS + webpack :)

  • I agree, I've stopped using browserify entirely in favor of webpack. –  Oct 19 '16 at 02:12