mirror of
https://github.com/locomotivemtl/locomotive-boilerplate.git
synced 2026-01-15 00:55:08 +08:00
Compare commits
36 Commits
devenini/d
...
feature/sc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dca6c5de1d | ||
|
|
05a00c4258 | ||
|
|
7517be0e76 | ||
|
|
297e0b4ec8 | ||
|
|
9a2083d894 | ||
|
|
1a81c865ae | ||
|
|
d6b5784cdd | ||
|
|
8894664743 | ||
|
|
8aac2ffea6 | ||
|
|
349d110dee | ||
|
|
7be5e48f22 | ||
|
|
1ee315663e | ||
|
|
89bb00790f | ||
|
|
9db0c71a82 | ||
|
|
7742bbb9d0 | ||
|
|
c9a9209b4b | ||
|
|
9d758f3b2c | ||
|
|
0738dd6491 | ||
|
|
a4656f59ed | ||
|
|
7ff6094e40 | ||
|
|
3fee6f4888 | ||
|
|
f774482255 | ||
|
|
b7d9311ac6 | ||
|
|
c22a006079 | ||
|
|
af57ebd9cb | ||
|
|
c82e9916d0 | ||
|
|
943324220a | ||
|
|
477cec7763 | ||
|
|
5d38685460 | ||
|
|
9079d735bc | ||
|
|
87238fcdd5 | ||
|
|
2f75d8f3d2 | ||
|
|
0346a15b57 | ||
|
|
a2d658bc13 | ||
|
|
1fe30a9837 | ||
|
|
aba77ea2d9 |
@@ -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.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": 1675197299100
|
||||
"version": 1683902331941
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import modular from 'modujs';
|
||||
import * as modules from './modules';
|
||||
import globals from './globals';
|
||||
import { html } from './utils/environment';
|
||||
import config from './config'
|
||||
import { debounce } from './utils/tickers'
|
||||
import { $html } from './utils/dom';
|
||||
import { ENV, FONT, CUSTOM_EVENT, CSS_CLASS } from './config'
|
||||
import { isFontLoadingAPIAvailable, loadFonts } from './utils/fonts';
|
||||
|
||||
const app = new modular({
|
||||
@@ -25,28 +26,32 @@ window.onload = (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const EAGER_FONTS = [
|
||||
{ family: 'Source Sans', style: 'normal', weight: 400 },
|
||||
{ family: 'Source Sans', style: 'normal', weight: 700 },
|
||||
];
|
||||
|
||||
function init() {
|
||||
globals();
|
||||
|
||||
app.init(app);
|
||||
|
||||
html.classList.add('is-loaded');
|
||||
html.classList.add('is-ready');
|
||||
html.classList.remove('is-loading');
|
||||
$html.classList.add(CSS_CLASS.LOADED);
|
||||
$html.classList.add(CSS_CLASS.READY);
|
||||
$html.classList.remove(CSS_CLASS.LOADING);
|
||||
|
||||
// Bind window resize event with default vars
|
||||
const resizeEndEvent = new CustomEvent(CUSTOM_EVENT.RESIZE_END)
|
||||
window.addEventListener('resize', () => {
|
||||
$html.style.setProperty('--vw', `${document.documentElement.clientWidth * 0.01}px`)
|
||||
debounce(() => {
|
||||
window.dispatchEvent(resizeEndEvent)
|
||||
}, 200, false)
|
||||
})
|
||||
|
||||
/**
|
||||
* Eagerly load the following fonts.
|
||||
*/
|
||||
if (isFontLoadingAPIAvailable) {
|
||||
loadFonts(EAGER_FONTS, config.IS_DEV).then((eagerFonts) => {
|
||||
html.classList.add('fonts-loaded');
|
||||
loadFonts(FONT.EAGER_FONTS, ENV.IS_DEV).then((eagerFonts) => {
|
||||
$html.classList.add(CSS_CLASS.FONTS_LOADED);
|
||||
|
||||
if (config.IS_DEV) {
|
||||
if (ENV.IS_DEV) {
|
||||
console.group('Eager fonts loaded!', eagerFonts.length, '/', document.fonts.size);
|
||||
console.group('State of eager fonts:')
|
||||
eagerFonts.forEach((font) => console.log(font.family, font.style, font.weight, font.status/*, font*/))
|
||||
|
||||
@@ -7,18 +7,50 @@
|
||||
* > (since `process` is a Node API, not a web API).
|
||||
* > — https://esbuild.github.io/api/#platform
|
||||
*/
|
||||
const env = process.env.NODE_ENV
|
||||
|
||||
export default config = Object.freeze({
|
||||
// Environments
|
||||
ENV: env,
|
||||
IS_PROD: env === 'production',
|
||||
IS_DEV: env === 'development',
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const IS_DESKTOP = typeof window.orientation === 'undefined'
|
||||
|
||||
// CSS class names
|
||||
CSS_CLASS: {
|
||||
LOADING: 'is-loading',
|
||||
READY: 'is-ready',
|
||||
LOADED: 'is-loaded',
|
||||
},
|
||||
// Main environment variables
|
||||
const ENV = Object.freeze({
|
||||
// Node environment
|
||||
NAME: NODE_ENV,
|
||||
IS_PROD: NODE_ENV === 'production',
|
||||
IS_DEV: NODE_ENV === 'development',
|
||||
|
||||
// Device
|
||||
IS_DESKTOP,
|
||||
IS_MOBILE: !IS_DESKTOP,
|
||||
})
|
||||
|
||||
// Main CSS classes used within the project
|
||||
const CSS_CLASS = Object.freeze({
|
||||
LOADING: 'is-loading',
|
||||
LOADED: 'is-loaded',
|
||||
READY: 'is-ready',
|
||||
FONTS_LOADED: 'fonts-loaded',
|
||||
LAZY_CONTAINER: 'c-lazy',
|
||||
LAZY_LOADED: '-lazy-loaded',
|
||||
// ...
|
||||
})
|
||||
|
||||
// Custom js events
|
||||
const CUSTOM_EVENT = Object.freeze({
|
||||
RESIZE_END: 'loco.resizeEnd',
|
||||
// ...
|
||||
})
|
||||
|
||||
// Fonts parameters
|
||||
const FONT = Object.freeze({
|
||||
EAGER: [
|
||||
{ family: 'Source Sans', style: 'normal', weight: 400 },
|
||||
{ family: 'Source Sans', style: 'normal', weight: 700 },
|
||||
],
|
||||
})
|
||||
|
||||
export {
|
||||
ENV,
|
||||
CSS_CLASS,
|
||||
CUSTOM_EVENT,
|
||||
FONT,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import svg4everybody from 'svg4everybody';
|
||||
import config from './config';
|
||||
import { ENV } from './config';
|
||||
|
||||
// Dynamic imports for development mode only
|
||||
let gridHelper;
|
||||
(async () => {
|
||||
if (config.IS_DEV) {
|
||||
if (ENV.IS_DEV) {
|
||||
const gridHelperModule = await import('./utils/grid-helper');
|
||||
gridHelper = gridHelperModule?.gridHelper;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { module } from 'modujs';
|
||||
import { EAGER_FONTS } from '../app';
|
||||
import { FONT } from '../config';
|
||||
import { whenReady } from '../utils/fonts';
|
||||
|
||||
export default class extends module {
|
||||
@@ -8,7 +8,7 @@ export default class extends module {
|
||||
}
|
||||
|
||||
init() {
|
||||
whenReady(EAGER_FONTS).then((fonts) => this.onFontsLoaded(fonts));
|
||||
whenReady(FONT.EAGER).then((fonts) => this.onFontsLoaded(fonts));
|
||||
}
|
||||
|
||||
onFontsLoaded(fonts) {
|
||||
|
||||
7
assets/scripts/utils/dom.js
Normal file
7
assets/scripts/utils/dom.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const $html = document.documentElement
|
||||
const $body = document.body
|
||||
|
||||
export {
|
||||
$html,
|
||||
$body,
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const APP_NAME = 'Boilerplate';
|
||||
const DATA_API_KEY = '.data-api';
|
||||
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
const isDebug = html.hasAttribute('data-debug');
|
||||
|
||||
|
||||
export { APP_NAME, DATA_API_KEY, html, body, isDebug };
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CSS_CLASS } from '../config'
|
||||
|
||||
/**
|
||||
* Get an image meta data
|
||||
*
|
||||
@@ -89,14 +91,14 @@ const lazyLoadImage = async ($el, url, callback) => {
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
let lazyParent = $el.closest('.c-lazy')
|
||||
let lazyParent = $el.closest(`.${CSS_CLASS.LAZY_CONTAINER}`)
|
||||
|
||||
if(lazyParent) {
|
||||
lazyParent.classList.add('-lazy-loaded')
|
||||
lazyParent.classList.add(CSS_CLASS.LAZY_LOADED)
|
||||
lazyParent.style.backgroundImage = ''
|
||||
}
|
||||
|
||||
$el.classList.add('-lazy-loaded')
|
||||
$el.classList.add(CSS_CLASS.LAZY_LOADED)
|
||||
|
||||
callback?.()
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ $input-icon-color: 424242; // No #
|
||||
.c-form_input {
|
||||
padding: rem(10px);
|
||||
border: 1px solid lightgray;
|
||||
background-color: $color-lightest;
|
||||
background-color: color(lightest);
|
||||
|
||||
&:hover {
|
||||
border-color: darkgray;
|
||||
@@ -71,7 +71,7 @@ $checkbox-icon-color: $input-icon-color;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: $color-lightest;
|
||||
background-color: color(lightest);
|
||||
border: 1px solid lightgray;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: $color-darkest;
|
||||
background-color: color(darkest);
|
||||
opacity: 0.5;
|
||||
width: 7px;
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
@@ -88,8 +73,8 @@ body {
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: $selection-background-color;
|
||||
color: $selection-text-color;
|
||||
background-color: $color-selection-background;
|
||||
color: $color-selection-text;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// ==========================================================================
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
--icon-height: calc(var(--icon-width) * (1 / (var(--icon-ratio))));
|
||||
--icon-height: calc(var(--icon-width) * math.div(1, (var(--icon-ratio))));
|
||||
|
||||
display: block;
|
||||
width: var(--icon-width);
|
||||
|
||||
@@ -5,24 +5,26 @@
|
||||
// Palette
|
||||
// =============================================================================
|
||||
|
||||
$color-lightest: #FFFFFF;
|
||||
$color-darkest: #000000;
|
||||
$colors: (
|
||||
primary: #3297FD,
|
||||
lightest: #FFFFFF,
|
||||
darkest: #000000,
|
||||
);
|
||||
|
||||
// Specific
|
||||
|
||||
// Specifics
|
||||
// =============================================================================
|
||||
|
||||
// Link
|
||||
$color-link: #1A0DAB;
|
||||
$color-link-focus: #1A0DAB;
|
||||
$color-link-hover: darken(#1A0DAB, 10%);
|
||||
$color-link: color(primary);
|
||||
$color-link-focus: color(primary);
|
||||
$color-link-hover: darken(color(primary), 10%);
|
||||
|
||||
// Selection
|
||||
$selection-text-color: #3297FD;
|
||||
$selection-background-color: #FFFFFF;
|
||||
|
||||
// Social Colors
|
||||
// =============================================================================
|
||||
$color-selection-text: color(darkest);
|
||||
$color-selection-background: color(lightest);
|
||||
|
||||
// Socials
|
||||
$color-facebook: #3B5998;
|
||||
$color-instagram: #E1306C;
|
||||
$color-youtube: #CD201F;
|
||||
|
||||
@@ -49,16 +49,7 @@ $font-faces: (
|
||||
// Base
|
||||
$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;
|
||||
$font-color: color(darkest);
|
||||
|
||||
// Weights
|
||||
$font-weight-light: 300;
|
||||
@@ -84,7 +75,6 @@ $padding: $unit;
|
||||
// Grid
|
||||
// ==========================================================================
|
||||
$base-column-nb: 12;
|
||||
$base-column-gap: $unit-small;
|
||||
|
||||
// Breakpoints
|
||||
// =============================================================================
|
||||
|
||||
34
assets/styles/settings/_config.variables.scss
Normal file
34
assets/styles/settings/_config.variables.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
// ==========================================================================
|
||||
// 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)};
|
||||
|
||||
// // Colors
|
||||
// @each $color, $value in $colors {
|
||||
// --color-#{"" + $color}: #{$value};
|
||||
// }
|
||||
|
||||
@media (min-width: $from-small) {
|
||||
--grid-columns: #{$base-column-nb};
|
||||
--grid-gutter: #{rem(16px)};
|
||||
--grid-margin: #{rem(20px)};
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@
|
||||
// @param {number} $num - id of the child
|
||||
|
||||
@mixin middle($num) {
|
||||
&:nth-child(#{round($num / 2)}) {
|
||||
&:nth-child(#{round(math.div($num, 2))}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,3 +139,22 @@
|
||||
}
|
||||
|
||||
$context: 'frontend' !default;
|
||||
|
||||
|
||||
// Returns color code.
|
||||
//
|
||||
// ```scss
|
||||
// .c-box {
|
||||
// width: color(primary);
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// @param {string} $key - The color key in $colors.
|
||||
// @return {color}
|
||||
|
||||
@function color($key) {
|
||||
@if not map-has-key($colors, $key) {
|
||||
@error "Unknown '#{$key}' in $colors.";
|
||||
}
|
||||
@return map-get($colors, $key);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// @return {number}
|
||||
|
||||
@function strip-units($number) {
|
||||
@return $number / ($number * 0 + 1);
|
||||
@return math.div($number, ($number * 0 + 1));
|
||||
}
|
||||
|
||||
// Returns the square root of the given number.
|
||||
@@ -21,7 +21,7 @@
|
||||
$value: $x;
|
||||
|
||||
@for $i from 1 through 10 {
|
||||
$value: $x - ($x * $x - abs($number)) / (2 * $x);
|
||||
$value: $x - math.div(($x * $x - abs($number)), (2 * $x));
|
||||
$x: $value;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
} @else if $exp < 0 {
|
||||
@for $i from 1 through -$exp {
|
||||
$value: $value / $number;
|
||||
$value: math.div($value, $number);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
// If the angle has `deg` as unit, convert to radians.
|
||||
@if ($unit == deg) {
|
||||
@return $angle / 180 * pi();
|
||||
@return math.div($angle, 180) * pi();
|
||||
}
|
||||
|
||||
@return $angle;
|
||||
@@ -104,7 +104,7 @@
|
||||
$angle: rad($angle);
|
||||
|
||||
@for $i from 0 through 10 {
|
||||
$sin: $sin + pow(-1, $i) * pow($angle, (2 * $i + 1)) / fact(2 * $i + 1);
|
||||
$sin: $sin + pow(-1, $i) * math.div(pow($angle, (2 * $i + 1)), fact(2 * $i + 1));
|
||||
}
|
||||
|
||||
@return $sin;
|
||||
@@ -120,7 +120,7 @@
|
||||
$angle: rad($angle);
|
||||
|
||||
@for $i from 0 through 10 {
|
||||
$cos: $cos + pow(-1, $i) * pow($angle, 2 * $i) / fact(2 * $i);
|
||||
$cos: $cos + pow(-1, $i) * math.div(pow($angle, 2 * $i), fact(2 * $i));
|
||||
}
|
||||
|
||||
@return $cos;
|
||||
@@ -132,5 +132,5 @@
|
||||
// @return {number}
|
||||
|
||||
@function tan($angle) {
|
||||
@return sin($angle) / cos($angle);
|
||||
@return math.div(sin($angle), cos($angle));
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
font-size: rem($font-size) $important;
|
||||
|
||||
@if ($line-height == "auto") {
|
||||
line-height: ceil($font-size / $line-height) * ($line-height / $font-size) $important;
|
||||
line-height: ceil(math.div($font-size, $line-height)) * math.div($line-height, $font-size) $important;
|
||||
}
|
||||
@else {
|
||||
@if (type-of($line-height) == number or $line-height == "inherit" or $line-height == "normal") {
|
||||
|
||||
@@ -58,7 +58,7 @@ $breakpoint-delimiter: \@ !default;
|
||||
@for $numerator from 1 through $denominator {
|
||||
// Build a class in the format `.u-3/4[@<breakpoint>]`.
|
||||
.u-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
|
||||
width: ($numerator / $denominator) * 100% $important;
|
||||
width: math.div($numerator, $denominator) * 100% $important;
|
||||
}
|
||||
|
||||
@if ($widths-offsets == true) {
|
||||
@@ -66,13 +66,13 @@ $breakpoint-delimiter: \@ !default;
|
||||
.u-push-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
|
||||
position: relative $important;
|
||||
right: auto $important;
|
||||
left: ($numerator / $denominator) * 100% $important;
|
||||
left: math.div($numerator, $denominator) * 100% $important;
|
||||
}
|
||||
|
||||
// Build a class in the format `.u-pull-5/6[@<breakpoint>]`.
|
||||
.u-pull-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
|
||||
position: relative $important;
|
||||
right: ($numerator / $denominator) * 100% $important;
|
||||
right: math.div($numerator, $denominator) * 100% $important;
|
||||
left: auto $important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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');
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
1283
package-lock.json
generated
1283
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user