uccser/cs-field-guide

View on GitHub
csfieldguide/gulpfile.mjs

Summary

Maintainability
Test Coverage
////////////////////////////////
// Setup
////////////////////////////////

// Gulp
import gulp from "gulp"
const { src, dest, parallel, series, watch, lastRun } = gulp

// Package
import { readFile } from "node:fs/promises";
const pjson = JSON.parse(await readFile('./package.json'))

// Plugins
import autoprefixer from 'autoprefixer'
import browserify from 'browserify'
import babelify from 'babelify'
import browserSync from 'browser-sync'
const { reload } = browserSync.create()
import buffer from 'vinyl-buffer'
import c from 'ansi-colors'
import concat from 'gulp-concat'
import cssnano from 'cssnano'
import dependents from 'gulp-dependents'
import errorHandler from 'gulp-error-handle'
import filter from 'gulp-filter'
import gulpif from 'gulp-if'
import { hideBin } from 'yargs/helpers'
import imagemin from 'gulp-imagemin'
import log from 'fancy-log'
import pixrem from 'pixrem'
import postcss from 'gulp-postcss'
import postcssFlexbugFixes from 'postcss-flexbugs-fixes'
import rename from 'gulp-rename'
import dartSass from 'sass';
import gulpSass from 'gulp-sass';
const sass = gulpSass(dartSass);
import sourcemaps from 'gulp-sourcemaps'
import tap from 'gulp-tap'
import terser from 'gulp-terser'
import yargs from 'yargs/yargs'

// Arguments
const argv = yargs(hideBin(process.argv)).argv
const PRODUCTION = !!argv.production;

// Relative paths function
function pathsConfig(appName) {
    const vendorsRoot = 'node_modules/'
    const staticSourceRoot = 'static/'
    const staticOutputRoot = 'build/'

    return {
        app: `./${pjson.name}`,
        // Source files
        bootstrap_source: `${vendorsRoot}bootstrap/scss`,
        images_source: `${staticSourceRoot}img`,
        svg_source: `${staticSourceRoot}svg`,
        interactives_source: `${staticSourceRoot}interactives`,
        files_source: `${staticSourceRoot}files`,
        // These directories are scoped higher to catch files in interactives directory
        css_source: `${staticSourceRoot}`,
        scss_source: `${staticSourceRoot}`,
        js_source: `${staticSourceRoot}`,
        // Vendor
        vendor_js_source: [
            `${vendorsRoot}jquery/dist/jquery.js`,
            `${vendorsRoot}popper.js/dist/umd/popper.js`,
            `${vendorsRoot}bootstrap/dist/js/bootstrap.js`,
            `${vendorsRoot}details-element-polyfill/dist/details-element-polyfill.js`,
            `${vendorsRoot}lity/dist/lity.js`,
            `${vendorsRoot}iframe-resizer/js/iframeResizer.js`,
            `${vendorsRoot}multiple-select/dist/multiple-select-es.js`,
        ],
        // Output files
        fonts_output: `${staticOutputRoot}fonts`,
        images_output: `${staticOutputRoot}img`,
        svg_output: `${staticOutputRoot}svg`,
        interactives_output: `${staticOutputRoot}interactives`,
        files_output: `${staticOutputRoot}files`,
        vendor_js_output: `${staticOutputRoot}js`,
        // These directories are scoped higher to output files in interactives directory
        css_output: `${staticOutputRoot}`,
        js_output: `${staticOutputRoot}`,
    }
}

var paths = pathsConfig()

function catchError(error) {
    log.error(
        c.bgRed('Error:'),
        c.red(error)
    );
    this.emit('end');
}

////////////////////////////////
// Config
////////////////////////////////

// CSS/SCSS
const processCss = [
    autoprefixer(),         // adds vendor prefixes
    pixrem(),               // add fallbacks for rem units
    postcssFlexbugFixes(),  // adds flexbox fixes
]
const minifyCss = [
    cssnano({ preset: 'default' })   // minify result
]

// JS

const js_files_skip_optimisation = [
  // Optimise all files
  '**',
  // But skip the following files
  '!static/interactives/huffman-tree/**/*.js',
  '!static/interactives/pixel-viewer/**/*.js',
  '!static/interactives/**/js/third-party/*.js',
];

////////////////////////////////
// Tasks
////////////////////////////////

// Styles autoprefixing and minification
function css() {
    return src([
            `${paths.css_source}/**/*.css`,
            `!${paths.css_source}/**/node_modules/**/*.css`,
        ])
        .pipe(errorHandler(catchError))
        .pipe(sourcemaps.init())
        .pipe(postcss(processCss))
        .pipe(sourcemaps.write())
        .pipe(gulpif(PRODUCTION, postcss(minifyCss))) // Minifies the result
        .pipe(dest(paths.css_output))
}

function scss() {
    return src([
            `${paths.scss_source}/**/*.scss`,
            `!${paths.scss_source}/**/node_modules/**/*.scss`,
        ], { since: lastRun(scss) })
        .pipe(errorHandler(catchError))
        .pipe(dependents())
        .pipe(sourcemaps.init())
        .pipe(sass({
            includePaths: [
                paths.bootstrap_source
            ],
            sourceComments: !PRODUCTION,
        }).on('error', error => { throw error }))
        .pipe(postcss(processCss))
        .pipe(sourcemaps.write())
        .pipe(gulpif(PRODUCTION, postcss(minifyCss))) // Minifies the result
        .pipe(rename(function (path) {
            path.dirname = path.dirname.replace("scss", "css");
        }))
        .pipe(dest(paths.css_output))
}

// Javascript
function js() {
    const js_filter = filter(js_files_skip_optimisation, { restore: true })
    return src([
            `${paths.js_source}/**/*.js`,
            `!${paths.js_source}/**/modules/**/*.js`,
            `!${paths.js_source}/**/node_modules/**/*.js`
        ], {since: lastRun(js)})
        .pipe(js_filter)
        .pipe(errorHandler(catchError))
        .pipe(sourcemaps.init())
        .pipe(tap(function (file) {
            file.contents = browserify(file.path, { debug: true })
                .transform(babelify, { 
                    // Some node modules are switching to ES modules, 
                    // browserify is not compatible with ES modules, 
                    // so transpile such node modules in the meantime.
                    // New modules can be written in ES2015+, making jQuery obsolete
                    // and supporting older browsers easier.
                    // Todo: replace browserify + gulp with
                    // a more actively supported (and ES + CJS module supporting) tool,
                    // (i.e. rollup, webpack, vite, etc.) to prevent transpiling dependencies.
                    presets: [
                        "@babel/preset-env", {"sourceType": "unambiguous"} 
                        // If no exports or imports, assume file is script.
                    ], 
                    global: true,
                    ignore: [/\/node_modules\/(?!three\/)/] // Only transpile three.js (to be safe).
                })                                          // Can add other node_modules if/when they break...
                .bundle()
                .on('error', catchError);
        }))
        .pipe(buffer())
        .pipe(gulpif(PRODUCTION, terser({ keep_fnames: true })))
        .pipe(sourcemaps.write())
        .pipe(js_filter.restore)
        .pipe(dest(paths.js_output))
}

// Vendor Javascript (always minified)
function vendorJs() {
    return src(paths.vendor_js_source)
        .pipe(errorHandler(catchError))
        .pipe(concat('vendors.js'))
        .pipe(terser())
        .pipe(dest(paths.vendor_js_output))
}

// Image compression
function img() {
    return src(`${paths.images_source}/**/*`)
        .pipe(gulpif(PRODUCTION, imagemin())) // Compresses PNG, JPEG, GIF and SVG images
        .pipe(dest(paths.images_output))
}

// SVGs
function svg() {
    return src(`${paths.svg_source}/**/*`)
        .pipe(dest(paths.svg_output))
}

// Interactive files (not SCSS or JS)
function interactives() {
    return src([
            `${paths.interactives_source}/**/*`,
            `!${paths.interactives_source}/**/node_modules/**/*`,
            `!${paths.interactives_source}/**/*.scss`,
            `!${paths.interactives_source}/**/*.js`
        ])
        .pipe(dest(paths.interactives_output))
}

// Downloadable files
function files() {
    return src(`${paths.files_source}/**/*`)
        .pipe(dest(paths.files_output))
}

// Watch
function watchPaths() {
    watch([`${paths.js_source}**/*.js`], js).on("change", reload)
    watch([`${paths.css_source}/*/*.css`], css).on("change", reload)
    watch([`${paths.scss_source}**/*.scss`], scss).on("change", reload)
    watch([`${paths.images_source}**/*`], img).on("change", reload)
}

// Generate all assets
export const generateAssets = series(
    parallel(
        css,
        scss,
        vendorJs,
        img,
        svg,
        interactives,
        files
    ),
    js
)
generateAssets.displayName = "generate-assets";

export const dev = parallel(
    // initBrowserSync,
    watchPaths
)

// TODO: Look at cleaning build folder

export default series(generateAssets, dev);