dev/watch.mjs (125 lines of code) (raw):
#!/usr/bin/env node
import path, { dirname } from 'path';
import cpy from 'cpy';
import chalk from 'chalk';
// ********************************** JAVASCRIPT **********************************
// webpack watch task always performs an intial bundle, and we don't want browsersync
// listening at the point. so we use this flag to know whether any change webpack reports
// is the initial bundle, or subsequent file changes
let INITIAL_BUNDLE = true;
// just a bit of visual feedback while webpack creates its initial bundles.
// fakes a listr step
import ora from 'ora';
const wpNotification = ora({
text: 'Create initial webpack bundles',
color: 'yellow',
});
wpNotification.start();
import webpack from 'webpack';
const watchArguments = [
{
ignored: /node_modules/,
},
(err, stats) => {
if (err) {
// log any unexpected error
console.log(chalk.red(err));
}
if (INITIAL_BUNDLE) {
INITIAL_BUNDLE = false;
wpNotification.succeed();
return undefined;
}
const info = stats.toJson();
// send editing errors to console and browser
if (stats.hasErrors()) {
console.log(chalk.red(info.errors));
return undefined;
}
if (stats.hasWarnings()) {
console.warn(chalk.yellow(info.warnings));
}
// announce the changes
console.log(
stats.toString({
all: false,
entrypoints: true, // show which entry points have been modified
colors: true,
}),
);
return undefined;
},
];
const mainWebpackBundler = webpack(
(await import('../webpack.config.dev.js')).default,
);
mainWebpackBundler.watch(...watchArguments);
// ********************************** Sass **********************************
import chokidar from 'chokidar';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const sassDir = path.resolve(__dirname, '../', 'static', 'src', 'stylesheets');
const targetDir = path.resolve(__dirname, '../', 'static', 'target');
const inlineStylesDir = path.resolve(
__dirname,
'../',
'common',
'conf',
'assets',
'inline-stylesheets',
);
import { parseDir } from 'sass-graph';
const sassGraph = parseDir(sassDir, {
loadPaths: [sassDir],
});
import { compileSass } from '../tools/compile-css.mjs';
// when we detect a change in a sass file, we look up the tree of imports
// and only compile what we need to. anything matching this regex, we can just ignore in dev.
const ignoredSassRegEx = /^(_|ie9|old-ie)/;
chokidar.watch(`${sassDir}/**/*.scss`).on('change', (changedFile) => {
// see what top-level files need to be recompiled
const filesToCompile = [];
const changedFileBasename = path.basename(changedFile);
sassGraph.visitAncestors(changedFile, (ancestorPath) => {
const ancestorFileName = path.basename(ancestorPath);
if (!ignoredSassRegEx.test(ancestorFileName)) {
filesToCompile.push(ancestorFileName);
}
});
if (!/^_/.test(changedFileBasename)) {
filesToCompile.push(changedFileBasename);
}
// now recompile all files that matter
Promise.all(
filesToCompile.map((fileName) => {
// email styles should not be remified
if (/head.email-(article|front).scss/.test(fileName)) {
return compileSass(fileName, { remify: false });
}
return compileSass(fileName);
}),
)
.then(() =>
// copy stylesheets that are to be inlined
Promise.all(
filesToCompile
.filter((file) => /head./.test(file))
.map((file) =>
cpy(
[`**/${file.replace('.scss', '.css')}`],
inlineStylesDir,
{
cwd: targetDir,
},
),
),
),
)
.then(() => {
console.log(chalk.green('✔️ Stylesheets compiled'));
})
.catch((e) => {
// send editing errors to console and browser
console.log(chalk.red(`\n${e.formatted}`));
});
});