4

We have recently switched from using a PHP asset manager to Gulp. We use bower to pull in our frontend packages and their dependencies. Using simple Bower packages that only have JS files listed in 'main' is pretty straightforward and is easily done. Using 'main-bower-files' we grab the required js files and concatenate them into one script file which we send to our site/script folder. The fonts we can collect and move to a fonts/ folder in our site. Then we hit a wall... What to do with images and their paths in the corresponding css/scss files. To further complicate things we want images to keep some of their original folder layout so they don't get overridden. We want to grab the css and scss (we use libsass) files and merge them with our own styles into one .css file. But how can we make sure they still have working paths to the images that come with them.

The desired folder layout is as follows:

bower.json
gulpfile.js
package.json
bower_components/
site/
  - css/styles.css
  - fonts/
  - images/
    - bower-package-a/
      - arrow.png
      - gradient.png
      - themea-a/
        - arrow.png
      - theme-b/
        - arrow.png
    - bower-package-b/
      - arrow.png
      - gradient.png
  - script/

This is our Gulpfile so far:

// Include gulp
var gulp = require('gulp');

// Include Plugins
var bower           = require('gulp-bower');
var browserSync     = require('browser-sync').create();
var concat          = require('gulp-concat');
var del             = require('del');
var filter          = require('gulp-filter');
var gutil           = require('gulp-util');
var imagemin        = require('gulp-imagemin');
var jshint          = require('gulp-jshint');
var mainBowerFiles  = require('main-bower-files');
var merge           = require('merge-stream');
var newer           = require('gulp-newer');
var plumber         = require('gulp-plumber');
var pngquant        = require('imagemin-pngquant');
var rename          = require('gulp-rename');
var sass            = require('gulp-sass');
var sourcemaps      = require('gulp-sourcemaps');
var changed         = require('gulp-changed');
var parallel        = require("concurrent-transform");
var os              = require("os");
var imageResize     = require('gulp-image-resize');
var spritesmith     = require('gulp.spritesmith');
var uglify          = require('gulp-uglify');

// Paden
var bowerDest       = 'site/script/lib/bower';
var imgSrc          = 'src/images/**';
var imgDest         = 'site/images';
var spriteImgDest   = './src/images/sprite/';
var scriptSrc       = 'src/script/**/*.js';
var scriptDest      = 'site/script';
var stylesSrc       = 'src/styles/styles.scss';
var stylesDest      = 'site/css';

// Helpers
var onSassError = function(error) {
    gutil.log('Sass Error', gutil.colors.red('123'));
    gutil.beep();
    console.log(error);
    this.emit('end');
}

// Clean images Task
gulp.task('clean:images', function () {
    return del([imgDest]);
});

// Clean script Task
gulp.task('clean:script', function () {
    return del([scriptDest]);
});

// Clean image Task
gulp.task('clean:styles', function () {
    return del([stylesDest]);
});

// Lint Task
gulp.task('lint', function() {
    return gulp.src(scriptSrc)
        .pipe(jshint())
        .pipe(jshint.reporter('jshint-stylish'))
    ;
});

// Sass Task
gulp.task('sass', ['sprite'], function() {
     return gulp.src(stylesSrc)
        .pipe(plumber({
            errorHandler: onSassError
        }))
        .pipe(sourcemaps.init())
        .pipe(sass({
            includePaths: [
              'bower_components/compass-mixins/lib',
              'bower_components/foundation/scss'
            ],
            outputStyle: 'compressed'
        }))
        .pipe(sourcemaps.write('./map'))
        .pipe(gulp.dest(stylesDest))
        .pipe(browserSync.stream())
    ;
});

// Concatenate & Minify JS
gulp.task('script', function() {
    return gulp.src(scriptSrc)
        .pipe(concat('script.js'))
        .pipe(gulp.dest(scriptDest))
        .pipe(rename('script.min.js'))
        .pipe(uglify())
        .pipe(gulp.dest(scriptDest))
    ;
});

// Voeg de JS bestanden die gebruikt worden vanuit bower samen. Let op: modernizr volgt niet de standaard
// en wordt daarom niet meegenomen in mainBowerFiles(). Deze voegen we dus los toe.
gulp.task('bower:js', function() {
    var modernizr = gulp.src('bower_components/modernizr/modernizr.js')
        .pipe(rename('modernizr.min.js'))
        .pipe(uglify())
        .pipe(gulp.dest(scriptDest))
    ;

    var frontend = gulp.src(mainBowerFiles())
        .pipe(sourcemaps.init())
        .pipe(filter('*.js'))
        .pipe(concat('frontend.min.js'))
        .pipe(uglify())
        .pipe(sourcemaps.write('maps'))
        .pipe(gulp.dest(scriptDest))
    ;

    return merge(modernizr, frontend);
});

// Imagemin Task (compress images)
gulp.task('imagemin', function () {
    return gulp.src([imgSrc, '!src/images/sprite{,/**}'])
        .pipe(newer(imgDest))
        .pipe(imagemin({
            use: [pngquant()]
        }))
        .pipe(gulp.dest(imgDest))
        .pipe(browserSync.stream())
    ;
});

// Compile sass into CSS & auto-inject into browsers
gulp.task('browser-sync', function() {
    // Serve files from the root of this project
    browserSync.init({
        proxy: "localhost/insyde/website_v6_devtools/site"
    });

    // add browserSync.reload to the tasks array to make
    // all browsers reload after tasks are complete.
    gulp.watch("./src/script/**/*.js", ['scripts-watch']);
});

// generate the x1 images from the big ones
gulp.task('generate-small-sprite-images', function () {
    return gulp.src('./src/images/sprite/*-2x.png')
        .pipe(newer(rename(function(path) {
              path.basename = path.basename.slice(0, -3);  //remove @2x label
        })))
        .pipe(parallel(
            imageResize({
                width: '50%',
                height: '50%'
            }), os.cpus().length
        ))
        .pipe(rename(function(path) {
              path.basename = path.basename.slice(0, -3);  //remove @2x label
        }))
        .pipe(gulp.dest(spriteImgDest))
;});

gulp.task('sprite', ['generate-small-sprite-images'], function () {
    var spriteData = gulp.src('./src/images/sprite/**/*.png').pipe(spritesmith({
        imgName: 'sprite.png',
        retinaImgName: 'sprite-2x.png',
        cssName: 'sprite.scss',
        imgPath: '../images/sprite.png', 
        retinaImgPath : '../images/sprite-2x.png', 
        retinaSrcFilter: '**/*-2x.png'
    }));
    // Pipe image stream through image optimizer and onto disk
    var imgStream = spriteData.img
        .pipe(imagemin())
        .pipe(gulp.dest(imgDest));

    // Pipe CSS stream through CSS optimizer and onto disk
    var cssStream = spriteData.css
        .pipe(gulp.dest('./src/styles/'));

    // Return a merged stream to handle both `end` events
    return merge(imgStream, cssStream);
});

// Watch Files For Changes
gulp.task('watch', function() {
    gulp.watch('./src/script/**/*.js', ['lint', 'script']);
    gulp.watch('./src/styles/**/*.scss', ['sass']);
    gulp.watch('./src/images/**', ['imagemin']);
    gulp.watch('./templates/**/*.html').on('change', browserSync.reload);
});

// Default Tasks
gulp.task('default', ['lint', 'sass', 'bower:js', 'script', 'imagemin', 'watch']);
gulp.task('frontend', ['lint', 'sprite', 'sass', 'bower:js', 'script', 'imagemin', 'browser-sync', 'watch']);
gulp.task('clean', ['clean:images', 'clean:script', 'clean:styles']);

// create a task that ensures the `scripts` task is complete before
// reloading browsers
gulp.task('scripts-watch', ['script'], browserSync.reload);   

2 Answers2

5

Got it working!

I used this as an example: https://github.com/jonkemp/gulp-useref/issues/60#issuecomment-77535822

What these tasks do is:

bower:assets Copy all asset files (images and fonts) that are defined in the 'main' property of bower packages (which we find by using main-bower-files) to site/dist/ while keeping the original folder layout of the packages themself.

bower:styles Parse each stylesheet that comes from main-bower-files (excluding two packages: foundation and compass-mixins) and rework the urls that point to the images and fonts that we copied earlier. This differs from the example in a way that the files in my situation aren't first copied to a .tmp directory, but get dealed with and then written to the site/css folder directly. I concatenate and minify the css, while using sourcemaps to make debugging easier.

//copy bower assets that need copying
gulp.task('bower:assets', function() {
    return gulp.src(mainBowerFiles(), {
        base: './bower_components'
    })
    .pipe(filter([
        '**/*.{png,gif,svg,jpeg,jpg,woff,eot,ttf}',
        '!foundation/**/*',
        '!compass-mixins/**/*'
    ]))
    .pipe(gulp.dest('./site/dist'));
});

//generate bower stylesheets with correct asset paths
gulp.task('bower:styles', function() {
    return gulp.src(mainBowerFiles(), {
        base: './bower_components'
    })
    .pipe(filter([
        '**/*.{css,scss}',
        '!foundation/**/*',
        '!compass-mixins/**/*'
    ]))
    .pipe(foreach(function(stream, file) {
        var dirName = path.dirname(file.path);
        return stream
            .pipe(rework(reworkUrl(function(url) {
                var fullUrl = path.join(dirName, url);
                if (fs.existsSync(fullUrl)) {
                    bowerCopyFiles.push(fullUrl);
                    console.log(path.relative('css', fullUrl).replace(/bower_components/, 'dist'));
                    return path.relative('css', fullUrl).replace(/bower_components/, 'dist');
                }
                return url;
            })));
    }))
    .pipe(sourcemaps.init())
    .pipe(concat('bower.css'))
    .pipe(minifyCss())
    .pipe(sourcemaps.write('./map'))
    .pipe(gulp.dest(stylesDest));
});

This all results in the following directory sturcture:

  • bower_components
  • site
    • css
      • bower.css
    • dist
      • bower-component-a
        • images
          • arrow.png
          • gradient.png
      • bower-component-b
        • images
          • arrow.png
          • gradient.png
1

Another option is to use the gulp-bower-normalize package, which will take the piped output from main-bower-files and then split them up based on the package and file extension, e.g.

gulp.src(mainBowerFiles(), { base: 'bower_components' })
    .pipe(bowerNormalize({ bowerJson: './bower.json'  }))
    .pipe(gulp.dest('assets/vendor'));

You may have to tweak the main files in bower.js as well, but it works quite nicely. If you were using Bootstrap...

bower.json:

{
    ...
    "dependencies": {
        "bootstrap": "^3.3.6",
        ...
    },
    "overrides": {
        "bootstrap": {
            "main": [
                "dist/css/bootstrap.min.css",
                "dist/js/bootstrap.min.js",
                "fonts/glyphicons-halflings-regular.eot",
                "fonts/glyphicons-halflings-regular.svg",
                "fonts/glyphicons-halflings-regular.ttf",
                "fonts/glyphicons-halflings-regular.woff",
                "fonts/glyphicons-halflings-regular.woff2"
            ],
            "normalize": {
                "fonts": ["*.eot", "*.svg", "*.ttf", "*.woff", "*.woff2"]
            }
        }
    } 
}

... it would generate the below structure:

assets/
|-- vendor/
    |-- bootstrap/
        |-- css/
        |   |-- bootstrap.min.css
        |
        |-- fonts/
        |   |-- glyphicons-halflings-regular.eot
        |   |-- glyphicons-halflings-regular.svg
        |   |-- glyphicons-halflings-regular.ttf
        |   |-- glyphicons-halflings-regular.woff
        |   |-- glyphicons-halflings-regular.woff2
        |
        |-- js/
            |-- bootstrap.min.js

Another nice option it has is to flatten the structure, which may be more useful when bundling up vendor files.

jamiebarrow
  • 2,416
  • 3
  • 27
  • 48