Untitled
unknown
javascript
2 years ago
9.9 kB
16
Indexable
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));
}
}
Editor is loading...