Skip to content

Instantly share code, notes, and snippets.

@ozanmuyes
Last active October 17, 2019 00:17
Show Gist options
  • Save ozanmuyes/3ee9dcebf3843f6500da67f5002bb396 to your computer and use it in GitHub Desktop.
Save ozanmuyes/3ee9dcebf3843f6500da67f5002bb396 to your computer and use it in GitHub Desktop.
gulpfile for Gulp 4 to ease development of website with Twig, SCSS and Babel
const fs = require('fs');
const path = require('path');
const gulp = require('gulp');
const Mem = require('gulp-mem');
const data = require('gulp-data');
const JSON5 = require('json5');
const twig = require('gulp-twig');
const bs = require('browser-sync').create();
const sass = require('gulp-sass');
const sourcemaps = require('gulp-sourcemaps');
const postcss = require('gulp-postcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const babel = require('gulp-babel');
const concat = require('gulp-concat');
const rimraf = require('rimraf');
const buildOutput = './dist'; // Relative to project root
const paths = {
templates: {
input: './src/templates',
extension: 'twig',
output: buildOutput // Put compiled HTML files to server root (e.g. '/index.html')
},
data: {
input: './src/data',
extension: 'json5',
// no output as the data merged with a template
},
styles: {
input: './src/styles',
extension: 'scss',
output: `${buildOutput}/public/css`
},
scripts: {
input: './src/scripts',
extension: 'js',
output: `${buildOutput}/public/js`,
outputFilename: 'all' // Concatenated JS file name (e.g. '/dist/public/js/all.js')
},
};
// To simulate Apache/nginx URL-rewrite on development server
const routes = {
// NOTE No need to add for index page (i.e. `'/': '/index.html'`) - unless an 'index.html' is created (via templating engine)
'/about': '/about.html',
'/products/foo': '/product.html',
//
};
const routeKeys = Object.keys(routes);
sass.compiler = require('node-sass');
const mem = new Mem();
mem.serveBasePath = buildOutput;
const compileTemplates = () => gulp
.src(`${paths.templates.input}/*.${paths.templates.extension}`, {
since: gulp.lastRun(compileTemplates)
})
.pipe(data((file) => {
const dataFilepath = `${paths.data.input}/${path.basename(file.path)}.${paths.data.extension}`;
return (fs.existsSync(dataFilepath))
? JSON5.parse(fs.readFileSync(dataFilepath))
: {};
}))
.pipe(twig())
.pipe(mem.dest(paths.templates.output));
const compileStyles = () => gulp
.src(`${paths.styles.input}/**/*.${paths.styles.extension}`)
.pipe(sass())
.pipe(mem.dest(paths.styles.output))
.pipe(bs.stream()); // inject CSS w/o reloading the page
const cleanStyles = () => rimraf(`${paths.styles.output}/*`);
const compileScripts = () => gulp
.src(`${paths.scripts.input}/**/*.${paths.scripts.extension}`)
.pipe(babel({ presets: ['@babel/env'] }))
.pipe(concat(`${paths.scripts.outputFilename}.${paths.scripts.extension}`))
.pipe(mem.dest(paths.scripts.output));
const cleanScripts = () => rimraf(`${paths.scripts.output}/*`);
const reload = (done) => {
bs.reload();
done(); // Without this subsequent changes on watched files won't trigger reload
};
const watch = () => {
gulp.watch(`${paths.templates.input}/**/*.${paths.templates.extension}`, gulp.series(compileTemplates, reload));
gulp.watch(`${paths.data.input}/*.${paths.data.extension}`, gulp.series(compileTemplates, reload));
gulp.watch(`${paths.styles.input}/**/*.${paths.styles.extension}`, gulp.series(compileStyles)); // NOTE No `reload` here thanks to `bs.stream()`
gulp.watch(`${paths.scripts.input}/**/*.${paths.scripts.extension}`, gulp.series(compileScripts, reload));
};
const serve = gulp.series(gulp.parallel(compileTemplates, compileStyles, compileScripts), () => {
bs.init({
serveStatic: [
{
route: '/public', // URL prefix for HTML and CSS
dir: './public' // Directory on the filesystem that houses static content
}
],
server: {
baseDir: buildOutput,
middleware: [
(req, _, next) => {
if (routeKeys.includes(req.url)) { // TODO What about regex keys?
req.url = routes[req.url]; // rewrite URL
}
next();
},
mem.middleware,
],
}
});
});
const compileStylesForProduction = () => gulp
.src(`${paths.styles.input}/**/*.${paths.styles.extension}`)
.pipe(sourcemaps.init())
.pipe(sass()) // No need to set `outputStyle` thanks to cssnano
.pipe(postcss([
autoprefixer(),
cssnano(),
]))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.styles.output));
const compileScriptsForProduction = () => gulp
.src(`${paths.scripts.input}/**/*.${paths.scripts.extension}`)
.pipe(sourcemaps.init())
.pipe(babel({ presets: ['@babel/env'] }))
.pipe(concat(`${paths.scripts.outputFilename}.${paths.scripts.extension}`))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.scripts.output));
const clean = (done) => rimraf(path.join(__dirname, buildOutput), done);
const compile = gulp.series(gulp.parallel(compileStylesForProduction, compileScriptsForProduction));
// NOTE This is for (production) build. There is no equivalent for this task
// for development simply because BrowserSync development server was
// setup to proxy '/public' URLs to 'ROOT/public' directory.
const copyPublicAsIs = () => gulp
.src('./public/**/*')
.pipe(gulp.dest(`${buildOutput}/public`));
module.exports = {
default: gulp.parallel(serve, watch),
'compile:styles': gulp.series(cleanStyles, compileStylesForProduction),
'compile:scripts': gulp.series(cleanScripts, compileScriptsForProduction),
clean,
compile,
build: gulp.series(clean, copyPublicAsIs, compile),
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment