mirror of
https://github.com/locomotivemtl/locomotive-boilerplate.git
synced 2026-01-15 00:55:08 +08:00
Merge pull request #132 from locomotivemtl/feature/build-task-chores
Various fixes and changes to build utilities
This commit is contained in:
25
build/helpers/config.js
Normal file
25
build/helpers/config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @file Provides simple user configuration options.
|
||||
*/
|
||||
|
||||
import loconfig from '../../loconfig.json' assert { type: 'json' };
|
||||
import { merge } from '../utils/index.js';
|
||||
|
||||
let usrconfig;
|
||||
|
||||
try {
|
||||
usrconfig = await import('../../loconfig.local.json', {
|
||||
assert: { type: 'json' },
|
||||
});
|
||||
usrconfig = usrconfig.default;
|
||||
|
||||
merge(loconfig, usrconfig);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
export default loconfig;
|
||||
|
||||
export {
|
||||
loconfig,
|
||||
};
|
||||
162
build/helpers/glob.js
Normal file
162
build/helpers/glob.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* @file Retrieve the first available glob library.
|
||||
*
|
||||
* Note that options vary between libraries.
|
||||
*
|
||||
* Candidates:
|
||||
*
|
||||
* - {@link https://npmjs.com/package/tiny-glob tiny-glob} [1][5][6]
|
||||
* - {@link https://npmjs.com/package/globby globby} [2][5]
|
||||
* - {@link https://npmjs.com/package/fast-glob fast-glob} [3]
|
||||
* - {@link https://npmjs.com/package/glob glob} [1][4][5]
|
||||
*
|
||||
* Notes:
|
||||
*
|
||||
* - [1] The library's function accepts only a single pattern.
|
||||
* - [2] The library's function accepts only an array of patterns.
|
||||
* - [3] The library's function accepts either a single pattern
|
||||
* or an array of patterns.
|
||||
* - [4] The library's function does not return a Promise but will be
|
||||
* wrapped in a function that does return a Promise.
|
||||
* - [5] The library's function will be wrapped in a function that
|
||||
* supports a single pattern and an array of patterns.
|
||||
* - [6] The library's function returns files and directories but will be
|
||||
* preconfigured to return only files.
|
||||
*/
|
||||
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
/**
|
||||
* @callback GlobFn
|
||||
*
|
||||
* @param {string|string[]} patterns - A string pattern
|
||||
* or an array of string patterns.
|
||||
* @param {object} options
|
||||
*
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} GlobOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {GlobFn|undefined} The discovered glob function.
|
||||
*/
|
||||
let glob;
|
||||
|
||||
/**
|
||||
* @type {string[]} A list of packages to attempt import.
|
||||
*/
|
||||
const candidates = [
|
||||
'tiny-glob',
|
||||
'globby',
|
||||
'fast-glob',
|
||||
'glob',
|
||||
];
|
||||
|
||||
try {
|
||||
glob = await importGlob();
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean} Whether a glob function was discovered (TRUE) or not (FALSE).
|
||||
*/
|
||||
const supportsGlob = (typeof glob === 'function');
|
||||
|
||||
/**
|
||||
* Imports the first available glob function.
|
||||
*
|
||||
* @throws {TypeError} If no glob library was found.
|
||||
*
|
||||
* @returns {GlobFn}
|
||||
*/
|
||||
async function importGlob() {
|
||||
for (let name of candidates) {
|
||||
try {
|
||||
let globModule = await import(name);
|
||||
|
||||
if (typeof globModule.default !== 'function') {
|
||||
throw new TypeError(`Expected ${name} to be a function`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the function to ensure
|
||||
* a common pattern.
|
||||
*/
|
||||
switch (name) {
|
||||
case 'tiny-glob':
|
||||
/** [1][5] */
|
||||
return createArrayableGlob(
|
||||
/** [6] */
|
||||
createPresetGlob(globModule.default, {
|
||||
filesOnly: true
|
||||
})
|
||||
);
|
||||
|
||||
case 'globby':
|
||||
/** [2][5] - If `patterns` is a string, wraps into an array. */
|
||||
return (patterns, options) => globModule.default([].concat(patterns), options);
|
||||
|
||||
case 'glob':
|
||||
/** [1][5] */
|
||||
return createArrayableGlob(
|
||||
/** [4] */
|
||||
promisify(globModule.default)
|
||||
);
|
||||
|
||||
default:
|
||||
return globModule.default;
|
||||
}
|
||||
} catch (err) {
|
||||
// swallow this error; skip to the next candidate.
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
`No glob library was found, expected one of: ${candidates.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapper function for the glob function
|
||||
* to provide support for arrays of patterns.
|
||||
*
|
||||
* @param {function} globFn - The glob function.
|
||||
*
|
||||
* @returns {GlobFn}
|
||||
*/
|
||||
function createArrayableGlob(globFn) {
|
||||
return (patterns, options) => {
|
||||
/** [2] If `patterns` is a string, wraps into an array. */
|
||||
patterns = [].concat(patterns);
|
||||
|
||||
const globs = patterns.map((pattern) => globFn(pattern, options));
|
||||
|
||||
return Promise.all(globs).then((files) => {
|
||||
return [].concat.apply([], files);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapper function for the glob function
|
||||
* to define new default options.
|
||||
*
|
||||
* @param {function} globFn - The glob function.
|
||||
* @param {GlobOptions} presets - The glob function options to preset.
|
||||
*
|
||||
* @returns {GlobFn}
|
||||
*/
|
||||
function createPresetGlob(globFn, presets) {
|
||||
return (patterns, options) => globFn(patterns, Object.assign({}, presets, options));
|
||||
}
|
||||
|
||||
export default glob;
|
||||
|
||||
export {
|
||||
glob,
|
||||
supportsGlob,
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import kleur from 'kleur';
|
||||
* @param {string} [type] - The type of message.
|
||||
* @param {string} [timerID] - The console time label to output.
|
||||
*/
|
||||
export default function message(text, type, timerID) {
|
||||
function message(text, type, timerID) {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
console.log('✅ ', kleur.bgGreen().black(text));
|
||||
@@ -52,4 +52,10 @@ export default function message(text, type, timerID) {
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
export default message;
|
||||
|
||||
export {
|
||||
message,
|
||||
};
|
||||
@@ -16,7 +16,7 @@ import notifier from 'node-notifier';
|
||||
* @param {function} callback - The notification callback.
|
||||
* @return {void}
|
||||
*/
|
||||
export default function notification(options, callback) {
|
||||
function notification(options, callback) {
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
message: options
|
||||
@@ -42,4 +42,10 @@ export default function notification(options, callback) {
|
||||
}
|
||||
|
||||
notifier.notify(options, callback);
|
||||
}
|
||||
|
||||
export default notification;
|
||||
|
||||
export {
|
||||
notification,
|
||||
};
|
||||
139
build/helpers/postcss.js
Normal file
139
build/helpers/postcss.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @file If available, returns the PostCSS Processor creator and
|
||||
* any the Autoprefixer PostCSS plugin.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('autoprefixer').autoprefixer.Options} AutoprefixerOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('postcss').AcceptedPlugin} AcceptedPlugin
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('postcss').Postcss} Postcss
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('postcss').ProcessOptions} ProcessOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('postcss').Processor} Processor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {AcceptedPlugin[]} PluginList
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object<string, AcceptedPlugin>} PluginMap
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {PluginList|PluginMap} PluginCollection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} PostCSSOptions
|
||||
*
|
||||
* @property {ProcessOptions} processor - The `Processor#process()` options.
|
||||
* @property {AutoprefixerOptions} autoprefixer - The `autoprefixer()` options.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Postcss|undefined} postcss - The discovered PostCSS function.
|
||||
* @type {AcceptedPlugin|undefined} autoprefixer - The discovered Autoprefixer function.
|
||||
*/
|
||||
let postcss, autoprefixer;
|
||||
|
||||
try {
|
||||
postcss = await import('postcss');
|
||||
postcss = postcss.default;
|
||||
|
||||
autoprefixer = await import('autoprefixer');
|
||||
autoprefixer = autoprefixer.default;
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean} Whether PostCSS was discovered (TRUE) or not (FALSE).
|
||||
*/
|
||||
const supportsPostCSS = (typeof postcss === 'function');
|
||||
|
||||
/**
|
||||
* @type {PluginList} A list of supported plugins.
|
||||
*/
|
||||
const pluginsList = [
|
||||
autoprefixer,
|
||||
];
|
||||
|
||||
/**
|
||||
* @type {PluginMap} A map of supported plugins.
|
||||
*/
|
||||
const pluginsMap = {
|
||||
'autoprefixer': autoprefixer,
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to create a PostCSS Processor with the given plugins and options.
|
||||
*
|
||||
* @param {PluginCollection} pluginsListOrMap - A list or map of plugins.
|
||||
* If a map of plugins, the plugin name looks up `options`.
|
||||
* @param {PostCSSOptions} options - The PostCSS wrapper options.
|
||||
*
|
||||
* @returns {Processor|null}
|
||||
*/
|
||||
function createProcessor(pluginsListOrMap, options)
|
||||
{
|
||||
if (!postcss) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const plugins = parsePlugins(pluginsListOrMap, options);
|
||||
|
||||
return postcss(plugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the PostCSS plugins and options.
|
||||
*
|
||||
* @param {PluginCollection} pluginsListOrMap - A list or map of plugins.
|
||||
* If a map of plugins, the plugin name looks up `options`.
|
||||
* @param {PostCSSOptions} options - The PostCSS wrapper options.
|
||||
*
|
||||
* @returns {PluginList}
|
||||
*/
|
||||
function parsePlugins(pluginsListOrMap, options)
|
||||
{
|
||||
if (Array.isArray(pluginsListOrMap)) {
|
||||
return pluginsListOrMap;
|
||||
}
|
||||
|
||||
/** @type {PluginList} */
|
||||
const plugins = [];
|
||||
|
||||
for (let [ name, plugin ] of Object.entries(pluginsListOrMap)) {
|
||||
if (name in options) {
|
||||
plugin = plugin[name](options[name]);
|
||||
}
|
||||
|
||||
plugins.push(plugin);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
export default postcss;
|
||||
|
||||
export {
|
||||
autoprefixer,
|
||||
createProcessor,
|
||||
parsePlugins,
|
||||
pluginsList,
|
||||
pluginsMap,
|
||||
postcss,
|
||||
supportsPostCSS,
|
||||
};
|
||||
@@ -3,6 +3,10 @@
|
||||
*/
|
||||
|
||||
import loconfig from './config.js';
|
||||
import {
|
||||
escapeRegExp,
|
||||
flatten
|
||||
} from '../utils/index.js';
|
||||
|
||||
const templateData = flatten({
|
||||
paths: loconfig.paths
|
||||
@@ -22,7 +26,7 @@ const templateData = flatten({
|
||||
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
|
||||
* @return {*} Returns the transformed value.
|
||||
*/
|
||||
export default function resolve(input, data = templateData) {
|
||||
function resolve(input, data = templateData) {
|
||||
switch (typeof input) {
|
||||
case 'string': {
|
||||
return resolveValue(input, data);
|
||||
@@ -56,7 +60,7 @@ export default function resolve(input, data = templateData) {
|
||||
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
|
||||
* @return {string} Returns the translated string.
|
||||
*/
|
||||
export function resolveValue(input, data = templateData) {
|
||||
function resolveValue(input, data = templateData) {
|
||||
const tags = [];
|
||||
|
||||
if (data !== templateData) {
|
||||
@@ -93,55 +97,9 @@ export function resolveValue(input, data = templateData) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
export default resolve;
|
||||
|
||||
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, '\\$&');
|
||||
}
|
||||
export {
|
||||
resolve,
|
||||
resolveValue,
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import loconfig from '../utils/config.js';
|
||||
import glob from '../utils/glob.js';
|
||||
import message from '../utils/message.js';
|
||||
import notification from '../utils/notification.js';
|
||||
import resolve from '../utils/template.js';
|
||||
import loconfig from '../helpers/config.js';
|
||||
import glob, { supportsGlob } from '../helpers/glob.js';
|
||||
import message from '../helpers/message.js';
|
||||
import notification from '../helpers/notification.js';
|
||||
import resolve from '../helpers/template.js';
|
||||
import { merge } from '../utils/index.js';
|
||||
import concat from 'concat';
|
||||
import {
|
||||
basename,
|
||||
@@ -64,7 +65,7 @@ export const productionConcatFilesArgs = [
|
||||
* @return {Promise}
|
||||
*/
|
||||
export default async function concatFiles(globOptions = null, concatOptions = null) {
|
||||
if (glob) {
|
||||
if (supportsGlob) {
|
||||
if (globOptions == null) {
|
||||
globOptions = productionGlobOptions;
|
||||
} else if (
|
||||
@@ -72,7 +73,7 @@ export default async function concatFiles(globOptions = null, concatOptions = nu
|
||||
globOptions !== developmentGlobOptions &&
|
||||
globOptions !== productionGlobOptions
|
||||
) {
|
||||
globOptions = Object.assign({}, defaultGlobOptions, globOptions);
|
||||
globOptions = merge({}, defaultGlobOptions, globOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +83,18 @@ export default async function concatFiles(globOptions = null, concatOptions = nu
|
||||
concatOptions !== developmentConcatOptions &&
|
||||
concatOptions !== productionConcatOptions
|
||||
) {
|
||||
concatOptions = Object.assign({}, defaultConcatOptions, concatOptions);
|
||||
concatOptions = merge({}, defaultConcatOptions, concatOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* @param {object} entry - The entrypoint to process.
|
||||
* @param {string[]} entry.includes - One or more paths to process.
|
||||
* @param {string} entry.outfile - The file to write to.
|
||||
* @param {?string} [entry.label] - The task label.
|
||||
* Defaults to the outfile name.
|
||||
* @return {Promise}
|
||||
*/
|
||||
loconfig.tasks.concats.forEach(async ({
|
||||
includes,
|
||||
outfile,
|
||||
@@ -98,25 +108,25 @@ export default async function concatFiles(globOptions = null, concatOptions = nu
|
||||
console.time(timeLabel);
|
||||
|
||||
try {
|
||||
if (!Array.isArray(includes)) {
|
||||
includes = [ includes ];
|
||||
}
|
||||
|
||||
includes = resolve(includes);
|
||||
outfile = resolve(outfile);
|
||||
|
||||
let files;
|
||||
|
||||
if (glob && globOptions) {
|
||||
files = await glob(includes, globOptions);
|
||||
} else {
|
||||
files = includes;
|
||||
if (supportsGlob && globOptions) {
|
||||
includes = await glob(includes, globOptions);
|
||||
}
|
||||
|
||||
if (concatOptions.removeDuplicates) {
|
||||
files = files.map((path) => normalize(path));
|
||||
files = [ ...new Set(files) ];
|
||||
includes = includes.map((path) => normalize(path));
|
||||
includes = [ ...new Set(includes) ];
|
||||
}
|
||||
|
||||
await concat(files, outfile);
|
||||
await concat(includes, outfile);
|
||||
|
||||
if (files.length) {
|
||||
if (includes.length) {
|
||||
message(`${label} concatenated`, 'success', timeLabel);
|
||||
} else {
|
||||
message(`${label} is empty`, 'notice', timeLabel);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import loconfig from '../utils/config.js';
|
||||
import message from '../utils/message.js';
|
||||
import notification from '../utils/notification.js';
|
||||
import resolve from '../utils/template.js';
|
||||
import loconfig from '../helpers/config.js';
|
||||
import message from '../helpers/message.js';
|
||||
import notification from '../helpers/notification.js';
|
||||
import resolve from '../helpers/template.js';
|
||||
import { merge } from '../utils/index.js';
|
||||
import esbuild from 'esbuild';
|
||||
import { basename } from 'node:path';
|
||||
|
||||
@@ -50,9 +51,20 @@ export default async function compileScripts(esBuildOptions = null) {
|
||||
esBuildOptions !== developmentESBuildOptions &&
|
||||
esBuildOptions !== productionESBuildOptions
|
||||
) {
|
||||
esBuildOptions = Object.assign({}, defaultESBuildOptions, esBuildOptions);
|
||||
esBuildOptions = merge({}, defaultESBuildOptions, esBuildOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* @param {object} entry - The entrypoint to process.
|
||||
* @param {string[]} entry.includes - One or more paths to process.
|
||||
* @param {string} [entry.outdir] - The directory to write to.
|
||||
* @param {string} [entry.outfile] - The file to write to.
|
||||
* @param {?string} [entry.label] - The task label.
|
||||
* Defaults to the outdir or outfile name.
|
||||
* @throws {TypeError} If outdir and outfile are missing.
|
||||
* @return {Promise}
|
||||
*/
|
||||
loconfig.tasks.scripts.forEach(async ({
|
||||
includes,
|
||||
outdir = '',
|
||||
@@ -67,6 +79,10 @@ export default async function compileScripts(esBuildOptions = null) {
|
||||
console.time(timeLabel);
|
||||
|
||||
try {
|
||||
if (!Array.isArray(includes)) {
|
||||
includes = [ includes ];
|
||||
}
|
||||
|
||||
includes = resolve(includes);
|
||||
|
||||
if (outdir) {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import loconfig from '../utils/config.js';
|
||||
import message from '../utils/message.js';
|
||||
import notification from '../utils/notification.js';
|
||||
import postcss, { pluginsMap as postcssPluginsMap } from '../utils/postcss.js';
|
||||
import resolve from '../utils/template.js';
|
||||
import loconfig from '../helpers/config.js';
|
||||
import message from '../helpers/message.js';
|
||||
import notification from '../helpers/notification.js';
|
||||
import {
|
||||
createProcessor,
|
||||
pluginsMap as postcssPluginsMap,
|
||||
supportsPostCSS
|
||||
} from '../helpers/postcss.js';
|
||||
import resolve from '../helpers/template.js';
|
||||
import { merge } from '../utils/index.js';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { basename } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
@@ -82,10 +87,10 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
|
||||
sassOptions !== developmentSassOptions &&
|
||||
sassOptions !== productionSassOptions
|
||||
) {
|
||||
sassOptions = Object.assign({}, defaultSassOptions, sassOptions);
|
||||
sassOptions = merge({}, defaultSassOptions, sassOptions);
|
||||
}
|
||||
|
||||
if (postcss) {
|
||||
if (supportsPostCSS) {
|
||||
if (postcssOptions == null) {
|
||||
postcssOptions = productionPostCSSOptions;
|
||||
} else if (
|
||||
@@ -93,10 +98,19 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
|
||||
postcssOptions !== developmentPostCSSOptions &&
|
||||
postcssOptions !== productionPostCSSOptions
|
||||
) {
|
||||
postcssOptions = Object.assign({}, defaultPostCSSOptions, postcssOptions);
|
||||
postcssOptions = merge({}, defaultPostCSSOptions, postcssOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* @param {object} entry - The entrypoint to process.
|
||||
* @param {string[]} entry.infile - The file to process.
|
||||
* @param {string} entry.outfile - The file to write to.
|
||||
* @param {?string} [entry.label] - The task label.
|
||||
* Defaults to the outfile name.
|
||||
* @return {Promise}
|
||||
*/
|
||||
loconfig.tasks.styles.forEach(async ({
|
||||
infile,
|
||||
outfile,
|
||||
@@ -116,9 +130,9 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
|
||||
outFile: outfile,
|
||||
}));
|
||||
|
||||
if (postcss && postcssOptions) {
|
||||
if (supportsPostCSS && postcssOptions) {
|
||||
if (typeof postcssProcessor === 'undefined') {
|
||||
postcssProcessor = createPostCSSProcessor(
|
||||
postcssProcessor = createProcessor(
|
||||
postcssPluginsMap,
|
||||
postcssOptions
|
||||
);
|
||||
@@ -191,35 +205,6 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a PostCSS Processor with the given plugins and options.
|
||||
*
|
||||
* @param {array<(function|object)>|object<string, (function|object)>} pluginsListOrMap -
|
||||
* A list or map of plugins.
|
||||
* If a map of plugins, the plugin name looks up `options`.
|
||||
* @param {object} options - The PostCSS options.
|
||||
*/
|
||||
function createPostCSSProcessor(pluginsListOrMap, options)
|
||||
{
|
||||
let plugins;
|
||||
|
||||
if (Array.isArray(pluginsListOrMap)) {
|
||||
plugins = pluginsListOrMap;
|
||||
} else {
|
||||
plugins = [];
|
||||
|
||||
for (let [ name, plugin ] of Object.entries(pluginsListOrMap)) {
|
||||
if (name in options) {
|
||||
plugin = plugin[name](options[name]);
|
||||
}
|
||||
|
||||
plugins.push(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
return postcss(plugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge unused styles from CSS files.
|
||||
*
|
||||
@@ -232,22 +217,23 @@ function createPostCSSProcessor(pluginsListOrMap, options)
|
||||
*/
|
||||
async function purgeUnusedCSS(outfile, label) {
|
||||
label = label ?? basename(outfile);
|
||||
|
||||
const timeLabel = `${label} purged in`;
|
||||
console.time(timeLabel);
|
||||
|
||||
const purgeCSSContentFiles = Array.from(loconfig.tasks.purgeCSS.content);
|
||||
|
||||
const purgeCSSResults = await new PurgeCSS().purge({
|
||||
const purgeCSSResults = await (new PurgeCSS()).purge({
|
||||
content: purgeCSSContentFiles,
|
||||
css: [ outfile ],
|
||||
rejected: true,
|
||||
defaultExtractor: content => content.match(/[a-z0-9_\-\\\/\@]+/gi) || [],
|
||||
defaultExtractor: (content) => content.match(/[a-z0-9_\-\\\/\@]+/gi) || [],
|
||||
safelist: {
|
||||
standard: [ /^((?!\bu-gc-).)*$/ ]
|
||||
}
|
||||
})
|
||||
|
||||
for(let result of purgeCSSResults) {
|
||||
for (let result of purgeCSSResults) {
|
||||
await writeFile(outfile, result.css)
|
||||
|
||||
message(`${label} purged`, 'chore', timeLabel);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import loconfig from '../utils/config.js';
|
||||
import message from '../utils/message.js';
|
||||
import notification from '../utils/notification.js';
|
||||
import resolve from '../utils/template.js';
|
||||
import loconfig from '../helpers/config.js';
|
||||
import message from '../helpers/message.js';
|
||||
import notification from '../helpers/notification.js';
|
||||
import resolve from '../helpers/template.js';
|
||||
import { merge } from '../utils/index.js';
|
||||
import { basename } from 'node:path';
|
||||
import mixer from 'svg-mixer';
|
||||
|
||||
@@ -44,9 +45,18 @@ export default async function compileSVGs(mixerOptions = null) {
|
||||
mixerOptions !== developmentMixerOptions &&
|
||||
mixerOptions !== productionMixerOptions
|
||||
) {
|
||||
mixerOptions = Object.assign({}, defaultMixerOptions, mixerOptions);
|
||||
mixerOptions = merge({}, defaultMixerOptions, mixerOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* @param {object} entry - The entrypoint to process.
|
||||
* @param {string[]} entry.includes - One or more paths to process.
|
||||
* @param {string} entry.outfile - The file to write to.
|
||||
* @param {?string} [entry.label] - The task label.
|
||||
* Defaults to the outfile name.
|
||||
* @return {Promise}
|
||||
*/
|
||||
loconfig.tasks.svgs.forEach(async ({
|
||||
includes,
|
||||
outfile,
|
||||
@@ -60,6 +70,10 @@ export default async function compileSVGs(mixerOptions = null) {
|
||||
console.time(timeLabel);
|
||||
|
||||
try {
|
||||
if (!Array.isArray(includes)) {
|
||||
includes = [ includes ];
|
||||
}
|
||||
|
||||
includes = resolve(includes);
|
||||
outfile = resolve(outfile);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import loconfig from '../utils/config.js';
|
||||
import message from '../utils/message.js';
|
||||
import resolve from '../utils/template.js';
|
||||
import loconfig from '../helpers/config.js';
|
||||
import message from '../helpers/message.js';
|
||||
import resolve from '../helpers/template.js';
|
||||
import { merge } from '../utils/index.js';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import events from 'node:events';
|
||||
import {
|
||||
@@ -94,11 +95,22 @@ export default async function bumpVersions(versionOptions = null) {
|
||||
versionOptions !== developmentVersionOptions &&
|
||||
versionOptions !== productionVersionOptions
|
||||
) {
|
||||
versionOptions = Object.assign({}, defaultVersionOptions, versionOptions);
|
||||
versionOptions = merge({}, defaultVersionOptions, versionOptions);
|
||||
}
|
||||
|
||||
const queue = new Map();
|
||||
|
||||
/**
|
||||
* @async
|
||||
* @param {object} entry - The entrypoint to process.
|
||||
* @param {string} entry.outfile - The file to write to.
|
||||
* @param {?string} [entry.label] - The task label.
|
||||
* Defaults to the outfile name.
|
||||
* @param {?string} [entry.format] - The version number format.
|
||||
* @param {?string} [entry.key] - The JSON field name assign the version number to.
|
||||
* @param {?string|number} [entry.pretty] - The white space to use to format the JSON file.
|
||||
* @return {Promise}
|
||||
*/
|
||||
loconfig.tasks.versions.forEach(({
|
||||
outfile,
|
||||
label = null,
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* @file Provides simple user configuration options.
|
||||
*/
|
||||
|
||||
import loconfig from '../../loconfig.json' assert { type: 'json' };
|
||||
|
||||
let usrconfig;
|
||||
|
||||
try {
|
||||
usrconfig = await import('../../loconfig.local.json', {
|
||||
assert: { type: 'json' }
|
||||
});
|
||||
usrconfig = usrconfig.default;
|
||||
|
||||
merge(loconfig, usrconfig);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
export default loconfig;
|
||||
|
||||
/**
|
||||
* Creates a new object with all nested object properties
|
||||
* merged into it recursively.
|
||||
*
|
||||
* @param {object} target - The target object.
|
||||
* @param {object[]} ...sources - The source object(s).
|
||||
* @throws {TypeError} If the target and source are the same.
|
||||
* @return {object} Returns the `target` object.
|
||||
*/
|
||||
export function merge(target, ...sources) {
|
||||
for (const source of sources) {
|
||||
if (target === source) {
|
||||
throw new TypeError(
|
||||
'Cannot merge, target and source are the same'
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in source) {
|
||||
if (source[key] != null) {
|
||||
if (isObjectLike(source[key]) && isObjectLike(target[key])) {
|
||||
merge(target[key], source[key]);
|
||||
continue;
|
||||
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
|
||||
target[key] = target[key].concat(source[key]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the passed value is an `Object`.
|
||||
*
|
||||
* @param {*} value - The value to be checked.
|
||||
* @return {boolean} Returns `true` if the value is an `Object`,
|
||||
* otherwise `false`.
|
||||
*/
|
||||
function isObjectLike(value) {
|
||||
return (value != null && typeof value === 'object');
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* @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 globby}
|
||||
* - {@link https://npmjs.com/package/fast-glob fast-glob}
|
||||
* - {@link https://npmjs.com/package/glob glob}
|
||||
*/
|
||||
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
/**
|
||||
* @type {string[]} A list of packages to attempt import.
|
||||
*/
|
||||
const candidates = [
|
||||
'tiny-glob',
|
||||
'globby',
|
||||
'fast-glob',
|
||||
'glob',
|
||||
];
|
||||
|
||||
let glob;
|
||||
|
||||
try {
|
||||
glob = await importGlob();
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
export default glob;
|
||||
|
||||
/**
|
||||
* Imports the first available glob function.
|
||||
*
|
||||
* @throws {TypeError} If no glob library was found.
|
||||
* @return {function}
|
||||
*/
|
||||
async function importGlob() {
|
||||
let glob, module;
|
||||
|
||||
for (let name of candidates) {
|
||||
try {
|
||||
module = await import(name);
|
||||
|
||||
if (typeof module.default !== 'function') {
|
||||
throw new TypeError(`Expected ${name} to be a function`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the function to ensure
|
||||
* a common pattern.
|
||||
*/
|
||||
switch (name) {
|
||||
case 'tiny-glob':
|
||||
return createArrayableGlob(module.default, {
|
||||
filesOnly: true
|
||||
});
|
||||
|
||||
case 'glob':
|
||||
return promisify(module.default);
|
||||
|
||||
default:
|
||||
return module.default;
|
||||
}
|
||||
} catch (err) {
|
||||
// swallow this error; skip to the next candidate.
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
`No glob library was found, expected one of: ${candidates.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {function}
|
||||
*/
|
||||
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);
|
||||
});
|
||||
};
|
||||
}
|
||||
115
build/utils/index.js
Normal file
115
build/utils/index.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @file Provides generic functions and constants.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {RegExp} - Match all special characters.
|
||||
*/
|
||||
const regexUnescaped = /[\[\]\{\}\(\)\-\*\+\?\.\,\\\^\$\|\#\s]/g;
|
||||
|
||||
/**
|
||||
* Quotes regular expression characters.
|
||||
*
|
||||
* @param {string} str - The input string.
|
||||
* @return {string} Returns the quoted (escaped) string.
|
||||
*/
|
||||
function escapeRegExp(str) {
|
||||
return str.replace(regexUnescaped, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (const key in input) {
|
||||
const field = (prefix ? prefix + '.' + key : key);
|
||||
|
||||
if (isObjectLike(input[key])) {
|
||||
flatten(input[key], field, target);
|
||||
} else {
|
||||
target[field] = input[key];
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the passed value is an `Object`.
|
||||
*
|
||||
* @param {*} value - The value to be checked.
|
||||
* @return {boolean} Returns `true` if the value is an `Object`,
|
||||
* otherwise `false`.
|
||||
*/
|
||||
function isObjectLike(value) {
|
||||
return (value != null && typeof value === 'object');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new object with all nested object properties
|
||||
* merged into it recursively.
|
||||
*
|
||||
* @param {object} target - The target object.
|
||||
* @param {object[]} ...sources - The source object(s).
|
||||
* @throws {TypeError} If the target and source are the same.
|
||||
* @return {object} Returns the `target` object.
|
||||
*/
|
||||
function merge(target, ...sources) {
|
||||
for (const source of sources) {
|
||||
if (target === source) {
|
||||
throw new TypeError(
|
||||
'Cannot merge, target and source are the same'
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in source) {
|
||||
if (source[key] != null) {
|
||||
if (isObjectLike(source[key]) && isObjectLike(target[key])) {
|
||||
merge(target[key], source[key]);
|
||||
continue;
|
||||
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
|
||||
target[key] = target[key].concat(source[key]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
export {
|
||||
escapeRegExp,
|
||||
flatten,
|
||||
isObjectLike,
|
||||
merge,
|
||||
regexUnescaped,
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* @file If available, returns the PostCSS Processor creator and
|
||||
* any the Autoprefixer PostCSS plugin.
|
||||
*/
|
||||
|
||||
let postcss, autoprefixer;
|
||||
|
||||
try {
|
||||
postcss = await import('postcss');
|
||||
postcss = postcss.default;
|
||||
|
||||
autoprefixer = await import('autoprefixer');
|
||||
autoprefixer = autoprefixer.default;
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
export default postcss;
|
||||
export const pluginsList = [
|
||||
autoprefixer,
|
||||
];
|
||||
export const pluginsMap = {
|
||||
'autoprefixer': autoprefixer,
|
||||
};
|
||||
export {
|
||||
autoprefixer
|
||||
};
|
||||
@@ -2,10 +2,11 @@ import concatFiles, { developmentConcatFilesArgs } from './tasks/concats.js';
|
||||
import compileScripts, { developmentScriptsArgs } from './tasks/scripts.js';
|
||||
import compileStyles, { developmentStylesArgs } from './tasks/styles.js' ;
|
||||
import compileSVGs, { developmentSVGsArgs } from './tasks/svgs.js';
|
||||
import loconfig, { merge } from './utils/config.js';
|
||||
import message from './utils/message.js';
|
||||
import notification from './utils/notification.js';
|
||||
import resolve from './utils/template.js';
|
||||
import loconfig from './helpers/config.js';
|
||||
import message from './helpers/message.js';
|
||||
import notification from './helpers/notification.js';
|
||||
import resolve from './helpers/template.js';
|
||||
import { merge } from './utils/index.js';
|
||||
import browserSync from 'browser-sync';
|
||||
import { join } from 'node:path';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user