Gulp 4 mit Browsersync Stream

Screenshot der Website von gulp.js

In der Version 4 von Gulp hat sich die Syntax geändert und noch ist manches meines Erachtens nicht ausreichend dokumentiert, so dass ich die Hilfe von Jan und Google brauchte, um ein funktionstüchtiges Script zu erstellen.

Im Folgenden findet ihr den Code, damit ihr euch nicht alles selbst zusammensuchen müsst - ich gehe davon aus, dass die Snippets meist selbst erklärend sind, falls ihr Fragen oder Anmerkungen habt, nutzt bitte den Kommentarbereich.

Als erstes brauchen wir diverse Node-Module, die in der package.json bereitgestellt werden. Wie man diese am besten erstellt, wird hier erklärt: https://docs.npmjs.com/creating-a-package-json-file

{
  "name": "gnuschichten-theme",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Holger Weischenberg",
  "license": "ISC",
  "dependencies": {
    "autoprefixer": "^9.6.1",
    "babel-core": "^6.26.3",
    "browser-sync": "^2.26.7",
    "del": "^5.1.0",
    "gulp-babel": "^8.0.0",
    "gulp-concat": "^2.6.1",
    "gulp-jshint": "^2.1.0",
    "gulp-notify": "^3.2.0",
    "gulp-plumber": "^1.2.1",
    "gulp-postcss": "^8.0.0",
    "gulp-rename": "^1.4.0",
    "gulp-sass-glob": "^1.1.0",
    "gulp-uglify": "^3.0.2",
    "node-pre-gyp": "^0.13.0",
    "node-sass": "^4.12.0",
    "stylelint-config-standard": "^18.3.0"
  },
  "devDependencies": {
    "@babel/core": "^7.5.5",
    "gulp": "^4.0.2",
    "gulp-sass": "^4.0.2",
    "gulp-sourcemaps": "^2.6.5",
    "gulp-stylelint": "^9.0.0",
    "jshint": "^2.10.2",
    "stylelint": "^10.1.0",
    "stylelint-scss": "^3.9.3"
  }
}

Anschließend kommt die wichtigste Datei, die gulpfile.js.

'use strict';

// Include gulp.
const gulp = require('gulp');
const config = require('./config.json');

// Include plugins.
const sass = require('gulp-sass');
const plumber = require('gulp-plumber');
const glob = require('gulp-sass-glob');
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const concat = require('gulp-concat');
const notify = require('gulp-notify');
const rename = require('gulp-rename');
const sourcemaps = require('gulp-sourcemaps');
const stylelint = require('gulp-stylelint');
const jshint = require('gulp-jshint');
const postcss = require('gulp-postcss');
const autoprefixer = require('autoprefixer');
const del = require('del');
const browserSync = require('browser-sync').create();

// Check if local config exists.
var fs = require('fs');
if (!fs.existsSync('./config-local.json')) {
  console.log('\x1b[33m', 'You need to rename default.config-local.json to' +
      ' config-local.json and update its content if necessary.', '\x1b[0m');
  process.exit();
}
// Include local config.
var configLocal = require('./config-local.json');

gulp.task('lint-scss', function fixCssTask() {
  return gulp.src(config.scss.src)

      .pipe(stylelint({
        reporters: [
          {formatter: 'string', console: true}
        ]
      }));
});

// Process CSS for production.
gulp.task('css', function () {
  var postcssPlugins = [
    autoprefixer('last 2 versions', '> 1%', 'ie 10')
  ];

  return gulp.src(config.css.src)
      .pipe(glob())
      .pipe(plumber({
        errorHandler: function (error) {
          notify.onError({
            title: "Gulp",
            subtitle: "Failure!",
            message: "Error: <%= error.message %>"
          })(error);
          this.emit('end');
        }
      }))
      .pipe(sass({
        outputStyle: 'compressed',
        errLogToConsole: true
      }))
      .pipe(postcss(postcssPlugins))
      .pipe(gulp.dest(config.css.dest))
});

// Process CSS for development.
gulp.task('css_dev', function () {
  var postcssPlugins = [
    autoprefixer('last 2 versions', '> 1%', 'ie 10')
  ];

  return gulp.src(config.css.src)
      .pipe(glob())
      .pipe(plumber({
        errorHandler: function (error) {
          notify.onError({
            title: "Gulp",
            subtitle: "Failure!",
            message: "Error: <%= error.message %>"
          })(error);
          this.emit('end');
        }
      }))
      .pipe(sourcemaps.init())
      .pipe(sass({
        outputStyle: 'nested',
        errLogToConsole: true
      }))
      .pipe(postcss(postcssPlugins))
      .pipe(sourcemaps.write('./'))
      .pipe(gulp.dest(config.css.dest))
      .pipe(browserSync.stream({match: '**/*.css'}));
});

// Concat all JS files into one file and minify it.
// @to-do enable uglify for minify before production and remove comment
gulp.task('scripts', function () {
  return gulp.src(config.js.src)
      .pipe(plumber({
        errorHandler: function (error) {
          notify.onError({
            title: 'Gulp scripts processing',
            subtitle: 'Failure!',
            message: 'Error: <%= error.message %>'
          })(error);
          this.emit('end');
        }
      }))
      .pipe(concat('./index.js'))
      .pipe(gulp.dest('./assets/scripts/'))
      .pipe(rename(config.js.file))
      .pipe(babel())
      // .pipe(uglify())
      .pipe(gulp.dest(config.js.dest));
});

// Concat all JS files into one file.
gulp.task('scripts_dev', function () {
  return gulp.src(config.js.src)
      .pipe(plumber({
        errorHandler: function (error) {
          notify.onError({
            title: 'Gulp scripts processing',
            subtitle: 'Failure!',
            message: 'Error: <%= error.message %>'
          })(error);
          this.emit('end');
        }
      }))
      .pipe(concat('./index.js'))
      .pipe(gulp.dest('./assets/scripts/'))
      .pipe(sourcemaps.init())
      .pipe(rename(config.js.file))
      .pipe(sourcemaps.write('./'))
      .pipe(gulp.dest(config.js.dest))
      .pipe(browserSync.reload({stream: true, match: '**/*.js'}))
      .pipe(notify({
        message: 'Rebuild all custom scripts and BrowserSync injects changes.',
        onLast: true
      }));
});

// Remove temporary JS storage.
gulp.task('removeTemporaryStorage', function () {
  return del('./assets/scripts/');
});

// Remove sourcemaps.
gulp.task('removeSourceMaps', function () {
  return del(['./assets/css/main.css.map', './assets/css/layout/homepage.css.map']);
});

// Watch task.
gulp.task('watch', function () {
  gulp.watch(config.css.src, {usePolling: true}, gulp.series('css_dev'));
  gulp.watch(config.js.src, {usePolling: true}, gulp.series('scripts_dev', 'removeTemporaryStorage'));
});

// JS Linting.
gulp.task('js-lint', function () {
  return gulp.src(config.js.src)
      .pipe(jshint())
      .pipe(jshint.reporter('default'));
});

// BrowserSync settings.
gulp.task('browserSync', function () {
  browserSync.init({
    proxy: configLocal.browserSyncProxy,
    port: configLocal.browserSyncPort,
    open: false,
    injectChanges: true,
  });
});

// Compile for production.
gulp.task('serve', gulp.parallel('css', 'lint-scss', gulp.series('scripts', 'removeTemporaryStorage'), 'removeSourceMaps'));

// Compile for development + BrowserSync + Watch
gulp.task('serve_dev', gulp.series(gulp.parallel('css_dev', gulp.series('scripts_dev', 'removeTemporaryStorage')), gulp.parallel('watch', 'browserSync')));

// Default Task
gulp.task('default', gulp.series('serve'));

// Development Task
gulp.task('dev', gulp.series('serve_dev'));

In der config.json Datei werden Ziel und Quelle des zu kompilierenden Code bestimmt (das ist natürlich nur ein Beispiel für eine Verzeichnisstruktur).

{
  "name": "gnuschichten theme",
  "description": "gnuschichten theme.",
  "css": {
    "file" : "main.scss",
    "src": [
      "./src/scss/**/*.scss"
    ],
    "dest": "./assets/css"
  },
  "scss": {
    "file" : "main.scss",
    "src": [
      "./src/scss/**/*.scss"
    ],
    "dest": "./src/scss"
  },
  "js": {
    "file" : "main.js",
    "src": [
      "./src/js/**/*.js"
    ],
    "dest": "./assets/js"
  }
}

Und in der config-local.json werden Proxy und Port für den Browsersync Stream festgelegt.

{
  "name": "gnuschichten theme",
  "description": "gnuschichten theme local config file",
  "browserSyncProxy" : "http://gnuschichten.test",
  "browserSyncPort" : 3000
}

Bei einer Umgebung ohne DNS sollte für den Proxy ein Port mit übergeben werden (ich nutze meisten Valet, das dnsmasq unterstützt).

  "browserSyncProxy" : "http://localhost:8000",

Zuletzt noch die Stylelint Dateien, die ich allerdings noch Optimierungsbedarf hätten. Ich bin mir nicht sicher, ob alle Dateien so benötigt werden, hatte aber noch keine Zeit mich näher damit zu beschäftigen. Hier findet man jedenfalls die Dokumentation: https://stylelint.io/user-guide/configuration 

stylelint.js:

"use strict";

module.exports = {
    "extends": "stylelint-config-standard",
    "plugins": [
        "stylelint-no-browser-hacks/lib"
    ],
    "rules": {
        "block-closing-brace-newline-after": "always",
        "color-no-invalid-hex": true,
        "indentation": 2,
        "property-no-unknown": true,
        "plugin/no-browser-hacks": [true, {
            "browsers": [
                "last 2 versions",
                "ie >=8"
            ]
        }],
        "syntax": "scss",
        "max-empty-lines": 1,
        "value-keyword-case": "lower",
        "at-rule-empty-line-before": null,
        "rule-empty-line-before": null
    },
};

.stylelintrc:

module.exports = {
  plugins: ['stylelint-scss'],
  extends: ['stylelint-config-standard'],
  ignoreFiles: [
    '**/node_modules/**',
  ],
  rules: {
    "max-nesting-depth": 5,
    "at-rule-no-vendor-prefix": true,
    'at-rule-no-unknown': null,
    'scss/at-rule-no-unknown': true
  },
};

Und endlich .stylelintignore (das ist natürlich nur ein Beispiel-Verzeichnis):

/src/scss/vendors/*

Über Kommentare mit Verbesserungsvorschlägen, Hinweisen auf Bugs usw. würde ich mich freuen!