Untitled
javascript
a month ago
9.9 kB
1
Indexable
Never
import autoprefixer from 'autoprefixer'; import chokidar from 'chokidar'; import copy from 'rollup-plugin-copy'; import del from 'rollup-plugin-delete'; import eslint from '@rollup/plugin-eslint'; import fs from 'fs'; import glob from 'glob'; import { nodeResolve as resolve } from '@rollup/plugin-node-resolve'; import read from 'read-file'; import * as rollup from 'rollup'; import styles from 'rollup-plugin-styles'; import { terser } from 'rollup-plugin-terser'; /* * Config * files: will create a new chunk * modules: wil not create a new chunk and cause a full rebuild */ const config = { mode: process.argv.includes('--watch') ? 'development' : 'production', notify: true, dest: './theme/assets', assets: 'dev/assets/*', js: { files: [ './dev/js/*.js', './dev/js/ext/*.js', ], modules: [ './dev/js/modules/*.js', ], }, css: { files: [ './dev/scss/critical.scss', './dev/scss/component/**/*.scss', './dev/scss/section/**/*.scss', './dev/scss/template/**/*.scss', ], modules: [ './dev/scss/ext/**/*.scss', './dev/scss/mixins/**/*.scss', './dev/scss/partials/**/*.scss', ], }, critical: { input: 'critical', // without extension inputFull: './dev/scss/critical.scss', dest: './theme/snippets', rename: 'critical-css.liquid', }, }; /* * Define Rollup, Eslint, Terser & scss options */ const eslintOptions = { fix: true, cache: true, throwOnError: false, throwOnWarning: false, include: [ ...config.js.files, ...config.js.modules, ], exclude: [ './node_modules/**/*', ], }; const terserOptions = { ecma: '2016', mangle: false, compress: false, toplevel: false, }; const scssOptions = { mode: 'extract', plugins: [ [ autoprefixer, {remove: false} ], [ 'postcss-preset-env' ], ], minimize: true, url: false, include: config.css.files, exclude: [], use: ['sass'], }; const inputOptions = { input: globify(config.js.files), cache: true, treeshake: false, watch: false, plugins: [ resolve(), ], }; const outputOptions = { dir: config.dest, format: 'es', sourcemap: true, assetFileNames: '[name][extname]', entryFileNames: '[name].js', chunkFileNames: '[name]-[hash].async.js', globals: { jquery: '$', }, plugins: [], }; const options = { ...inputOptions, output: outputOptions, }; /* * Notify * @param message {string}: your console message */ function notify(message) { if (!message || !config.notify) { return false; } console.log(`\x1b[36m ${message}`); } /* * Globify an array * @param array {array}: array with globs */ function globify(array = []) { if (!Array.isArray(array) || !array[0]) { return array; } return array.map((item) => { return glob.sync(item); }).flat(); } /* * Check if file is used in critical css * @param file {string}: path to file */ function usedInCriticalCss(file) { const criticalContent = read.sync(config.critical.inputFull, 'utf8'); file = file.split('/').pop().replace('.scss', '').replace('.css', ''); return !!criticalContent.includes(file); } /* * Rollup plugin * Notify file updates in the terminal * @param type {string}: file extension (.js/.css) */ function notifyUpdates(type) { return { name: 'notify-updates', writeBundle(options, bundle) { // Show updated file notifications for (let filename in bundle) { let file = `${config.dest}/${filename}`; if (!filename.includes('.old.js')) { notify(`Updated: ${file}`); } } // JS file will trigger a full reload if (type == 'JS') { notify('Trigger reload...'); } }, }; } /* * Bundle function. * @param type {string}: 'CSS' or 'JS' * @param inputOptions {object}: object with rollup input options * @param outOptions {object}: object with rollup output options */ let cleanedUp = false; async function bundle(type, inputOptions, outputOptions) { if (!inputOptions || !outputOptions) { return false; } // Clean up assets folder if command uses --all and not cleaned up yet. if (!cleanedUp && config.mode == 'production' && process.argv.includes('--all')) { cleanedUp = true; inputOptions.plugins = [ del({ targets: `${config.dest}/**/*`, }), copy({ targets: [ { src: config.assets, dest: config.dest }, ], }), ...inputOptions.plugins, ]; notify('Cleaned up assets'); notify('Copied assets'); } // Add touch file in asset folder to tricker shopify-cli if (config.mode == 'development') { outputOptions.plugins = [ ...outputOptions.plugins, notifyUpdates(type), ]; } // Notify when build starts notify(`Bundling ${type} assets...`); // Catch error when bundling fails try { // Create a bundle const bundle = await rollup.rollup(inputOptions); // Write the bundle to disk await bundle.write(outputOptions); // Close the bundle await bundle.close(); // Notify when build is done notify(`Built ${type} assets`); } catch (err) { // Log CSS/JS error notify(err); } } /* * Process and optimise Javascript. * @param input {string / array}: input files or array. */ async function processJs(input) { if (!input) { return false; } // Copy default options let _inputOptions = { ...inputOptions }; let _outputOptions = { ...outputOptions }; // Update options _inputOptions.input = input; _inputOptions.plugins = [ ..._inputOptions.plugins, eslint(eslintOptions), terser(terserOptions), ]; // Create the bundle await bundle('JS', _inputOptions, _outputOptions); } /* * Process and optimise CSS. * @param input {string / array}: input files or array. */ async function processCss(input) { if (!input) { return false; } // Check if critical css needs to be generated. let processCritical = false; if (typeof input == 'string' ) { processCritical = !!input.includes(config.critical.input); } else if(Array.isArray(input)) { processCritical = input.map((file) => !!file.includes(config.critical.input)).filter(Boolean)[0]; } // Copy default options let _inputOptions = { ...inputOptions }; let _outputOptions = { ...outputOptions }; // Update options _inputOptions.input = input; _inputOptions.plugins = [ ..._inputOptions.plugins, styles(scssOptions), // CSS bundles will also create a JS file. Clean this up. del({ hook: 'closeBundle', targets: `${config.dest}/*.old.js`, }), // Create critical inline css processCritical && copy({ hook: 'closeBundle', targets: [{ src: `${config.dest}/${config.critical.input}.css`, dest: config.critical.dest, rename: () => config.critical.rename, transform: (contents) => { return `{% style %}${contents.toString()}{% endstyle %}`; }, }], }), ]; // Update output plugins _outputOptions.sourcemap = false, _outputOptions.entryFileNames = '[name].old.js', _outputOptions.chunkFileNames ='[name]-[hash].old.js', // Create the bundle await bundle('CSS', _inputOptions, _outputOptions); } /* * Create watcher */ class Watcher { constructor() { this.javascript(); this.css(); this.assets(); this.freeze = false; this.timer = (ms = 1000) => { this.freeze = true; setTimeout(() => { this.freeze = false; }, ms); }; } /* * Watch javascript files */ javascript() { // Watch input files and process single input file again. chokidar.watch(config.js.files).on('change', (file) => { if (this.freeze) return false; this.timer(); notify(`Processing: ${file}...`); processJs(file); }); // Watch module files and process all input files again. chokidar.watch(config.js.modules).on('change', (file) => { if (this.freeze) return false; this.timer(); notify('Processing: all js...'); processJs(globify(config.js.files), 'fallback'); }); } /* * Watch CSS files */ css() { // Watch input files and process single input file again. chokidar.watch(config.css.files).on('change', (file) => { if (this.freeze) return false; this.timer(); notify(`Processing: ${file}...`); processCss(file); // If used in critical css, update critical css if(usedInCriticalCss(file)) { notify(`Processing: ${config.critical.inputFull}...`); processCss(config.critical.inputFull, '.css'); } }); // Watch input files and process single input file again. chokidar.watch(config.css.modules).on('change', (file) => { if (this.freeze) return false; this.timer(); notify('Processing: all css...'); processCss(globify(config.css.files), 'fallback'); }); } /* * Watch assets folder */ assets() { // Watch input files and process asset to theme asset folder chokidar.watch(config.assets).on('change', (file) => { if (this.freeze) return false; this.timer(); let destination = file.replace('dev/assets/', 'theme/assets/'); fs.copyFile( file, destination, null, (err) => { if (err) { console.error(err); return; } notify(`Updated asset: ${destination}...`); }); }); } } /* * Initialize */ if (config.mode == 'development') { // If development mode only watch files const watcher = new Watcher(); } else { // If production mode build assets switch(true) { case process.argv.includes('--js'): processJs(globify(config.js.files)); break; case process.argv.includes('--css'): processCss(globify(config.css.files)); break; default: processJs(globify(config.js.files)); processCss(globify(config.css.files)); } }