Refactor build tasks and config file

Changed:
- Renamed 'mconfig.json' to 'loconfig.json'.
- Renamed 'concat.js' to 'concats.js' to represent flexible functionality.
- loconfig.json: Base paths are nested under "paths".
- loconfig.json: Paths for tasks are nested under "tasks".
- Refactored each task to process corresponding entries under "tasks" in 'loconfig.json'.
- watch.js: Changed concats watch to use task's includes.

Added:
- tiny-glob v0.2.9
- Utility 'glob.js' to use dynamic imports to fetch an available glob function from node modules.
- Utility 'template.js' to provide a function to render template tags (`{% ... %}`) in tasks.
- concats.js: Support for concatenating groupes of files.
- scripts.js: Support for ESBuild's "outdir" option.
This commit is contained in:
Chauncey McAskill
2021-09-17 00:26:18 -04:00
parent 589ec99135
commit d4ded2a64e
14 changed files with 428 additions and 134 deletions

View File

@@ -1,9 +1,9 @@
import { concatVendors } from './tasks/concat.js';
import { concatFiles } from './tasks/concats.js';
import { compileScripts } from './tasks/scripts.js';
import { compileStyles } from './tasks/styles.js' ;
import { compileSVGs } from './tasks/svgs.js' ;
concatVendors();
concatFiles();
compileScripts();
compileStyles();
compileSVGs();

View File

@@ -1,49 +0,0 @@
import paths from '../mconfig.json';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import concat from 'concat';
import { readdir } from 'node:fs/promises';
/**
* Concatenates third-party JavaScript files.
*/
export async function concatVendors() {
const filename = 'vendors.js';
const outfile = paths.scripts.dest + filename;
const external = [
// Add files in node_modules example:
// 'node_modules/gsap/dist/gsap.min.js',
];
const timeLabel = `${filename} concatenated in`;
console.time(timeLabel);
try {
// Get all files in `scripts/vendors/`
let files = await readdir(paths.scripts.vendors.src);
if (files.length) {
// Exclude files that are not JavaScript
files = files.filter((file) => file.includes('.js'));
// Prepend absolute path
files = files.map((file) => paths.scripts.vendors.src + file);
}
if (external.length) {
files = files.concat(external);
}
await concat(files, outfile);
message(`${filename} concatenated`, 'success', timeLabel);
} catch (err) {
message(`Error concatenating ${filename}`, 'error');
message(err);
notification({
title: `${filename} concatenation failed 🚨`,
message: `${err.name}: ${err.message}`
});
}
};

45
build/tasks/concats.js Normal file
View File

@@ -0,0 +1,45 @@
import loconfig from '../../loconfig.json';
import glob from '../utils/glob.js';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import template from '../utils/template.js';
import concat from 'concat';
import { basename } from 'node:path';
/**
* Concatenates groups of files.
*/
export async function concatFiles() {
loconfig.tasks.concats.forEach(async ({
includes,
outfile
}) => {
const filename = basename(outfile || 'undefined');
const timeLabel = `${filename} concatenated in`;
console.time(timeLabel);
try {
includes = includes.map((path) => template(path));
outfile = template(outfile);
const files = await glob(includes);
await concat(files, outfile);
if (files.length) {
message(`${filename} concatenated`, 'success', timeLabel);
} else {
message(`${filename} is empty`, 'notice', timeLabel);
}
} catch (err) {
message(`Error concatenating ${filename}`, 'error');
message(err);
notification({
title: `${filename} concatenation failed 🚨`,
message: `${err.name}: ${err.message}`
});
}
});
};

View File

@@ -1,40 +1,58 @@
import paths from '../mconfig.json';
import loconfig from '../../loconfig.json';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import template from '../utils/template.js';
import esbuild from 'esbuild';
import { basename } from 'node:path';
/**
* Bundles and minifies main JavaScript files.
*/
export async function compileScripts() {
[
'app.js',
].forEach((filename) => {
const includes = [ paths.scripts.src + filename ];
const outfile = paths.scripts.dest + filename;
loconfig.tasks.scripts.forEach(async ({
includes,
outdir = '',
outfile = ''
}) => {
const filename = basename(outdir || outfile || 'undefined');
const timeLabel = `${filename} compiled in`;
console.time(timeLabel);
esbuild.build({
entryPoints: includes,
bundle: true,
minify: true,
sourcemap: true,
color: true,
logLevel: 'error',
target: [
'es2015',
],
outfile
}).catch((err) => {
try {
includes = includes.map((path) => template(path));
if (outdir) {
outdir = template(outdir);
} else if (outfile) {
outfile = template(outfile);
} else {
throw new TypeError(
'Expected \'outdir\' or \'outfile\''
);
}
await esbuild.build({
entryPoints: includes,
bundle: true,
minify: true,
sourcemap: true,
color: true,
logLevel: 'error',
target: [
'es2015',
],
outdir,
outfile
});
message(`${filename} compiled`, 'success', timeLabel);
} catch (err) {
// errors managments (already done in esbuild)
notification({
title: `${filename} compilation failed 🚨`,
message: `${err.errors[0].text} in ${err.errors[0].location.file} line ${err.errors[0].location.line}`
});
}).then(() => {
message(`${filename} compiled`, 'success', timeLabel);
});
}
});
};

View File

@@ -1,8 +1,10 @@
import paths from '../mconfig.json';
import loconfig from '../../loconfig.json';
import message from '../utils/message.js';
import postcss from '../utils/postcss.js';
import notification from '../utils/notification.js';
import postcss from '../utils/postcss.js';
import template from '../utils/template.js';
import { writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { promisify } from 'node:util';
import sass from 'node-sass';
@@ -12,17 +14,19 @@ const sassRender = promisify(sass.render);
* Compiles and minifies main Sass files to CSS.
*/
export async function compileStyles() {
[
'critical',
'main',
].forEach(async (name) => {
const infile = paths.styles.src + name + '.scss';
const outfile = paths.styles.dest + name + '.css';
loconfig.tasks.styles.forEach(async ({
infile,
outfile
}) => {
const name = basename((outfile || 'undefined'), '.scss');
const timeLabel = `${name}.css compiled in`;
console.time(timeLabel);
try {
infile = template(infile);
outfile = template(outfile);
let result = await sassRender({
file: infile,
omitSourceMapUrl: true,

View File

@@ -1,35 +1,37 @@
import paths from '../mconfig.json';
import loconfig from '../../loconfig.json';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import template from '../utils/template.js';
import { basename } from 'node:path';
import mixer from 'svg-mixer';
/**
* Generates and transforms SVG spritesheets.
*/
export async function compileSVGs() {
[
{
includes: [ paths.svgs.src + '*.svg' ],
filename: 'sprite.svg'
},
].forEach(({
loconfig.tasks.svgs.forEach(async ({
includes,
filename
outfile
}) => {
const outfile = paths.scripts.dest + filename;
const filename = basename(outfile || 'undefined');
const timeLabel = `${filename} compiled in`;
console.time(timeLabel);
mixer(includes, {
spriteConfig: {
usages: false
}
}).then((result) => {
result.write(outfile).then(() => {
message(`${filename} compiled`, 'success', timeLabel);
try {
includes = includes.map((path) => template(path));
outfile = template(outfile);
const result = await mixer(includes, {
spriteConfig: {
usages: false
}
});
}).catch((err) => {
await result.write(outfile);
message(`${filename} compiled`, 'success', timeLabel);
} catch (err) {
message(`Error compiling ${filename}`, 'error');
message(err);
@@ -37,6 +39,6 @@ export async function compileSVGs() {
title: `${filename} compilation failed 🚨`,
message: `${err.name}: ${err.message}`
});
});
}
});
};

75
build/utils/glob.js Normal file
View File

@@ -0,0 +1,75 @@
/**
* @file Retrieve the first available glob library.
*
* Note that options vary between libraries.
*
* Candidates:
* - {@link https://npmjs.com/package/tiny-glob tiny-glob}
* - {@link https://npmjs.com/package/globby}
* - {@link https://npmjs.com/package/fast-glob fast-glob}
* - {@link https://npmjs.com/package/glob glob}
*/
import { promisify } from 'node:util';
const modules = [
'tiny-glob',
'globby',
'fast-glob',
'glob',
];
var glob;
for (let name of modules) {
try {
glob = await import(name);
glob = glob.default;
/**
* Wrap the function to ensure
* a common pattern.
*/
switch (name) {
case 'tiny-glob':
glob = createArrayableGlob(glob, {
filesOnly: true
});
break;
case 'glob':
glob = promisify(glob);
break;
}
break; // loop
} catch (err) {
// swallow this error; skip to the next candidate.
}
}
if (!glob) {
throw new TypeError(
`No glob library was found, expected one of: ${modules.join(', ')}`
);
}
export default glob;
/**
* Creates a wrapper function for the glob function
* to provide support for arrays of patterns.
*
* @param {function} glob - The glob function.
* @param {object} options - The glob options.
* @return {Promise}
*/
function createArrayableGlob(glob, options) {
return (patterns, options) => {
const globs = patterns.map((pattern) => glob(pattern, options));
return Promise.all(globs).then((files) => {
return [].concat.apply([], files);
});
};
}

111
build/utils/template.js Normal file
View File

@@ -0,0 +1,111 @@
/**
* @file Provides simple template tags.
*/
import loconfig from '../../loconfig.json';
const templateData = flatten({
paths: loconfig.paths
});
/**
* Replaces all template tags from a map of keys and values.
*
* If replacement pairs contain a mix of substrings, regular expressions,
* and functions, regular expressions are executed last.
*
* @param {string} input - The string being searched and replaced on.
* @param {object} data - An object in the form `{ 'from': 'to', … }`.
* @return {string} Returns the translated string.
*/
export default function template(input, data) {
const tags = [];
if (data) {
data = flatten(data);
} else {
data = templateData;
}
for (let tag in data) {
tags.push(escapeRegExp(tag));
}
if (tags.length === 0) {
return input;
}
const search = new RegExp('\\{%\\s*(' + tags.join('|') + ')\\s*%\\}', 'g');
return input.replace(search, (match, key) => {
let value = data[key];
switch (typeof value) {
case 'function':
/**
* Retrieve the offset of the matched substring `args[0]`
* and the whole string being examined `args[1]`.
*/
let args = Array.prototype.slice.call(arguments, -2);
return value.call(data, match, args[0], args[1]);
case 'string':
case 'number':
return value;
}
return '';
});
};
/**
* Creates a new object with all nested object properties
* concatenated into it recursively.
*
* Nested keys are flattened into a property path:
*
* ```js
* {
* a: {
* b: {
* c: 1
* }
* },
* d: 1
* }
* ```
*
* ```js
* {
* "a.b.c": 1,
* "d": 1
* }
* ```
*
* @param {object} input - The object to flatten.
* @param {string} prefix - The parent key prefix.
* @param {object} target - The object that will receive the flattened properties.
* @return {object} Returns the `target` object.
*/
function flatten(input, prefix, target = {}) {
for (let key in input) {
let field = (prefix ? prefix + '.' + key : key);
if (typeof input[key] === 'object') {
flatten(input[key], field, target);
} else {
target[field] = input[key];
}
}
return target;
}
/**
* Quotes regular expression characters.
*
* @param {string} str - The input string.
* @return {string} Returns the quoted (escaped) string.
*/
function escapeRegExp(str) {
return str.replace(/[\[\]\{\}\(\)\-\*\+\?\.\,\\\^\$\|\#\s]/g, '\\$&');
}

View File

@@ -1,10 +1,13 @@
import paths from '../mconfig.json';
import { concatVendors } from './tasks/concat.js';
import loconfig from '../loconfig.json';
import { concatFiles } from './tasks/concats.js';
import { compileScripts } from './tasks/scripts.js';
import { compileStyles } from './tasks/styles.js' ;
import { compileSVGs } from './tasks/svgs.js';
import template from './utils/template.js';
import server from 'browser-sync';
const { paths, tasks } = loconfig;
const serverConfig = {
open: false,
notify: false
@@ -23,8 +26,9 @@ if (typeof paths.url === 'string' && paths.url.length > 0) {
// Start the Browsersync server
server.init(serverConfig);
// Build scripts, compile styles, concat vendors and generate the svgs sprite on first hit
concatVendors();
// Build scripts, compile styles, concat files,
// and generate spritesheets on first hit
concatFiles();
compileScripts();
compileStyles();
compileSVGs();
@@ -48,13 +52,14 @@ server.watch(
compileScripts();
});
// Watch scripts vendors
// Watch concats
server.watch(
[
paths.scripts.vendors.src + '*.js',
]
tasks.concats.reduce(
(patterns, { includes }) => patterns.concat(includes),
[]
).map((path) => template(path))
).on('change', () => {
concatVendors();
concatFiles();
});
// Watch styles