Workflow ES6 au sein d’une application Angular existante ES5

ECMAScript 6 (alias spécification de la future version de Javascript) se rapproche de plus en plus d’une version finale. De nombreux outils permettent déjà d’utiliser la syntaxe et les fonctionnalités d’ES6 avec du code généré compatible avec les navigateurs récents (et même un peu plus vieux). Et donc afin de limiter au maximum la dette technique au sein de nos applications, nous pensons que le changement, c’est maintenant.

Chez PMSIpilot, nous avons essentiellement un certain nombre d’applications frontend AngularJS avec un workflow de développement basé sur Bower/Grunt et sur internet on trouve de nombreux boilerplates permettant de développer une application AngularJS (1.*, faudra encore attendre pour le 2 hein!!! hipsters) hors on ne trouve pas vraiment de guides fiables lorsqu’il s’agit d’intégrer du ES6 dans une application existante sans devoir repasser sur les milliers de lignes de codes existants.

Contraintes du Workflow cible

  • Ne pas réécrire l’application existante ES5
  • Workflow capable de gérer les modules ES6
  • Modules ES6 doivent être contaténés dans le même fichier JS pour éviter trop d’appels asynchrone (surtout au premier chargement)
  • Code généré compatible avec tous les navigateurs récents et IE à partir de la version 9

L’idée est donc de pouvoir distinguer les fichiers ES6 et les fichier ES5 (extension spécifique) et de générer deux fichiers : app-es5.js en concaténant tous les fichiers js ES5 et app-es6.js en transpilant (grâce à babel, ex 6to5 et browserify) le code ES6 à partir d’un point d’entrée unique. Nous avons donc abouti au gulpfile suivant (il est également possible d’utiliser Grunt avec un résultat iso-fonctionnel)

var gulp = require('gulp');
var browserify = require('browserify');
var babelify = require('babelify');
var del = require('del');
var source = require('vinyl-source-stream');
var _ = require('lodash');
var extReplace = require('gulp-ext-replace');
var less = require('gulp-less');
var gulpMerge = require('merge-stream');
var ngTemplates = require('gulp-ng-templates');
var concat = require('gulp-concat');
var wrap = require("gulp-wrap");
var ngAnnotate = require('gulp-ng-annotate');
var jshint = require('gulp-jshint');
var jscs = require('gulp-jscs');
var uglify = require('gulp-uglify');
var runSequence = require('run-sequence');
var rev = require("gulp-rev");
var replace = require("gulp-replace");
var karma = require('karma').server;
var minifyHTML = require('gulp-minify-html');

var output = './public';
var config = {
    outputDir: output,
    tmpDir: './tmp',

    es6Files: './src/js/**/*.es6',
    es6EntryFile: './tmp/es6/app.js',
    es6TmpDir: './tmp/es6',
    es6TmpOutputDir: './tmp/es6-output',
    es5EntryFiles: './src/js/**/*.js',
    outputJSES5File: 'app-es5.js',
    outputJSES6File: 'app-es6.js',
    outputJSDir: output + '/js',

    lessEntry: './src/less/main.less',
    outputCssDir: output + '/css',

    fontsEntry: [
        './components/font-awesome/fonts/*'
        // Other fonts
    ],
    outputFontsDir: output + '/font',

    imagesEntry: ['./src/images/**'],
    outputImagesDir: output + '/images',

    partialsConfig: {
        './src/partials/**': '/'
        // Other partials
    },
    partialsTmpDir: './tmp/partials',
    outputPartialsFile: 'templates.js',

    localesConfig: {
        './src/locales/**': '/'
        // Other locales
    },
    outputLocalesDir: output + '/locales',

    indexEntry: './src/index.html',
    outputIndex: output + '/index.html',

    vendorsJsEntry: [
        './components/angular/angular.min.js'
        // Other ES5 vendors
    ],
    outputJSVendorsFile: 'vendor.js',

    vendorsCssEntry: [
        './components/select2/select2.css'
        // Other CSS vendors
    ],
    outputVendorsCssFile: 'vendor.css',

    jscsConfig: './src/pmsipilot.jscs.json'
};

gulp.task('build', ['build/index', 'build/es5', 'build/es6', 'build/less', 'build/fonts', 'build/images', 'build/partials', 'build/locales', 'build/js/vendor', 'build/css/vendor']);

gulp.task('release', function() {
    runSequence('clean', 'build', 'release/js', 'release/index');
});

gulp.task('clean', function(done){
    del(config.outputDir, function() {
        del(config.tmpDir, done);
    });
});

// Build Javascripts
gulp.task('prepare/es6', function() {
    return gulp.src(config.es6Files)
        .pipe(extReplace('.js'))
        .pipe(gulp.dest(config.es6TmpDir));
});

gulp.task('build/es6', ['prepare/es6'], function() {
    return browserify(config.es6EntryFile,  { debug: true })
        .transform(babelify)
        .bundle()
        .on('error', function(err) { console.log('Error: ' + err.message); })
        .pipe(source(config.outputJSES6File))
        .pipe(ngAnnotate())
        .pipe(wrap('<%= contents %>\nloadES5Query();'))
        .pipe(gulp.dest(config.outputJSDir));
});

gulp.task('build/es5', function() {
    return gulp.src(config.es5EntryFiles)
        .pipe(jshint())
        .pipe(jshint.reporter('jshint-stylish'))
        .pipe(jshint.reporter('fail'))
        .pipe(jscs({ configPath: config.jscsConfig }))
        .pipe(concat(config.outputJSES5File))
        .pipe(ngAnnotate())
        .pipe(wrap('function loadES5Query(){\n<%= contents %>\n};'))
        .pipe(gulp.dest(config.outputJSDir));
});

// Build Less
gulp.task('build/less', function() {
    return gulp.src(config.lessEntry)
        .pipe(less())
        .pipe(gulp.dest(config.outputCssDir));
});

// Build Fonts
gulp.task('build/fonts', function() {
    return gulp.src(config.fontsEntry)
        .pipe(gulp.dest(config.outputFontsDir));
});

// Build Images
gulp.task('build/images', function() {
    return gulp.src(config.imagesEntry)
        .pipe(gulp.dest(config.outputImagesDir));
});

// Build Partials
gulp.task('prepare/partials', function() {
    var merge;
    Object.keys(config.partialsConfig).forEach(function(srcDir) {
        var destDir = config.partialsTmpDir + config.partialsConfig[srcDir];
        var stream = gulp.src(srcDir)
            .pipe(gulp.dest(destDir));
        merge = merge ? gulpMerge(merge, stream) : stream;
    });
    return merge;
});

gulp.task('build/partials', ['prepare/partials'], function() {
    return gulp.src(config.partialsTmpDir + '/**/*.html')
        .pipe(minifyHTML())
        .pipe(ngTemplates({
            module: 'pmsipilot-templates',
            filename: config.outputPartialsFile,
            path: function (path, base) {
                return 'partials/' + path.replace(base, '');
            }
        }))
        .pipe(gulp.dest(config.outputJSDir));
});

// Build Locales
gulp.task('build/locales', function() {
    var merge;
    Object.keys(config.localesConfig).forEach(function(srcDir) {
        var destDir = config.outputLocalesDir + config.localesConfig[srcDir];
        var stream = gulp.src(srcDir)
            .pipe(gulp.dest(destDir));
        merge = merge ? gulpMerge(merge, stream) : stream;
    });
    return merge;
});

// Build Index
gulp.task('build/index', function() {
    gulp.src(config.indexEntry)
        .pipe(gulp.dest(config.outputDir));
});

// Build Vendors
gulp.task('build/js/vendor', function() {
    gulp.src(config.vendorsJsEntry)
        .pipe(concat(config.outputJSVendorsFile))
        .pipe(gulp.dest(config.outputJSDir));
});

// Build Vendors CSS
gulp.task('build/css/vendor', function() {
    gulp.src(config.vendorsCssEntry)
        .pipe(concat(config.outputVendorsCssFile))
        .pipe(gulp.dest(config.outputCssDir));
});

// Release JS
gulp.task('release/js', function() {
    gulp.src([
        config.outputJSDir + '/' + config.outputJSES5File,
        config.outputJSDir + '/' + config.outputJSES6File,
        config.outputJSDir + '/' + config.outputPMSIJSVendorsFile,
        config.outputJSDir + '/' + config.outputPMSIGlobalJSVendorsFile
    ])
        .pipe(uglify())
        .pipe(gulp.dest(config.outputJSDir));
});

// Release Index "Rev"
gulp.task("release/index-rev", function() {
    return gulp.src([config.outputJSDir + '/**/*.js', config.outputCssDir + '/**/*.css'], { base: config.outputDir })
        .pipe(rev())
        .pipe(gulp.dest(config.outputDir))
        .pipe(rev.manifest())
        .pipe(gulp.dest(config.tmpDir));
});

gulp.task("release/index", ['release/index-rev'], function() {
    var manifest = require(config.tmpDir + "/rev-manifest.json");
    var stream = gulp.src(config.outputIndex);

    Object.keys(manifest).reduce(function(stream, key){
        del(key);
        return stream.pipe(replace(key.replace(__dirname + '/public/', ''), manifest[key]));
    }, stream).pipe(gulp.dest(config.outputDir));
});

// Watch
gulp.task('watch', function() {
    gulp.watch(config.es5EntryFiles, ['build/es5']);
    gulp.watch(config.es6Files, ['build/es6']);
    gulp.watch(config.lessEntry, ['build/less']);
    gulp.watch(config.fontsEntry, ['build/fonts']);
    gulp.watch(config.imagesEntry, ['build/images']);
    gulp.watch(Object.keys(config.partialsConfig), ['build/partials']);
    gulp.watch(Object.keys(config.localesConfig), ['build/locales']);
    gulp.watch('gulpfile.js', ['build']);
});

// Karma jenkins
gulp.task('jenkins', ['build'], function (done) {
    karma.start({
        configFile: __dirname + '/test/unit/karma.conf.js',
        reporters: ['progress', 'junit'],
        singleRun: true
    }, done);
});

// Karma test
gulp.task('test', ['build'], function (done) {
    karma.start({
        configFile: __dirname + '/test/unit/karma.conf.js',
        singleRun: true
    }, done);
});

Un gist du gulpfile est également disponible.

Quelques resources pour aller plus loin

 

Riad

Lead Developper Frontend @PMSIpilot, Computer Science Engineer (INSA Lyon, USTO Oran), Amateur photographer share about various topics: Web, Technology, ...

 

Laisser un commentaire