1
0
mirror of https://github.com/locomotivemtl/locomotive-boilerplate.git synced 2026-01-15 00:55:08 +08:00

29 Commits

Author SHA1 Message Date
Chauncey McAskill
89ce8ce1ad Add support for arbitrary data in template tags
Where data is defined under a `data` field in 'loconfig.json'.

Example:

```json5
{
    "paths": {…},
    "data": {
        "foo": "b"
    },
    "tasks": {
        "concats": [
            {
                "includes": [
                    "{% paths.scripts.src %}/vendors/*.js"
                ],
                "outfile": "{% paths.scripts.dest %}/a{% foo %}c.js"
            }
        ]
    }
}
```
2023-03-23 10:27:04 -04:00
Deven Caron
8aac2ffea6 Merge pull request #132 from locomotivemtl/feature/build-task-chores
Various fixes and changes to build utilities
2023-03-06 09:07:04 -05:00
Chauncey McAskill
349d110dee Revert merging of usrconfig to use let declaration 2023-03-03 13:13:31 -05:00
Chauncey McAskill
7be5e48f22 Fix import of merge util in 'watch.js'
The `merge` function is provided by 'utils/index.js'.
2023-03-03 13:07:47 -05:00
Chauncey McAskill
1ee315663e Fix glob.js and improve documentation
Documented API inconsistencies between supported glob libraries (expected/supported parameters and return types).

Fixed API inconsistencies between 'tiny-glob', 'globby', and 'glob', to match 'fast-glob'.

Fixed broken support for preset options for 'tiny-glob'.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
89bb00790f Update postcss.js documentation 2023-03-03 13:04:22 -05:00
Chauncey McAskill
9db0c71a82 Move helpers/utils exports to end of file
Improves readability by always expecting exports at the end of the file.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
7742bbb9d0 Add block comments to task iteratees 2023-03-03 13:04:22 -05:00
Chauncey McAskill
c9a9209b4b Refactor build utilities
Separated generic functions from build helpers.

Changed:
- Moved 'utils/*.js' to 'helpers/*.js'
- From 'utils/config.js':
  - Moved function `merge` to 'utils/index.js'.
  - Moved function `isObjectLike` to 'utils/index.js'.
- From 'utils/template.js':
  - Moved function `flatten` to 'utils/index.js'.
  - Moved function `escapeRegExp` to 'utils/index.js'.
- From 'tasks/styles.js':
  - Moved function `createPostCSSProcessor` to 'helpers/postcss.js' as `createProcessor`.
- Replaced function `Object.assign` with `merge` for task options parsing in all tasks.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
9d758f3b2c Improve postcss.js
Added constant `supportsPostCSS` to provide a boolean to check if PostCSS is available.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
0738dd6491 Improve glob.js
Added constant `supportsGlob` to provide a boolean to check if a glob function is available.
2023-03-03 13:04:22 -05:00
Chauncey McAskill
a4656f59ed Improve concats.js, scripts.js, svgs.js
Added:
- Condition to cast `includes` into an array.

Removed:
- Variable `files` in favour of reusing `includes` in 'concats.js'.
2023-03-03 13:04:20 -05:00
Deven Caron
7ff6094e40 Move sass to dev dependencies 2023-03-03 10:49:57 -05:00
Chauncey McAskill
3fee6f4888 Update NPM depdencies
Updated:
- browser-sync v2.27.5 → v2.27.11
- esbuild v0.16.17 → v0.17.6
- engine.io v3.5.0 → v6.4.0
- qs v6.2.3 → v6.11.0

Changed:
- Overrode ua-parser-js constraint in browser-sync from `1.0.2` to `~1.0.33` to fix security notice.
2023-02-09 10:52:26 -05:00
Deven Caron
f774482255 Update README node version 2023-02-09 10:21:39 -05:00
Deven Caron
b7d9311ac6 Remove container width; replace with padding 2023-02-09 10:18:47 -05:00
Deven Caron
c22a006079 Merge pull request #139 from locomotivemtl/feature/update-node-version
Update node version to v17.9 and switch from node-sass to sass
2023-02-09 10:15:42 -05:00
Deven Caron
af57ebd9cb Merge branch 'master' into feature/update-node-version 2023-02-09 10:11:24 -05:00
Deven Caron
c82e9916d0 Merge branch 'master' into feature/update-node-version 2023-02-09 10:05:25 -05:00
Deven Caron
943324220a Downgrade to node v17.9 2023-02-09 10:00:31 -05:00
Deven Caron
477cec7763 Merge pull request #141 from locomotivemtl/feature/optimize-grid
Optimise grid-column loops / Use css vars for grid columns
2023-02-09 09:08:35 -05:00
Deven Caron
5d38685460 Merge branch 'master' into feature/optimize-grid 2023-02-09 09:07:35 -05:00
Deven Caron
9079d735bc Merge pull request #138 from locomotivemtl/feature/css-variables
Feature / CSS Variables
2023-02-09 09:02:59 -05:00
Deven Caron
87238fcdd5 Merge branch 'master' into feature/css-variables 2023-02-09 09:01:24 -05:00
Deven Caron
2f75d8f3d2 Merge pull request #142 from locomotivemtl/dependabot/npm_and_yarn/http-cache-semantics-4.1.1
Bump http-cache-semantics from 4.1.0 to 4.1.1
2023-02-07 09:30:26 -05:00
dependabot[bot]
0346a15b57 Bump http-cache-semantics from 4.1.0 to 4.1.1
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-04 05:43:43 +00:00
Deven Caron
a2d658bc13 Remove log to prevent throwing unwanted errors 2023-02-02 10:14:48 -05:00
Lucas Vallenet
1fe30a9837 Optimize grid-column loops / Use css vars for grid columns 2023-02-01 10:23:43 +01:00
Lucas Vallenet
aba77ea2d9 Add default CSS Variables 2023-01-04 11:21:43 +01:00
31 changed files with 1559 additions and 799 deletions

2
.nvmrc
View File

@@ -1 +1 @@
v18.13
v17.9

View File

@@ -23,7 +23,7 @@ Learn more about [languages and technologies](docs/technologies.md).
Make sure you have the following installed:
* [Node] — at least 18.13, the latest LTS is recommended.
* [Node] — at least 17.9, the latest LTS is recommended.
* [NPM] — at least 8.0, the latest LTS is recommended.
> 💡 You can use [NVM] to install and use different versions of Node via the command-line.

View File

@@ -1,3 +1,3 @@
{
"version": 1675197299100
"version": 1675955478084
}

View File

@@ -3,30 +3,29 @@
// ==========================================================================
.c-heading {
line-height: $line-height-h;
margin-bottom: rem(30px);
&.-h1 {
font-size: rem($font-size-h1);
font-size: var(--font-size-h1);
}
&.-h2 {
font-size: rem($font-size-h2);
font-size: var(--font-size-h2);
}
&.-h3 {
font-size: rem($font-size-h3);
font-size: var(--font-size-h3);
}
&.-h4 {
font-size: rem($font-size-h4);
font-size: var(--font-size-h4);
}
&.-h5 {
font-size: rem($font-size-h5);
font-size: var(--font-size-h5);
}
&.-h6 {
font-size: rem($font-size-h6);
font-size: var(--font-size-h6);
}
}

View File

@@ -2,21 +2,6 @@
// Elements / Document
// ==========================================================================
:root {
// Grid
--grid-columns : 4;
--grid-gutter : #{rem(10px)};
--grid-gutter-half : calc(0.5 * var(--grid-gutter));
--grid-margin : 0px;
@media (min-width: $from-small) {
--grid-columns : 12;
--grid-gutter : #{rem(16px)};
--grid-margin : #{rem(20px)};
}
}
//
// Simple page-level setup.
//

View File

@@ -3,13 +3,6 @@
// ==========================================================================
@use "sass:math";
// Settings
// ==========================================================================
@import "settings/config.eases";
@import "settings/config.colors";
@import "settings/config";
// ==========================================================================
// Tools
// ==========================================================================
@@ -22,6 +15,14 @@
// @import "tools/widths";
// @import "tools/family";
// Settings
// ==========================================================================
@import "settings/config.eases";
@import "settings/config.colors";
@import "settings/config";
@import "settings/config.variables";
// Generic
// ==========================================================================

View File

@@ -13,6 +13,6 @@
.o-container {
margin-right: auto;
margin-left: auto;
padding-right: $base-column-gap;
padding-left: $base-column-gap;
padding-left: var(--grid-margin);
padding-right: var(--grid-margin);
}

View File

@@ -31,6 +31,12 @@
// ==========================================================================
// Cols
// ==========================================================================
// Responsive grid columns based on `--grid-columns`
&.-cols {
grid-template-columns: repeat(var(--grid-columns), 1fr);
}
&.-col-#{$base-column-nb} {
grid-template-columns: repeat(#{$base-column-nb}, 1fr);
}
@@ -51,8 +57,8 @@
// Gutters rows and columns
&.-gutters {
gap: $base-column-gap;
column-gap: $base-column-gap;
gap: var(--grid-gutter);
column-gap: var(--grid-gutter);
}
// ==========================================================================
@@ -163,7 +169,8 @@
// By default, a grid item takes full width of its parent.
//
.o-grid_item {
grid-column: 1 / -1;
grid-column-start: var(--gc-start, 1);
grid-column-end: var(--gc-end, -1);
&.-align-end {
align-self: end;

View File

@@ -51,15 +51,6 @@ $font-size: 16px;
$line-height: math.div(24px, $font-size);
$font-color: $color-darkest;
// Headings
$font-size-h1: 36px !default;
$font-size-h2: 28px !default;
$font-size-h3: 24px !default;
$font-size-h4: 20px !default;
$font-size-h5: 18px !default;
$font-size-h6: 16px !default;
$line-height-h: $line-height;
// Weights
$font-weight-light: 300;
$font-weight-normal: 400;
@@ -84,7 +75,6 @@ $padding: $unit;
// Grid
// ==========================================================================
$base-column-nb: 12;
$base-column-gap: $unit-small;
// Breakpoints
// =============================================================================

View File

@@ -0,0 +1,29 @@
// ==========================================================================
// Settings / Config / CSS VARS
// ==========================================================================
:root {
// Grid
--grid-columns: 4;
--grid-gutter: #{rem(10px)};
--grid-gutter-half: calc(0.5 * var(--grid-gutter));
--grid-margin: #{rem(10px)};
// Container
--container-width: calc(100% - 2 * var(--grid-margin));
// Font sizes
--font-size-h1: #{rem(36px)};
--font-size-h2: #{rem(28px)};
--font-size-h3: #{rem(24px)};
--font-size-h4: #{rem(20px)};
--font-size-h5: #{rem(18px)};
--font-size-h6: #{rem(16px)};
@media (min-width: $from-small) {
--grid-columns: #{$base-column-nb};
--grid-gutter: #{rem(16px)};
--grid-margin: #{rem(20px)};
}
}

View File

@@ -13,48 +13,26 @@ $colsMax: $base-column-nb + 1;
$breakpoints: (
"null" null,
"from-tiny" "from-tiny",
"from-small" "from-small",
"from-medium" "from-medium",
"from-large" "from-large",
"from-big" "from-big"
"from-tiny" $from-tiny,
"from-small" $from-small,
"from-medium" $from-medium,
"from-large" $from-large,
"from-big" $from-big
) !default;
@each $breakpoint-namespace, $breakpoint in $breakpoints {
@each $breakpoint, $mediaquery in $breakpoints {
@for $fromIndex from 1 through $colsMax {
@for $toIndex from 1 through $colsMax {
@if $breakpoint == null {
@if $mediaquery == null {
.u-gc-#{$fromIndex}\/#{$toIndex} {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
--gc-start: #{$fromIndex};
--gc-end: #{$toIndex};
}
} @else {
.u-gc-#{$fromIndex}\/#{$toIndex}\@#{$breakpoint} {
@if $breakpoint-namespace == "from-tiny" {
@media (min-width: $from-tiny) {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
}
} @else if $breakpoint-namespace == "from-small" {
@media (min-width: $from-small) {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
}
} @else if $breakpoint-namespace == "from-medium" {
@media (min-width: $from-medium) {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
}
} @else if $breakpoint-namespace == "from-large" {
@media (min-width: $from-large) {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
}
} @else if $breakpoint-namespace == "from-big" {
@media (min-width: $from-big) {
grid-column-start: #{$fromIndex};
grid-column-end: #{$toIndex};
}
@media (min-width: #{$mediaquery}) {
--gc-start: #{$fromIndex};
--gc-end: #{$toIndex};
}
}
}

29
build/helpers/config.js Normal file
View File

@@ -0,0 +1,29 @@
/**
* @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
}
loconfig.data = (loconfig.data ?? {});
loconfig.paths = (loconfig.paths ?? {});
loconfig.tasks = (loconfig.tasks ?? {});
export default loconfig;
export {
loconfig,
};

162
build/helpers/glob.js Normal file
View 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,
};

View File

@@ -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,
};

View File

@@ -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
View 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,
};

View File

@@ -3,9 +3,14 @@
*/
import loconfig from './config.js';
import {
escapeRegExp,
flatten
} from '../utils/index.js';
const templateData = flatten({
paths: loconfig.paths
...loconfig.data,
paths: loconfig.paths,
});
/**
@@ -22,7 +27,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 +61,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 +98,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,
};

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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) {
console.error(err);
}
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');
}

View File

@@ -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
View 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,
};

View File

@@ -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
};

View File

@@ -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';

1283
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
"author": "Locomotive <info@locomotive.ca>",
"type": "module",
"engines": {
"node": ">=18.13",
"node": ">=17.9",
"npm": ">=8.0"
},
"scripts": {
@@ -18,22 +18,25 @@
"modujs": "^1.4.2",
"modularload": "^1.2.6",
"normalize.css": "^8.0.1",
"sass": "^1.57.1",
"svg4everybody": "^2.1.9"
},
"devDependencies": {
"autoprefixer": "^10.4.13",
"browser-sync": "^2.27.11",
"concat": "^1.0.3",
"esbuild": "^0.16.17",
"esbuild": "^0.17.6",
"kleur": "^4.1.5",
"node-notifier": "^10.0.1",
"postcss": "^8.4.21",
"purgecss": "^5.0.0",
"sass": "^1.57.1",
"svg-mixer": "~2.3.14",
"tiny-glob": "^0.2.9"
},
"overrides": {
"browser-sync": {
"ua-parser-js": "~1.0.33"
},
"svg-mixer": {
"postcss": "^8.4.20"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long