tohokuaikiのチラシの裏

技術的ネタとか。

Gulp4で画像のCacheBusterみたいな

今更だけど。

ファイル構造はこんな感じ。

gulpfile.js
gulp-functions.js
package.json
package-lock.json
htdocs/
├── css
│   └── images
├── images
└── js
src
├── javascripts
│   └── vendor
└── scss

src/scss => htdocs/css
src/javascripts => htdocs/js
へとコンパイルされる。

package.json

{
  "name": "bt-gulpfile",
  "version": "1.0.0",
  "description": "",
  "main": "gulpfile.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "private": true,
  "devDependencies": {
    "cheerio": "^1.0.0-rc.3",
    "compass-mixins": "^0.12.10",
    "event-stream": "^4.0.1",
    "gulp": "^4.0.2",
    "gulp-autoprefixer": "^6.1.0",
    "gulp-concat": "^2.6.1",
    "gulp-connect": "^5.7.0",
    "gulp-ejs": "^4.1.1",
    "gulp-htmlmin": "^5.0.1",
    "gulp-if": "^2.0.2",
    "gulp-plumber": "^1.2.1",
    "gulp-postcss": "^8.0.0",
    "gulp-sass": "^4.0.2",
    "gulp-sourcemaps": "^2.6.5",
    "gulp-uglify": "^3.0.2",
    "minimist": "^1.2.0",
    "postcss-cachebuster": "^0.1.6"
  },
  "dependencies": {},
  "browserslist": [
    "last 1 version",
    "ie 9",
    "Android 4.2"
  ]
}

gulpfile.js

var gulp        = require('gulp');
var gulpF       = require('./gulp-functions.js');
var connect     = require('gulp-connect');
var ejs         = require('gulp-ejs');
var uglify      = require("gulp-uglify");
var plumber     = require("gulp-plumber");
var concat      = require("gulp-concat");
var sourcemaps  = require("gulp-sourcemaps");
var autoprefixer= require('gulp-autoprefixer');
var sass        = require('gulp-sass');
var htmlmin     = require('gulp-htmlmin');
var minimist    = require("minimist");
var gulpif      = require('gulp-if');
var postcss     = require('gulp-postcss');
var cachebuster = require('postcss-cachebuster');
const fs        = require('fs');

var options = minimist(process.argv.slice(2), {
  string: 'env',
  default: { env: 'production' }
});

const config = {
    production: options.env !== 'dev',
    sourceMaps: options.env === 'dev'
};

var packageJson = JSON.parse(fs.readFileSync('./package.json'));

gulp.task('javascript', function(cb){
    gulp.src(['./src/javascripts/vendor/*.js', './src/javascripts/*.js'])
      .pipe(plumber())
        .pipe(gulpif(config.sourceMaps, sourcemaps.init()))
          .pipe(concat('dest.js'))
            .pipe(uglify())
              .pipe(gulpif(config.sourceMaps, sourcemaps.write()))
                .pipe(gulp.dest("htdocs/js/"));
    cb();
});

gulp.task('sass', function (cb) {
    gulp.src(['./src/scss/*.scss', './src/scss/vendor/*.scss'])
      .pipe(plumber())
        .pipe(gulpif(config.sourceMaps, sourcemaps.init()))
          .pipe(concat('style.css'))
            .pipe(sass({
              outputStyle: 'compressed',
            }))
              .pipe(autoprefixer({
                cascade: false
              }))
                .pipe(postcss([
                    cachebuster({
                      type: 'checksum',
                      cssPath : '/htdocs/css',
                    }),
                    ]))
                  .pipe(gulpif(config.sourceMaps, sourcemaps.write()))
                    .pipe(gulp.dest('./htdocs/css/'));
    cb();
});

gulp.task('html', function (cb) {
    gulp.src(['./src/**/*.html', '!./src/**/_*.html'])
      .pipe(plumber())
        .pipe(ejs())
          .pipe(gulpF.htmlrev())
            .pipe(htmlmin({
              collapseWhitespace : false,
              removeComments : true
            }))
              .pipe(gulp.dest('./htdocs'));
    cb();
});

gulp.task('watch', function(cb){
    // auto compile
    gulp.watch(
        ['./src/**/*.html',
         './src/javascripts/**/*.js',
         './src/scss/**/*.scss',
         ],
        gulp.series('html', 'javascript', 'sass'));
    
    // browser live reload
    gulp.watch('htdocs').on('change', function(filepath){
        gulp.src(filepath, { read: false }).pipe(connect.reload());
    });
    cb();
});

gulp.task('connect', function(done) {
  connect.server({
    root: 'htdocs',
    livereload: true
  }, function () { this.server.on('close', done) });
});

// only-compile
gulp.task('compile', gulp.series(
    'html', 'javascript', 'sass'));

// dev
gulp.task('default', gulp.series(
    'html', 'javascript', 'sass',
    'watch', 'connect'));

gulp-functions.js

var eventstream = require('event-stream');
const cheerio   = require('cheerio');
const path      = require('path');
const fs        = require('fs');
const crypto    = require('crypto');

module.exports = {
    // htmlファイルのimg/script/linkのファイルにmd5値でリビジョンを付ける
    htmlrev : ()=>{
        return eventstream.map(function(file, cb) {
            var $ = cheerio.load(file.contents.toString(),
                                 { decodeEntities: false });
            $('link[rel="stylesheet"],script[src],img').each(function(i, ele) {
                let attrname = "";
                let hash;
                switch (ele.name.toLowerCase()){
                  case 'link':
                    attrname = 'href';
                    break;
                  case 'img':
                  case 'script':
                    attrname = 'src';
                    break;
                }
                if (attrname) {
                    let src = $(ele).attr(attrname);
                    if (!src.match(new RegExp('^https?:\/\/'))) {
                        var normPath = path.normalize(src);
                        if (normPath.indexOf(path.sep) === 0) {
                            dependencyPath = path.join(file.base, normPath);
                        }
                        else {
                            dependencyPath = path.resolve(path.dirname(file.path), normPath);
                        }
                        let hasQS = dependencyPath.lastIndexOf("?");
                        if (hasQS >= 0){
                            dependencyPath = dependencyPath.substring(0, hasQS);
                        }
                        // replace src => htdocs
                        dependencyPath = dependencyPath.replace(
                            path.sep+'src'+path.sep,
                            path.sep+'htdocs'+path.sep);
                        try {
                            data = fs.readFileSync(dependencyPath);
                            hash = crypto.createHash('md5');
                            hash.update(data.toString(), 'utf8');
                            $(ele).attr(attrname,
                                        $(ele).attr(attrname) + '?rev=' + hash.digest('hex'));
                        }
                        catch(e) {
                            // fail silently.
                            console.log('[ERROR] revision attach failed: ' + e.message);
                        }
                    }
                }
            });
            file.contents =
              new Buffer.from($.root().html());
            cb(null, file);
        });
    }
}

実行する

npxで実行

$ npx gulp watch

でファイル監視の開発モード

$ npx gulp compile

コンパイルしてくれる。