mirror of
https://github.com/locomotivemtl/locomotive-boilerplate.git
synced 2026-01-15 00:55:08 +08:00
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:
@@ -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();
|
||||
|
||||
@@ -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
45
build/tasks/concats.js
Normal 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}`
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
75
build/utils/glob.js
Normal 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
111
build/utils/template.js
Normal 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, '\\$&');
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user