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

31 Commits

Author SHA1 Message Date
Lucas Vallenet
b026bdb809 New Typography prefix and default styles 2022-05-02 11:30:04 +02:00
Chauncey McAskill
53beae26b0 Fix missing link in Development documentation
Amends 9a01c0f17f
2022-03-24 13:59:02 -04:00
Chauncey McAskill
9a01c0f17f Update README and documentation
Changed:
- Features, Installation, and Development sections in README.
- Introduction, Configuration, and Tasks in Development.
- Heading levels in Technologies.

Fixed:
- Anchors to option sections in Development.
2022-03-24 13:57:28 -04:00
Deven Caron
97d9f1ec00 Merge pull request #107 from locomotivemtl/feature/documentation
Rewrite README and add documentation
2022-03-24 10:03:58 -04:00
Chauncey McAskill
8b8b267e9d Rewrite README
And split sections from README into dedicated documentation files.

Added:
- "Features" section to summarize the boilerplate's architecture.
- "Getting Started" section to describe how to create a project from the boilerplate.
- "Development" documentation to describe how NPM dependencies, configuring assets and tasks.
- "Technologies" documentation to describe CSS, JS, Locomotive Scroll, ModularLoad, ModularJS.

Changed:
- Moved section "Configuration" to 'docs/development.md'.
- Moved sections "Styles", "Scripts", "Page transitions", and "Scroll detection" to 'docs/technologies.md'.

TODO:
- Move "Environment configuration" section from "Development" to 'feature/local-config' branch.
2022-03-24 09:58:40 -04:00
Deven Caron
822cf7daa8 Merge pull request #106 from locomotivemtl/feature/local-config
Add support for loconfig.local.json
2022-03-24 09:55:02 -04:00
Chauncey McAskill
7f452f1fcc Add example of loconfig.local.json 2022-03-23 14:06:11 -04:00
Chauncey McAskill
5010560ee3 Improve HTTPS/Proxy URL support in watch.js
Added logic to prepend "https://" to the proxy URL if missing.
2022-03-23 13:13:46 -04:00
Chauncey McAskill
0cfb3fbc7d Add support for loconfig.local.json
If a 'loconfig.local.json' file is present (ignored by git), its settings will be merged with those in 'loconfig.json'. Useful for customizing localhost development (see example below).

Added:
- Utility 'config.js' to prepare build settings.
- Function 'merge()' for recursively merging objects and concatenating arrays.

Example:

```json
{
    "paths": {
        "url": "yourlocal.dev"
    },
    "server": {
        "open": true,
        "https": {
            "key": "~/.config/valet/Certificates/{% paths.url %}.key",
            "cert": "~/.config/valet/Certificates/{% paths.url %}.crt"
        }
    }
}
```
2022-03-22 16:40:51 -04:00
Chauncey McAskill
86f88c3f14 Refactor watch.js
Switched to BrowserSync's recommended post-2.0.0 syntax.

Organized logic into functions for easier reading.

Add support for customizing the BrowserSync server from 'loconfig.json'.

Example:

```json
"server": {
    "open": true,
    "https": {
        "key": "~/.config/valet/Certificates/{% paths.url %}.key",
        "cert": "~/.config/valet/Certificates/{% paths.url %}.crt"
    }
}
```
2022-03-22 16:33:19 -04:00
Chauncey McAskill
48bd911804 Refactor template.js
Added:
- Function `resolve()` to process any template tags (in a string) in objects and arrays.

Changed:
- Renamed function `template()` to `resolveValue()`.
- Replaced default export `resolveValue()` with new function `resolve()`.
2022-03-22 16:33:19 -04:00
Chauncey McAskill
d49d3eabb2 Add file comment to notification.js 2022-03-22 16:32:24 -04:00
Chauncey McAskill
28aa6c7de6 Update NPM dependencies
Resolves #105

Updated:
- autoprefixer v10.4.2 → v10.4.4
- browser-sync v2.27.7 → v2.27.9
- esbuild v0.14.21 → v0.14.27
- postcss v8.4.6 → v8.4.12
2022-03-21 17:24:10 -04:00
Chauncey McAskill
38dd28832e Update NPM dependencies
Resolves #102

Updated:
- esbuild v0.14.14 → v0.14.21
- locomotive-scroll v4.1.3 → v4.1.4
- node-notifier v10.0.0 → v10.0.1
- postcss v8.4.5 → v8.4.6
2022-02-15 09:27:37 -05:00
Chauncey McAskill
757c26c772 Fix glob.js file comment 2022-02-15 09:16:03 -05:00
Chauncey McAskill
5e07473396 Make glob optional in concats.js
Added:
- Condition to process includes with glob if it's available.

Changed:
- Utility 'glob.js' to not throw an error if a glob function is unavailable.
2022-02-08 15:18:18 -05:00
Chauncey McAskill
c9056b27d8 Add support for removing duplicates in concats.js
Added:
- Argument `concatOptions` to `concatFiles()` function to customize concatenation.
- Option 'removeDuplicates' to `concatOptions` to remove duplicate paths from the array of globbed paths. Defaults to `true`.

The 'removeDuplicates' is useful for customizing the order of files to concatenate, in which globbing will usually sort paths alphabetically.

This option is convenient when the installed glob library does not support removing duplicates on their own (feature supported in 'fast-glob' and 'glob').

Example:

```
/assets/scripts/vendors/a.js
/assets/scripts/vendors/b.js
/assets/scripts/vendors/c.js
/assets/scripts/vendors/d.js
```

```json
"concats": [
    {
        "includes": [
            "{% paths.scripts.src %}/vendors/c.js",
            "{% paths.scripts.src %}/vendors/*.js"
        ],
        "outfile": "{% paths.scripts.dest %}/vendors.js"
    }
]
```
2022-02-08 14:46:34 -05:00
Chauncey McAskill
38a6c73d2f Add support for customizing glob in concats.js
Added:
- Argument `globOptions` to `concatFiles()` function to customize glob library.
2022-02-08 14:40:00 -05:00
Chauncey McAskill
9d18205b0f Update NPM dependencies
Updated:
- autoprefixer v10.4.0 → v10.4.2
- esbuild v0.13.12 → v0.14.14
- node-sass v6.0.1 → v7.0.1
- postcss v8.3.11 → v8.4.5
2022-01-28 16:00:57 -05:00
Chauncey McAskill
1e7e90c8aa Add support for custom task labels
If ever the basename from the outfile or outdir is an insufficient description.

Example:

```json
"concats": [
    {
        "label": "third-parties",
        "includes": [
            "{% paths.scripts.src %}/vendors/*.js"
        ],
        "outfile": "{% paths.scripts.dest %}/vendors.js"
    }
]
```
2021-12-17 10:39:35 -05:00
Chauncey McAskill
47007cddaf Fix glob.js
Amends 8dec2c69fe

Fixed:
- Forgot to rename all occurrences of 'modules' with 'candidates'.
2021-12-03 15:10:21 -05:00
Chauncey McAskill
5dd3fa843f Add support for watching multiple view paths
Added routine to convert `paths.views` into an array.

Supports a single path as a string, a map of paths, or a list of paths:

```json
"views": "./views/boilerplate/template"
```

```json
"views": {
    "src": "./views/boilerplate/template"
}
```

```json
"views": [
    "./views/boilerplate/template"
]
```
2021-12-03 11:55:35 -05:00
Chauncey McAskill
a5623d3122 Improve comments in watch.js 2021-12-03 11:52:27 -05:00
Chauncey McAskill
67e1fae8f4 Update block comment in styles.js 2021-11-03 17:08:38 -04:00
Chauncey McAskill
a95fe4523c Update postcss.js
Remove extraneous `null` assignment on optional exports.
2021-11-03 16:50:08 -04:00
Chauncey McAskill
eadc414329 Fix merging of default options for PostCSS
Amends 9e3d304654
2021-11-03 16:16:08 -04:00
Chauncey McAskill
b19c18b18c Update NPM dependencies
Updated:
- autoprefixer v10.3.7 → v10.4.0
- browser-sync v2.26.13 → v2.27.7
- esbuild v0.13.4 → v0.13.12
- postcss v8.3.9 → v8.3.11
2021-11-03 13:21:11 -04:00
Chauncey McAskill
db85740a18 Merge pull request #96 from locomotivemtl/mcaskill-refactor-build-options 2021-11-03 13:16:43 -04:00
Chauncey McAskill
5c24fabaa2 Compile assets 2021-11-03 10:50:33 -04:00
Chauncey McAskill
9e3d304654 Add support for task options
Added support for customizing processors in `scripts.js` (esbuild), `styles.js` (node-sass, postcss, autoprefixer), and `svgs.js` (svg-mixer), via arguments for the exported task functions.

Added:
- Constants to decouple shared default options, options for development, and options for production.
- Constants to export default options for development and production as an array of arguments to pass to task functions.

Changed:
- watch.js to apply development args to tasks
2021-11-03 10:49:35 -04:00
Chauncey McAskill
6ded72bc79 Refactor postcss.js
Moved creation of Processor from utility file to styles.js to allow for future customization of `postcss` and `autoprefixer`.
2021-11-03 10:49:35 -04:00
30 changed files with 8729 additions and 2642 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
node_modules
.DS_Store
Thumbs.db
loconfig.*.json
!loconfig.example.json

257
README.md
View File

@@ -4,230 +4,95 @@
</a>
</p>
<h1 align="center">Locomotive Boilerplate</h1>
<p align="center">Front-end boilerplate for projects by Locomotive.</p>
<p align="center">Front-end boilerplate for projects by <a href="https://locomotive.ca/">Locomotive</a>.</p>
## Requirements
## Features
| Name | Version |
| ---------- | -------- |
| [Node] | >= 14.17 |
| [NPM] | >= 6.0 |
* Uses a custom [task runner](docs/development.md) for handling assets.
* Uses [BrowserSync] for fast development and testing in browsers.
* Uses [Sass] for a feature rich superset of CSS.
* Uses [ESBuild] for extremely fast processing of JS/ES modules.
* Uses [SVG Mixer] for processing SVG files and generating spritesheets.
* Uses [ITCSS] for a sane and scalable CSS architecture.
* Uses [Locomotive Scroll] for smooth scrolling with parallax effects.
[Node]: https://nodejs.org/
[NPM]: https://npmjs.com/
Learn more about [languages and technologies](docs/technologies.md).
You can use [nvm](https://github.com/nvm-sh/nvm) to install the node version in `.nvmrc`.
## Getting started
## Installation
Make sure you have the following installed:
* [Node] — at least 14.17, the latest LTS is recommended.
* [NPM] — at least 6.0, the latest LTS is recommended.
> 💡 You can use [NVM] to install and use different versions of Node via the command-line.
```sh
npm i
# Clone the repository.
git clone https://github.com/locomotivemtl/locomotive-boilerplate.git my-new-project
# Enter the newly-cloned directory.
cd my-new-project
```
## Usage
Then replace the original remote repository with your project's repository.
```sh
# start it
npm start
```
Then update the following files to suit your project:
## Configuration
There are a few occurrences that should be renamed for your project:
* [package.json](package.json):
* [`README.md`](README.md):
The file you are currently reading.
* [`package.json`](package.json):
* Package name: `@locomotivemtl/boilerplate`
* Package title: `Locomotive Boilerplate`
* [package-lock.json](package-lock.json):
* [`package-lock.json`](package-lock.json):
* Package name: `@locomotivemtl/boilerplate`
* [loconfig.json](loconfig.json):
* Browser Sync proxy URL: `locomotive-boilerplate.test`
Remove `paths.url` to use Browser Sync's built-in server which uses `paths.dest`.
* [`loconfig.json`](loconfig.json):
* BrowserSync proxy URL: `locomotive-boilerplate.test`
Remove `paths.url` to use BrowserSync's built-in server which uses `paths.dest`.
* View path: `./views/boilerplate/template`
* [environment.js](assets/scripts/utils/environment.js):
* [`environment.js`](assets/scripts/utils/environment.js):
* Application name: `Boilerplate`
* [site.webmanifest](www/site.webmanifest):
* [`site.webmanifest`](www/site.webmanifest):
* Manifest name: `Locomotive Boilerplate`
* Manifest short name: `Boilerplate`
* HTML files:
* Page title: `Locomotive Boilerplate`
## Build
## Installation
#### Tasks
```sh
# watch
# Switch to recommended Node version from .nvmrc
nvm use
# Install dependencies from package.json
npm install
```
## Development
```sh
# Start development server, watch for changes, and compile assets
npm start
# build
# Compile and minify assets
npm run build
```
## Styles
Learn more about [development and building](docs/development.md).
[Sass](https://github.com/sass/node-sass) is our CSS preprocessor. [Autoprefixer](https://github.com/postcss/autoprefixer) is also included.
## Documentation
#### Architecture
* [Development and building](docs/development.md)
* [Languages and technologies](docs/technologies.md)
[ITCSS](https://github.com/itcss) is our CSS architecture.
* `settings`: Global variables, site-wide settings, config switches, etc.
* `tools`: Site-wide mixins and functions.
* `generic`: Low-specificity, far-reaching rulesets (e.g. resets).
* `elements`: Unclassed HTML elements (e.g. `a {}`, `blockquote {}`, `address {}`).
* `objects`: Objects, abstractions, and design patterns (e.g. `.o-layout {}`).
* `components`: Discrete, complete chunks of UI (e.g. `.c-carousel {}`).
* `utilities`: High-specificity, very explicit selectors. Overrides and helper
classes (e.g. `.u-hidden {}`)
[_source_](https://github.com/inuitcss/inuitcss#css-directory-structure)
#### Naming
We use a simplified [BEM](https://github.com/bem) syntax.
```
.block .block_element -modifier
```
#### Namespaces
We namespace our classes for more [transparency](https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/).
* `o-`: Object that it may be used in any number of unrelated contexts to the one you can currently see it in. Making modifications to these types of class could potentially have knock-on effects in a lot of other unrelated places.
* `c-`: Component is a concrete, implementation-specific piece of UI. All of the changes you make to its styles should be detectable in the context youre currently looking at. Modifying these styles should be safe and have no side effects.
* `u-`: Utility has a very specific role (often providing only one declaration) and should not be bound onto or changed. It can be reused and is not tied to any specific piece of UI.
* `s-`: Scope creates a new styling context. Similar to a Theme, but not necessarily cosmetic, these should be used sparingly—they can be open to abuse and lead to poor CSS if not used wisely.
* `is-`, `has-`: Is currently styled a certain way because of a state or condition. It tells us that the DOM currently has a temporary, optional, or short-lived style applied to it due to a certain state being invoked.
[_source_](https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/#the-namespaces)
#### Example
```html
<div class="c-block -large">
<div class="c-block_layout o-layout">
<div class="o-layout_item u-1/2@from-medium">
<div class="c-block_heading o-h -medium">Heading</div>
</div>
<div class="o-layout_item u-1/2@from-medium">
<a class="c-block_button o-button -outline" href="#">Button</a>
</div>
</div>
</div>
```
```scss
.c-block {
&.-large {
padding: rem(60px);
}
}
.c-block_heading {
@media (max-width: $to-medium) {
.c-block.-large & {
margin-bottom: rem(40px);
}
}
}
```
## Scripts
[modularJS](https://github.com/modularorg/modularjs) is a small framework we use on top of ES modules. It compiles with [Rollup](https://github.com/rollup/rollup) and [Babel](https://github.com/babel/babel).
#### Why
* Automatically init visible modules.
* Easily call other modules methods.
* Quickly set scoped events with delegation.
* Simply select DOM elements scoped in their module.
[_source_](https://github.com/modularorg/modularjs#why)
#### Example
```html
<div data-module-example>
<div data-example="main">
<h2>Example</h2>
</div>
<button data-example="load">More</button>
</div>
```
```js
import { module } from 'modujs';
export default class extends module {
constructor(m) {
super(m);
this.events = {
click: {
load: 'loadMore'
}
}
}
loadMore() {
this.$('main')[0].classList.add('is-loading');
}
}
```
[Learn more](https://github.com/modularorg/modularjs)
## Page transitions
[modularLoad](https://github.com/modularorg/modularload) is used for page transitions and lazy loading.
#### Example
```html
<nav>
<a href="/">Home</a>
<a href="/page" data-load="transitionName">Page</a>
</nav>
<div data-load-container>
<img data-load-src="assets/images/hello.jpg">
</div>
```
```js
import modularLoad from 'modularload';
this.load = new modularLoad({
enterDelay: 300,
transitions: {
transitionName: {
enterDelay: 450
}
}
});
```
[Learn more](https://github.com/modularorg/modularload)
## Scroll detection
[Locomotive Scroll](https://github.com/locomotivemtl/locomotive-scroll) is used for elements in viewport detection and smooth scrolling with parallax.
#### Example
```html
<div data-module-scroll>
<div data-scroll>Trigger</div>
<div data-scroll data-scroll-speed="1">Parallax</div>
</div>
```
```js
import LocomotiveScroll from 'locomotive-scroll';
this.scroll = new LocomotiveScroll({
el: this.el,
smooth: true
});
````
[Learn more](https://github.com/locomotivemtl/locomotive-scroll)
[BrowserSync]: https://npmjs.com/package/browser-sync
[ESBuild]: https://npmjs.com/package/esbuild
[ITCSS]: https://itcss.io/
[Locomotive Scroll]: https://npmjs.com/package/locomotive-scroll
[modularJS]: https://npmjs.com/package/modujs
[modularLoad]: https://npmjs.com/package/modularload
[Sass]: https://sass-lang.com/
[SVG Mixer]: https://npmjs.com/package/svg-mixer
[Node]: https://nodejs.org/
[NPM]: https://npmjs.com/
[NVM]: https://github.com/nvm-sh/nvm

View File

@@ -1,28 +0,0 @@
.c-heading {
line-height: $line-height-h;
margin-bottom: rem(30px);
&.-h1 {
font-size: rem($font-size-h1);
}
&.-h2 {
font-size: rem($font-size-h2);
}
&.-h3 {
font-size: rem($font-size-h3);
}
&.-h4 {
font-size: rem($font-size-h4);
}
&.-h5 {
font-size: rem($font-size-h5);
}
&.-h6 {
font-size: rem($font-size-h6);
}
}

View File

@@ -2,6 +2,17 @@
// Base / Page
// ==========================================================================
:root {
// 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)};
}
//
// Simple page-level setup.
//

View File

@@ -31,6 +31,11 @@
@import "elements/fonts";
@import "elements/page";
// Typography
// ==========================================================================
@import "typography/headings";
@import "typography/wysiwyg";
// Objects
// ==========================================================================
@import "objects/scroll";
@@ -47,7 +52,6 @@
// Components
// ==========================================================================
@import "components/scrollbar";
@import "components/heading";
@import "components/button";
@import "components/form";

View File

@@ -21,14 +21,6 @@ $font-size: 16px;
$line-height: 24px / $font-size;
$font-family: $font-sans-serif;
$color: #222222;
// 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
$light: 300;
$normal: 400;

View File

@@ -0,0 +1,38 @@
// ==========================================================================
// Typography / Headings
// ==========================================================================
.t-h1,
.t-h2,
.t-h3,
.t-h4,
.t-h5,
.t-h6 {
--heading-line-height: #{$line-height};
line-height: var(--heading-line-height);
}
.t-h1 {
font-size: var(--font-size-h1);
}
.t-h2 {
font-size: var(--font-size-h2);
}
.t-h3 {
font-size: var(--font-size-h3);
}
.t-h4 {
font-size: var(--font-size-h4);
}
.t-h5 {
font-size: var(--font-size-h5);
}
.t-h6 {
font-size: var(--font-size-h6);
}

View File

@@ -0,0 +1,55 @@
// ==========================================================================
// Typography / Wysiwyg
// ==========================================================================
.t-wysiwyg {
p,
ul,
ol,
blockquote {
margin-bottom: $line-height * 1em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1em 0 0.5em;
}
h1 { @extend .t-h1; }
h2 { @extend .t-h2; }
h3 { @extend .t-h3; }
h4 { @extend .t-h4; }
h5 { @extend .t-h5; }
h6 { @extend .t-h6; }
ul,
ol {
padding-left: 2em;
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
b,
strong {
font-weight: 700;
}
> *:first-child {
margin-top: 0;
}
> *:last-child {
margin-bottom: 0;
}
}

View File

@@ -1,46 +1,132 @@
import loconfig from '../../loconfig.json';
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 template from '../utils/template.js';
import resolve from '../utils/template.js';
import concat from 'concat';
import { basename } from 'node:path';
import {
basename,
normalize,
} from 'node:path';
/**
* @const {object} defaultGlobOptions - The default shared glob options.
* @const {object} developmentGlobOptions - The predefined glob options for development.
* @const {object} productionGlobOptions - The predefined glob options for production.
*/
export const defaultGlobOptions = {
};
export const developmentGlobOptions = Object.assign({}, defaultGlobOptions);
export const productionGlobOptions = Object.assign({}, defaultGlobOptions);
/**
* @typedef {object} ConcatOptions
* @property {boolean} removeDuplicates - Removes duplicate paths from
* the array of matching files and folders.
* Only the first occurrence of each path is kept.
*/
/**
* @const {ConcatOptions} defaultConcatOptions - The default shared concatenation options.
* @const {ConcatOptions} developmentConcatOptions - The predefined concatenation options for development.
* @const {ConcatOptions} productionConcatOptions - The predefined concatenation options for production.
*/
export const defaultConcatOptions = {
removeDuplicates: true,
};
export const developmentConcatOptions = Object.assign({}, defaultConcatOptions);
export const productionConcatOptions = Object.assign({}, defaultConcatOptions);
/**
* @const {object} developmentConcatFilesArgs - The predefined `concatFiles()` options for development.
* @const {object} productionConcatFilesArgs - The predefined `concatFiles()` options for production.
*/
export const developmentConcatFilesArgs = [
developmentGlobOptions,
developmentConcatOptions,
];
export const productionConcatFilesArgs = [
productionGlobOptions,
productionConcatOptions,
];
/**
* Concatenates groups of files.
*
* @todo Add support for minification.
*
* @async
* @param {object|boolean} [globOptions=null] - Customize the glob options.
* If `null`, default production options are used.
* If `false`, the glob function will be ignored.
* @param {object} [concatOptions=null] - Customize the concatenation options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function concatFiles() {
export default async function concatFiles(globOptions = null, concatOptions = null) {
if (glob) {
if (globOptions == null) {
globOptions = productionGlobOptions;
} else if (
globOptions !== false &&
globOptions !== developmentGlobOptions &&
globOptions !== productionGlobOptions
) {
globOptions = Object.assign({}, defaultGlobOptions, globOptions);
}
}
if (concatOptions == null) {
concatOptions = productionConcatOptions;
} else if (
concatOptions !== developmentConcatOptions &&
concatOptions !== productionConcatOptions
) {
concatOptions = Object.assign({}, defaultConcatOptions, concatOptions);
}
loconfig.tasks.concats.forEach(async ({
includes,
outfile
outfile,
label = null
}) => {
const filename = basename(outfile || 'undefined');
if (!label) {
label = basename(outfile || 'undefined');
}
const timeLabel = `${filename} concatenated in`;
const timeLabel = `${label} concatenated in`;
console.time(timeLabel);
try {
includes = includes.map((path) => template(path));
outfile = template(outfile);
includes = resolve(includes);
outfile = resolve(outfile);
const files = await glob(includes);
let files;
if (glob && globOptions) {
files = await glob(includes, globOptions);
} else {
files = includes;
}
if (concatOptions.removeDuplicates) {
files = files.map((path) => normalize(path));
files = [ ...new Set(files) ];
}
await concat(files, outfile);
if (files.length) {
message(`${filename} concatenated`, 'success', timeLabel);
message(`${label} concatenated`, 'success', timeLabel);
} else {
message(`${filename} is empty`, 'notice', timeLabel);
message(`${label} is empty`, 'notice', timeLabel);
}
} catch (err) {
message(`Error concatenating ${filename}`, 'error');
message(`Error concatenating ${label}`, 'error');
message(err);
notification({
title: `${filename} concatenation failed 🚨`,
title: `${label} concatenation failed 🚨`,
message: `${err.name}: ${err.message}`
});
}

View File

@@ -1,59 +1,95 @@
import loconfig from '../../loconfig.json';
import loconfig from '../utils/config.js';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import template from '../utils/template.js';
import resolve from '../utils/template.js';
import esbuild from 'esbuild';
import { basename } from 'node:path';
/**
* @const {object} defaultESBuildOptions - The default shared ESBuild options.
* @const {object} developmentESBuildOptions - The predefined ESBuild options for development.
* @const {object} productionESBuildOptions - The predefined ESBuild options for production.
*/
export const defaultESBuildOptions = {
bundle: true,
color: true,
sourcemap: true,
target: [
'es2015',
],
};
export const developmentESBuildOptions = Object.assign({}, defaultESBuildOptions);
export const productionESBuildOptions = Object.assign({}, defaultESBuildOptions, {
logLevel: 'warning',
minify: true,
});
/**
* @const {object} developmentScriptsArgs - The predefined `compileScripts()` options for development.
* @const {object} productionScriptsArgs - The predefined `compileScripts()` options for production.
*/
export const developmentScriptsArgs = [
developmentESBuildOptions,
];
export const productionScriptsArgs = [
productionESBuildOptions,
];
/**
* Bundles and minifies main JavaScript files.
*
* @async
* @param {object} [esBuildOptions=null] - Customize the ESBuild build API options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function compileScripts() {
export default async function compileScripts(esBuildOptions = null) {
if (esBuildOptions == null) {
esBuildOptions = productionESBuildOptions;
} else if (
esBuildOptions !== developmentESBuildOptions &&
esBuildOptions !== productionESBuildOptions
) {
esBuildOptions = Object.assign({}, defaultESBuildOptions, esBuildOptions);
}
loconfig.tasks.scripts.forEach(async ({
includes,
outdir = '',
outfile = ''
outfile = '',
label = null
}) => {
const filename = basename(outdir || outfile || 'undefined');
if (!label) {
label = basename(outdir || outfile || 'undefined');
}
const timeLabel = `${filename} compiled in`;
const timeLabel = `${label} compiled in`;
console.time(timeLabel);
try {
includes = includes.map((path) => template(path));
includes = resolve(includes);
if (outdir) {
outdir = template(outdir);
outdir = resolve(outdir);
} else if (outfile) {
outfile = template(outfile);
outfile = resolve(outfile);
} else {
throw new TypeError(
'Expected \'outdir\' or \'outfile\''
);
}
await esbuild.build({
await esbuild.build(Object.assign({}, esBuildOptions, {
entryPoints: includes,
bundle: true,
minify: true,
sourcemap: true,
color: true,
logLevel: 'error',
target: [
'es2015',
],
outdir,
outfile
});
outfile,
}));
message(`${filename} compiled`, 'success', timeLabel);
message(`${label} compiled`, 'success', timeLabel);
} catch (err) {
// errors managments (already done in esbuild)
notification({
title: `${filename} compilation failed 🚨`,
title: `${label} compilation failed 🚨`,
message: `${err.errors[0].text} in ${err.errors[0].location.file} line ${err.errors[0].location.line}`
});
}

View File

@@ -1,8 +1,8 @@
import loconfig from '../../loconfig.json';
import loconfig from '../utils/config.js';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import postcss from '../utils/postcss.js';
import template from '../utils/template.js';
import postcss, { pluginsMap as postcssPluginsMap } from '../utils/postcss.js';
import resolve from '../utils/template.js';
import { writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { promisify } from 'node:util';
@@ -10,50 +10,130 @@ import sass from 'node-sass';
const sassRender = promisify(sass.render);
let postcssProcessor;
/**
* @const {object} defaultSassOptions - The default shared Sass options.
* @const {object} developmentSassOptions - The predefined Sass options for development.
* @const {object} productionSassOptions - The predefined Sass options for production.
*/
export const defaultSassOptions = {
omitSourceMapUrl: true,
sourceMap: true,
sourceMapContents: true,
};
export const developmentSassOptions = Object.assign({}, defaultSassOptions, {
outputStyle: 'expanded',
});
export const productionSassOptions = Object.assign({}, defaultSassOptions, {
outputStyle: 'compressed',
});
/**
* @const {object} defaultPostCSSOptions - The default shared PostCSS options.
* @const {object} developmentPostCSSOptions - The predefined PostCSS options for development.
* @const {object} productionPostCSSOptions - The predefined PostCSS options for production.
*/
export const defaultPostCSSOptions = {
processor: {
map: {
annotation: false,
inline: false,
sourcesContent: true,
},
},
};
export const developmentPostCSSOptions = Object.assign({}, defaultPostCSSOptions);
export const productionPostCSSOptions = Object.assign({}, defaultPostCSSOptions);
/**
* @const {object|boolean} developmentStylesArgs - The predefined `compileStyles()` options for development.
* @const {object|boolean} productionStylesArgs - The predefined `compileStyles()` options for production.
*/
export const developmentStylesArgs = [
developmentSassOptions,
developmentPostCSSOptions,
];
export const productionStylesArgs = [
productionSassOptions,
productionPostCSSOptions,
];
/**
* Compiles and minifies main Sass files to CSS.
*
* @todo Add deep merge of `postcssOptions` to better support customization
* of default processor options.
*
* @async
* @param {object} [sassOptions=null] - Customize the Sass render API options.
* If `null`, default production options are used.
* @param {object|boolean} [postcssOptions=null] - Customize the PostCSS processor API options.
* If `null`, default production options are used.
* If `false`, PostCSS processing will be ignored.
* @return {Promise}
*/
export default async function compileStyles() {
export default async function compileStyles(sassOptions = null, postcssOptions = null) {
if (sassOptions == null) {
sassOptions = productionSassOptions;
} else if (
sassOptions !== developmentSassOptions &&
sassOptions !== productionSassOptions
) {
sassOptions = Object.assign({}, defaultSassOptions, sassOptions);
}
if (postcss) {
if (postcssOptions == null) {
postcssOptions = productionPostCSSOptions;
} else if (
postcssOptions !== false &&
postcssOptions !== developmentPostCSSOptions &&
postcssOptions !== productionPostCSSOptions
) {
postcssOptions = Object.assign({}, defaultPostCSSOptions, postcssOptions);
}
}
loconfig.tasks.styles.forEach(async ({
infile,
outfile
outfile,
label = null
}) => {
const name = basename((outfile || 'undefined'), '.css');
const filestem = basename((outfile || 'undefined'), '.css');
const timeLabel = `${name}.css compiled in`;
const timeLabel = `${label || `${filestem}.css`} compiled in`;
console.time(timeLabel);
try {
infile = template(infile);
outfile = template(outfile);
infile = resolve(infile);
outfile = resolve(outfile);
let result = await sassRender({
let result = await sassRender(Object.assign({}, sassOptions, {
file: infile,
omitSourceMapUrl: true,
outFile: outfile,
outputStyle: 'compressed',
sourceMap: true,
sourceMapContents: true
});
}));
if (postcss) {
result = await postcss.process(result.css, {
from: outfile,
to: outfile,
map: {
annotation: false,
inline: false,
sourcesContent: true
}
});
if (postcss && postcssOptions) {
if (typeof postcssProcessor === 'undefined') {
postcssProcessor = createPostCSSProcessor(
postcssPluginsMap,
postcssOptions
);
}
result = await postcssProcessor.process(
result.css,
Object.assign({}, postcssOptions.processor, {
from: outfile,
to: outfile,
})
);
if (result.warnings) {
const warnings = result.warnings();
if (warnings.length) {
message(`Error processing ${name}.css`, 'warning');
message(`Error processing ${label || `${filestem}.css`}`, 'warning');
warnings.forEach((warn) => {
message(warn.toString());
});
@@ -65,17 +145,17 @@ export default async function compileStyles() {
await writeFile(outfile, result.css);
if (result.css) {
message(`${name}.css compiled`, 'success', timeLabel);
message(`${label || `${filestem}.css`} compiled`, 'success', timeLabel);
} else {
message(`${name}.css is empty`, 'notice', timeLabel);
message(`${label || `${filestem}.css`} is empty`, 'notice', timeLabel);
}
} catch (err) {
message(`Error compiling ${name}.css`, 'error');
message(`Error compiling ${label || `${filestem}.css`}`, 'error');
message(err);
notification({
title: `${name}.css save failed 🚨`,
message: `Could not save stylesheet to ${name}.css`
title: `${label || `${filestem}.css`} save failed 🚨`,
message: `Could not save stylesheet to ${label || `${filestem}.css`}`
});
}
@@ -83,23 +163,52 @@ export default async function compileStyles() {
try {
await writeFile(outfile + '.map', result.map.toString());
} catch (err) {
message(`Error compiling ${name}.css.map`, 'error');
message(`Error compiling ${label || `${filestem}.css.map`}`, 'error');
message(err);
notification({
title: `${name}.css.map save failed 🚨`,
message: `Could not save sourcemap to ${name}.css.map`
title: `${label || `${filestem}.css.map`} save failed 🚨`,
message: `Could not save sourcemap to ${label || `${filestem}.css.map`}`
});
}
}
} catch (err) {
message(`Error compiling ${name}.scss`, 'error');
message(`Error compiling ${label || `${filestem}.scss`}`, 'error');
message(err.formatted || err);
notification({
title: `${name}.scss compilation failed 🚨`,
title: `${label || `${filestem}.scss`} compilation failed 🚨`,
message: (err.formatted || `${err.name}: ${err.message}`)
});
}
});
};
/**
* 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);
}

View File

@@ -1,45 +1,79 @@
import loconfig from '../../loconfig.json';
import loconfig from '../utils/config.js';
import message from '../utils/message.js';
import notification from '../utils/notification.js';
import template from '../utils/template.js';
import resolve from '../utils/template.js';
import { basename } from 'node:path';
import mixer from 'svg-mixer';
/**
* @const {object} defaultMixerOptions - The default shared Mixer options.
* @const {object} developmentMixerOptions - The predefined Mixer options for development.
* @const {object} productionMixerOptions - The predefined Mixer options for production.
*/
export const defaultMixerOptions = {
spriteConfig: {
usages: false,
},
};
export const developmentMixerOptions = Object.assign({}, defaultMixerOptions);
export const productionMixerOptions = Object.assign({}, defaultMixerOptions);
/**
* @const {object} developmentSVGsArgs - The predefined `compileSVGs()` options for development.
* @const {object} productionSVGsArgs - The predefined `compileSVGs()` options for production.
*/
export const developmentSVGsArgs = [
developmentMixerOptions,
];
export const productionSVGsArgs = [
productionMixerOptions,
];
/**
* Generates and transforms SVG spritesheets.
*
* @async
* @param {object} [mixerOptions=null] - Customize the Mixer API options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function compileSVGs() {
export default async function compileSVGs(mixerOptions = null) {
if (mixerOptions == null) {
mixerOptions = productionMixerOptions;
} else if (
mixerOptions !== developmentMixerOptions &&
mixerOptions !== productionMixerOptions
) {
mixerOptions = Object.assign({}, defaultMixerOptions, mixerOptions);
}
loconfig.tasks.svgs.forEach(async ({
includes,
outfile
outfile,
label = null
}) => {
const filename = basename(outfile || 'undefined');
if (!label) {
label = basename(outfile || 'undefined');
}
const timeLabel = `${filename} compiled in`;
const timeLabel = `${label} compiled in`;
console.time(timeLabel);
try {
includes = includes.map((path) => template(path));
outfile = template(outfile);
includes = resolve(includes);
outfile = resolve(outfile);
const result = await mixer(includes, {
spriteConfig: {
usages: false
}
});
const result = await mixer(includes, mixerOptions);
await result.write(outfile);
message(`${filename} compiled`, 'success', timeLabel);
message(`${label} compiled`, 'success', timeLabel);
} catch (err) {
message(`Error compiling ${filename}`, 'error');
message(`Error compiling ${label}`, 'error');
message(err);
notification({
title: `${filename} compilation failed 🚨`,
title: `${label} compilation failed 🚨`,
message: `${err.name}: ${err.message}`
});
}

64
build/utils/config.js Normal file
View File

@@ -0,0 +1,64 @@
/**
* @file Provides simple user configuration options.
*/
import loconfig from '../../loconfig.json';
let usrconfig;
try {
usrconfig = await import('../../loconfig.local.json');
usrconfig = usrconfig.default;
merge(loconfig, usrconfig);
} catch (err) {
// do nothing
}
export default loconfig;
/**
* Creates a new object with all nested object properties
* merged into it recursively.
*
* @param {object} target - The target object.
* @param {object[]} ...sources - The source object(s).
* @throws {TypeError} If the target and source are the same.
* @return {object} Returns the `target` object.
*/
export function merge(target, ...sources) {
for (const source of sources) {
if (target === source) {
throw new TypeError(
'Cannot merge, target and source are the same'
);
}
for (const key in source) {
if (source[key] != null) {
if (isObjectLike(source[key]) && isObjectLike(target[key])) {
merge(target[key], source[key]);
continue;
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
target[key] = target[key].concat(source[key]);
continue;
}
}
target[key] = source[key];
}
}
return target;
}
/**
* Determines whether the passed value is an `Object`.
*
* @param {*} value - The value to be checked.
* @return {boolean} Returns `true` if the value is an `Object`,
* otherwise `false`.
*/
function isObjectLike(value) {
return (value != null && typeof value === 'object');
}

View File

@@ -4,8 +4,9 @@
* Note that options vary between libraries.
*
* Candidates:
*
* - {@link https://npmjs.com/package/tiny-glob tiny-glob}
* - {@link https://npmjs.com/package/globby}
* - {@link https://npmjs.com/package/globby globby}
* - {@link https://npmjs.com/package/fast-glob fast-glob}
* - {@link https://npmjs.com/package/glob glob}
*/
@@ -22,7 +23,15 @@ const candidates = [
'glob',
];
export default await importGlob();
let glob;
try {
glob = await importGlob();
} catch (err) {
// do nothing
}
export default glob;
/**
* Imports the first available glob function.
@@ -63,7 +72,7 @@ async function importGlob() {
}
throw new TypeError(
`No glob library was found, expected one of: ${modules.join(', ')}`
`No glob library was found, expected one of: ${candidates.join(', ')}`
);
}

View File

@@ -1,3 +1,7 @@
/**
* @file Provides a decorator for cross-platform notification.
*/
import notifier from 'node-notifier';
/**

View File

@@ -1,14 +1,27 @@
/**
* @file If available, returns the PostCSS processor with any plugins.
* @file If available, returns the PostCSS Processor creator and
* any the Autoprefixer PostCSS plugin.
*/
try {
var { default: postcss } = await import('postcss');
let { default: autoprefixer } = await import('autoprefixer');
let postcss, autoprefixer;
postcss = postcss([ autoprefixer ]);
try {
postcss = await import('postcss');
postcss = postcss.default;
autoprefixer = await import('autoprefixer');
autoprefixer = autoprefixer.default;
} catch (err) {
postcss = null;
// do nothing
}
export default postcss;
export const pluginsList = [
autoprefixer,
];
export const pluginsMap = {
'autoprefixer': autoprefixer,
};
export {
autoprefixer
};

View File

@@ -2,7 +2,7 @@
* @file Provides simple template tags.
*/
import loconfig from '../../loconfig.json';
import loconfig from './config.js';
const templateData = flatten({
paths: loconfig.paths
@@ -14,17 +14,53 @@ const templateData = flatten({
* If replacement pairs contain a mix of substrings, regular expressions,
* and functions, regular expressions are executed last.
*
* @param {string} input - The string being searched and replaced on.
* @param {object} data - An object in the form `{ 'from': 'to', … }`.
* @param {*} input - The value being searched and replaced on.
* If input is, or contains, a string, tags will be resolved.
* If input is, or contains, an object, it is mutated directly.
* If input is, or contains, an array, a shallow copy is returned.
* Otherwise, the value is left intact.
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
* @return {*} Returns the transformed value.
*/
export default function resolve(input, data = templateData) {
switch (typeof input) {
case 'string': {
return resolveValue(input, data);
}
case 'object': {
if (input == null) {
break;
}
if (Array.isArray(input)) {
return input.map((value) => resolve(value, data));
} else {
for (const key in input) {
input[key] = resolve(input[key], data);
}
}
}
}
return input;
}
/**
* Replaces all template tags in a string from a map of keys and values.
*
* If replacement pairs contain a mix of substrings, regular expressions,
* and functions, regular expressions are executed last.
*
* @param {string} input - The string being searched and replaced on.
* @param {object} [data] - An object in the form `{ 'from': 'to', … }`.
* @return {string} Returns the translated string.
*/
export default function template(input, data) {
export function resolveValue(input, data = templateData) {
const tags = [];
if (data) {
if (data !== templateData) {
data = flatten(data);
} else {
data = templateData;
}
for (let tag in data) {
@@ -55,7 +91,7 @@ export default function template(input, data) {
return '';
});
};
}
/**
* Creates a new object with all nested object properties

View File

@@ -1,82 +1,195 @@
import loconfig from '../loconfig.json';
import concatFiles from './tasks/concats.js';
import compileScripts from './tasks/scripts.js';
import compileStyles from './tasks/styles.js' ;
import compileSVGs from './tasks/svgs.js';
import template from './utils/template.js';
import server from 'browser-sync';
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 browserSync from 'browser-sync';
import { join } from 'node:path';
const { paths, tasks } = loconfig;
const serverConfig = {
open: false,
notify: false
};
if (typeof paths.url === 'string' && paths.url.length > 0) {
// Use proxy
serverConfig.proxy = paths.url;
} else {
// Use base directory
serverConfig.server = {
baseDir: paths.dest
};
}
// Start the Browsersync server
server.init(serverConfig);
// Match a URL protocol.
const regexUrlStartsWithProtocol = /^[a-z0-9\-]:\/\//i;
// Build scripts, compile styles, concat files,
// and generate spritesheets on first hit
concatFiles();
compileScripts();
compileStyles();
compileSVGs();
concatFiles(...developmentConcatFilesArgs);
compileScripts(...developmentScriptsArgs);
compileStyles(...developmentStylesArgs);
compileSVGs(...developmentSVGsArgs);
// and call any methods on it.
server.watch(
[
paths.views.src,
join(paths.scripts.dest, '*.js'),
join(paths.styles.dest, '*.css'),
join(paths.svgs.dest, '*.svg'),
]
).on('change', server.reload);
// Create a new BrowserSync instance
const server = browserSync.create();
// Watch scripts
server.watch(
[
join(paths.scripts.src, '**/*.js'),
]
).on('change', () => {
compileScripts();
// Start the BrowserSync server
server.init(createServerOptions(loconfig), (err) => {
if (err) {
message('Error starting development server', 'error');
message(err);
notification({
title: 'Development server failed',
message: `${err.name}: ${err.message}`
});
}
});
// Watch concats
server.watch(
tasks.concats.reduce(
(patterns, { includes }) => patterns.concat(includes),
[]
).map((path) => template(path))
).on('change', () => {
concatFiles();
});
configureServer(server, loconfig);
// Watch styles
server.watch(
[
join(paths.styles.src, '**/*.scss'),
]
).on('change', () => {
compileStyles();
});
/**
* Configures the BrowserSync options.
*
* @param {BrowserSync} server - The BrowserSync API.
* @param {object} loconfig - The project configset.
* @param {object} loconfig.paths - The paths options.
* @param {object} loconfig.tasks - The tasks options.
* @return {void}
*/
function configureServer(server, { paths, tasks }) {
const views = createViewsArray(paths.views);
// Watch svgs
server.watch(
[
join(paths.svgs.src, '*.svg'),
]
).on('change', () => {
compileSVGs();
});
// Reload on any changes to views or processed files
server.watch(
[
...views,
join(paths.scripts.dest, '*.js'),
join(paths.styles.dest, '*.css'),
join(paths.svgs.dest, '*.svg'),
]
).on('change', server.reload);
// Watch source scripts
server.watch(
[
join(paths.scripts.src, '**/*.js'),
]
).on('change', () => {
compileScripts(...developmentScriptsArgs);
});
// Watch source concats
server.watch(
resolve(
tasks.concats.reduce(
(patterns, { includes }) => patterns.concat(includes),
[]
)
)
).on('change', () => {
concatFiles(...developmentConcatFilesArgs);
});
// Watch source styles
server.watch(
[
join(paths.styles.src, '**/*.scss'),
]
).on('change', () => {
compileStyles(...developmentStylesArgs);
});
// Watch source SVGs
server.watch(
[
join(paths.svgs.src, '*.svg'),
]
).on('change', () => {
compileSVGs(...developmentSVGsArgs);
});
}
/**
* Creates a new object with all the BrowserSync options.
*
* @param {object} loconfig - The project configset.
* @param {object} loconfig.paths - The paths options.
* @param {object} loconfig.server - The server options.
* @return {object} Returns the server options.
*/
function createServerOptions({
paths,
server: options
}) {
const config = {
open: false,
notify: false
};
// Resolve the URL for the BrowserSync server
if (isNonEmptyString(paths.url)) {
// Use proxy
config.proxy = paths.url;
} else if (isNonEmptyString(paths.dest)) {
// Use base directory
config.server = {
baseDir: paths.dest
};
}
merge(config, resolve(options));
// If HTTPS is enabled, prepend `https://` to proxy URL
if (options?.https) {
if (isNonEmptyString(config.proxy?.target)) {
config.proxy.target = prependSchemeToUrl(config.proxy.target, 'https');
} else if (isNonEmptyString(config.proxy)) {
config.proxy = prependSchemeToUrl(config.proxy, 'https');
}
}
return config;
}
/**
* Creates a new array (shallow-copied) from the views configset.
*
* @param {*} views - The views configset.
* @throws {TypeError} If views is invalid.
* @return {array} Returns the views array.
*/
function createViewsArray(views) {
if (Array.isArray(views)) {
return Array.from(views);
}
switch (typeof views) {
case 'string':
return [ views ];
case 'object':
if (views != null) {
return Object.values(views);
}
}
throw new TypeError(
'Expected \'views\' to be a string, array, or object'
);
}
/**
* Prepends the scheme to the URL.
*
* @param {string} url - The URL to mutate.
* @param {string} [scheme] - The URL scheme to prepend.
* @return {string} Returns the mutated URL.
*/
function prependSchemeToUrl(url, scheme = 'http') {
if (regexUrlStartsWithProtocol.test(url)) {
return url.replace(regexUrlStartsWithProtocol, `${scheme}://`);
}
return `${scheme}://${url}`;
}
/**
* Determines whether the passed value is a string with at least one character.
*
* @param {*} value - The value to be checked.
* @return {boolean} Returns `true` if the value is a non-empty string,
* otherwise `false`.
*/
function isNonEmptyString(value) {
return (typeof value === 'string' && value.length > 0);
}

344
docs/development.md Normal file
View File

@@ -0,0 +1,344 @@
# Development
* [Installation](#installation)
* [Usage](#usage)
* [Configuration](#configuration)
* [Environment Configuration](#environment-configuration)
* [Development Configuration](#development-configuration)
* [`paths` option](#paths-option)
* [`paths.url` option](#pathsurl-option)
* [`paths.dest` option](#pathsdest-option)
* [`tasks` option](#tasks-option)
* [`server` option](#server-option)
* [Tasks](#tasks)
* [`concats`](#concats)
* [`scripts`](#scripts)
* [`styles`](#styles)
* [`svgs`](#svgs)
---
The boilerplate provides a custom, easily configured, and very simple,
task runner for [Node] to process assets and test quickly in browsers.
Learn more about the boilerplate's [tasks](#tasks) below.
## Installation
Make sure you have the following installed:
* [Node] — at least 14.17, the latest LTS is recommended.
* [NPM] — at least 6.0, the latest LTS is recommended.
```sh
# Switch to recommended Node version from .nvmrc
nvm use
# Install dependencies from package.json
npm install
```
## Usage
```sh
# Start development server, watch for changes, and compile assets
npm start
# Compile and minify assets
npm run build
```
See [`build.js`](../build/build.js) and [`watch.js`](../build/watch.js)
for details.
## Configuration
For development, most configuration values for processing front-end assets
are defined in the [`loconfig.json`](../loconfig.json) file that exists at
the root directory of your project.
### Environment Configuration
If any configuration options vary depending on whether your project is
running on your computer, a collaborator's computer, or on a web server,
these values should be stored in a `loconfig.local.json` file.
In fresh copy of the boilerplate, the root directory of your project
will contain a [`loconfig.example.json`](../loconfig.example.json) file.
> 💡 The boilerplate's default example customizes the development server
> to use a custom SSL certificate.
That file can be copied to `loconfig.local.json` and customized to suit
your local environment.
Your `loconfig.local.json` _should not_ be committed to your project's
source control.
> 💡 If you are developing with a team, you may wish to continue
> including a `loconfig.example.json` file with your project.
### Development Configuration
The boilerplate provides a few configuration settings to control the
behaviour for processing front-end assets.
#### `paths` option
The `paths` option defines URIs and file paths.
It is primarily used for template tags to reference any configuration
properties to reduce repetition.
Template tags are specified using `{% %}` delimiters. They will be
automatically expanded when tasks process paths.
```jsonc
{
"paths": {
"styles": {
"src": "./assets/styles",
"dest": "./www/assets/styles"
}
},
"tasks": {
"styles": [
{
"infile": "{% paths.styles.src %}/main.scss", // → ./assets/styles/main.scss
"outfile": "{% paths.styles.dest %}/main.css" // → ./www/assets/styles/main.css
}
]
}
}
```
#### `paths.url` option
The `paths.url` option defines the base URI of the project.
By default, it is used by the development server as a proxy
for an existing virtual host.
```json
{
"paths": {
"url": "locomotive-boilerplate.test"
}
}
```
#### `paths.dest` option
The `paths.dest` option defines the public web directory of the project.
By default, it is used by the development server as the base directory
to serve the website from if a proxy URI is not provided.
```json
{
"paths": {
"dest": "./www"
}
}
```
#### `tasks` option
Which assets and how they should be processed can be configured via
the `tasks` option:
```json
{
"tasks": {
"scripts": [
{
"includes": [
"./assets/scripts/app.js"
],
"outfile": "./www/assets/scripts/app.js"
}
],
"styles": [
{
"infile": "./assets/styles/main.scss",
"outfile": "./www/assets/styles/main.css"
}
]
}
}
```
See [tasks](#tasks) section, below, for details.
#### `server` option
The development server (BrowserSync) can be configured via
the `server` option:
```json
{
"server": {
"open": true,
"https": {
"key": "~/.config/valet/Certificates/{% paths.url %}.key",
"cert": "~/.config/valet/Certificates/{% paths.url %}.crt"
}
}
}
```
Visit [BrowserSync's documentation](https://browsersync.io/docs/options)
for all options.
## Tasks
The boilerplate provides a handful of tasks for handling
the most commonly processed assets.
### `concats`
A wrapper around [concat] (with optional support for globbing) for concatenating multiple files.
By default, [tiny-glob] is installed with the boilerplate.
Example:
```json
{
"concats": [
{
"label": "Application Vendors",
"includes": [
"{% paths.scripts.src %}/vendors/*.js",
"node_modules/focus-visible/dist/focus-visible.min.js",
"node_modules/vue/dist/vue.min.js",
"node_modules/vuelidate/dist/vuelidate.min.js",
"node_modules/vuelidate/dist/validators.min.js"
],
"outfile": "{% paths.scripts.dest %}/app/vendors.js"
},
{
"label": "Public Site Vendors",
"includes": [
"{% paths.scripts.src %}/vendors/*.js",
"node_modules/focus-visible/dist/focus-visible.min.js"
],
"outfile": "{% paths.scripts.dest %}/site/vendors.js"
}
]
}
```
See [`concats.js`](../build/tasks/concats.js) for details.
### `scripts`
A wrapper around [esbuild] for bundling and minifying modern JS/ES modules.
Example:
```json
{
"scripts": [
{
"label": "Application Dashboard JS",
"includes": [
"{% paths.scripts.src %}/app/dashboard.js"
],
"outfile": "{% paths.scripts.dest %}/app/dashboard.js"
},
{
"label": "Public Site JS",
"includes": [
"{% paths.scripts.src %}/site/main.js"
],
"outfile": "{% paths.scripts.dest %}/site/main.js"
}
]
}
```
See [`scripts.js`](../build/tasks/scripts.js) for details.
### `styles`
A wrapper around [node-sass] (and optionally [Autoprefixer] via [PostCSS])
for compiling and minifying Sass into CSS.
By default, [PostCSS] and [Autoprefixer] are installed with the boilerplate.
Example:
```json
{
"styles": [
{
"label": "Text Editor CSS",
"infile": "{% paths.styles.src %}/app/editor.scss",
"outfile": "{% paths.styles.dest %}/app/editor.css"
},
{
"label": "Application Dashboard CSS",
"infile": "{% paths.styles.src %}/app/dashboard.scss",
"outfile": "{% paths.styles.dest %}/app/dashboard.css"
},
{
"label": "Public Site Critical CSS",
"infile": "{% paths.styles.src %}/site/critical.scss",
"outfile": "{% paths.styles.dest %}/site/critical.css"
},
{
"label": "Public Site CSS",
"infile": "{% paths.styles.src %}/site/main.scss",
"outfile": "{% paths.styles.dest %}/site/main.css"
}
]
}
```
See [`styles.js`](../build/tasks/styles.js) for details.
### `svgs`
A wrapper around [SVG Mixer] for transforming and minifying SVG files
and generating spritesheets.
Example:
```json
{
"svgs": [
{
"label": "Application Spritesheet",
"includes": [
"{% paths.images.src %}/app/*.svg"
],
"outfile": "{% paths.svgs.dest %}/app/sprite.svg"
},
{
"label": "Public Site Spritesheet",
"includes": [
"{% paths.images.src %}/site/*.svg"
],
"outfile": "{% paths.svgs.dest %}/site/sprite.svg"
}
]
}
```
See [`svgs.js`](../build/tasks/svgs.js) for details.
[Autoprefixer]: https://npmjs.com/package/autoprefixer
[BrowserSync]: https://npmjs.com/package/browser-sync
[concat]: https://npmjs.com/package/concat
[esbuild]: https://npmjs.com/package/esbuild
[fast-glob]: https://npmjs.com/package/fast-glob
[glob]: https://npmjs.com/package/glob
[globby]: https://npmjs.com/package/globby
[Node]: https://nodejs.org/
[node-sass]: https://npmjs.com/package/node-sass
[NPM]: https://npmjs.com/
[NVM]: https://github.com/nvm-sh/nvm
[PostCSS]: https://npmjs.com/package/postcss
[SVG Mixer]: https://npmjs.com/package/svg-mixer
[tiny-glob]: https://npmjs.com/package/tiny-glob

207
docs/technologies.md Normal file
View File

@@ -0,0 +1,207 @@
# Technologies
* [Styles](#styles)
* [CSS Architecture](#css-architecture)
* [CSS Naming Convention](#css-naming-convention)
* [CSS Namespacing](#css-namespacing)
* [Example](#example-1)
* [Scripts](#scripts)
* [Example](#example-2)
* [Page transitions](#page-transitions)
* [Example](#example-3)
* [Scroll detection](#scroll-detection)
* [Example](#example-4)
## Styles
[SCSS][Sass] is a superset of CSS that adds many helpful features to improve
and modularize our styles.
We use [node-sass] (LibSass) for processing and minifying SCSS into CSS.
We also use [PostCSS] and [Autoprefixer] to parse our CSS and add
vendor prefixes for experimental features.
### CSS Architecture
The boilerplate's CSS architecture is based on [Inuit CSS][inuitcss] and [ITCSS].
* `settings`: Global variables, site-wide settings, config switches, etc.
* `tools`: Site-wide mixins and functions.
* `generic`: Low-specificity, far-reaching rulesets (e.g. resets).
* `elements`: Unclassed HTML elements (e.g. `a {}`, `blockquote {}`, `address {}`).
* `objects`: Objects, abstractions, and design patterns (e.g. `.o-layout {}`).
* `components`: Discrete, complete chunks of UI (e.g. `.c-carousel {}`).
* `utilities`: High-specificity, very explicit selectors. Overrides and helper
classes (e.g. `.u-hidden {}`)
Learn more about [Inuit CSS](https://github.com/inuitcss/inuitcss#css-directory-structure).
### CSS Naming Convention
We use a simplified [BEM] (Block, Element, Modifier) syntax:
* `.block`
* `.block_element`
* `.-modifier`
### CSS Namespacing
We namespace our classes for more UI transparency:
* `o-`: Object that it may be used in any number of unrelated contexts to the one you can currently see it in. Making modifications to these types of class could potentially have knock-on effects in a lot of other unrelated places.
* `c-`: Component is a concrete, implementation-specific piece of UI. All of the changes you make to its styles should be detectable in the context youre currently looking at. Modifying these styles should be safe and have no side effects.
* `u-`: Utility has a very specific role (often providing only one declaration) and should not be bound onto or changed. It can be reused and is not tied to any specific piece of UI.
* `s-`: Scope creates a new styling context. Similar to a Theme, but not necessarily cosmetic, these should be used sparingly—they can be open to abuse and lead to poor CSS if not used wisely.
* `is-`, `has-`: Is currently styled a certain way because of a state or condition. It tells us that the DOM currently has a temporary, optional, or short-lived style applied to it due to a certain state being invoked.
Learn about [namespacing](https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/).
### Example \#1
```html
<div class="c-block -large">
<div class="c-block_layout o-layout">
<div class="o-layout_item u-1/2@from-medium">
<div class="c-block_heading o-h -medium">Heading</div>
</div>
<div class="o-layout_item u-1/2@from-medium">
<a class="c-block_button o-button -outline" href="#">Button</a>
</div>
</div>
</div>
```
```scss
.c-block {
&.-large {
padding: rem(60px);
}
}
.c-block_heading {
@media (max-width: $to-medium) {
.c-block.-large & {
margin-bottom: rem(40px);
}
}
}
```
## Scripts
We use [esbuild] for bundling and minifying JavaScript/ES modules.
[modularJS] is a small framework we use on top of ES modules.
* Automatically init visible modules.
* Easily call other modules methods.
* Quickly set scoped events with delegation.
* Simply select DOM elements scoped in their module.
[_source_](https://npmjs.com/package/modujs#why)
### Example \#2
```html
<div data-module-example>
<div data-example="main">
<h2>Example</h2>
</div>
<button data-example="load">More</button>
</div>
```
```js
import { module } from 'modujs';
export default class extends module {
constructor(m) {
super(m);
this.events = {
click: {
load: 'loadMore'
}
};
}
loadMore() {
this.$('main')[0].classList.add('is-loading');
}
}
```
Learn more about [modularJS].
## Page transitions
[modularLoad] is used for page transitions and lazy loading.
### Example \#3
```html
<nav>
<a href="/">Home</a>
<a href="/page" data-load="transitionName">Page</a>
</nav>
<div data-load-container>
<img data-load-src="assets/images/hello.jpg">
</div>
```
```js
import modularLoad from 'modularload';
this.load = new modularLoad({
enterDelay: 300,
transitions: {
transitionName: {
enterDelay: 450
}
}
});
```
Learn more about [modularLoad].
## Scroll detection
[Locomotive Scroll][locomotive-scroll] is used for elements in viewport
detection and smooth scrolling with parallax.
### Example \#4
```html
<div data-module-scroll>
<div data-scroll>Trigger</div>
<div data-scroll data-scroll-speed="1">Parallax</div>
</div>
```
```js
import LocomotiveScroll from 'locomotive-scroll';
this.scroll = new LocomotiveScroll({
el: this.el,
smooth: true
});
````
Learn more about [Locomotive Scroll][locomotive-scroll].
[Autoprefixer]: https://npmjs.com/package/autoprefixer
[BEM]: https://bem.info/
[BrowserSync]: https://npmjs.com/package/browser-sync
[esbuild]: https://npmjs.com/package/esbuild
[inuitcss]: https://github.com/inuitcss/inuitcss
[ITCSS]: https://itcss.io/
[locomotive-scroll]: https://npmjs.com/package/locomotive-scroll
[modularJS]: https://npmjs.com/package/modujs
[modularLoad]: https://npmjs.com/package/modularload
[node-sass]: https://npmjs.com/package/node-sass
[PostCSS]: https://npmjs.com/package/postcss
[Sass]: https://sass-lang.com/
[svg-mixer]: https://npmjs.com/package/svg-mixer
[Node]: https://nodejs.org/
[NPM]: https://npmjs.com/
[NVM]: https://github.com/nvm-sh/nvm

8
loconfig.example.json Executable file
View File

@@ -0,0 +1,8 @@
{
"server": {
"https": {
"key": "~/.config/valet/Certificates/{% paths.url %}.key",
"cert": "~/.config/valet/Certificates/{% paths.url %}.crt"
}
}
}

4685
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,21 +14,21 @@
"build": "node --experimental-json-modules --no-warnings build/build.js"
},
"dependencies": {
"locomotive-scroll": "^4.1.3",
"locomotive-scroll": "^4.1.4",
"modujs": "^1.4.2",
"modularload": "^1.2.6",
"normalize.css": "^8.0.1",
"svg4everybody": "^2.1.9"
},
"devDependencies": {
"autoprefixer": "^10.3.7",
"browser-sync": "^2.26.13",
"autoprefixer": "^10.4.4",
"browser-sync": "^2.27.9",
"concat": "^1.0.3",
"esbuild": "^0.13.4",
"esbuild": "^0.14.27",
"kleur": "^4.1.4",
"node-notifier": "^10.0.0",
"node-sass": "^6.0.1",
"postcss": "^8.3.9",
"node-notifier": "^10.0.1",
"node-sass": "^7.0.1",
"postcss": "^8.4.12",
"svg-mixer": "^2.3.14",
"tiny-glob": "^0.2.9"
}

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

View File

@@ -31,7 +31,7 @@
<main data-scroll-section>
<div class="o-container">
<h1 class="c-heading -h1">Page</h1>
<h1 class="t-h1">Page</h1>
<div class="o-layout">
<div class="o-layout_item u-1/2@from-small">

View File

@@ -31,19 +31,19 @@
<main data-scroll-section>
<div class="o-container">
<h1 class="c-heading -h1">Images</h1>
<h1 class="t-h1">Images</h1>
<section>
<h2 class="c-heading -h2">Lazy load demo</h2>
<h2 class="t-h2">Lazy load demo</h2>
<h3 class="c-heading -h3">Basic</h3>
<h3 class="t-h3">Basic</h3>
<div style="width: 640px; max-width: 100%;">
<div class="o-ratio u-4:3"><img data-load-src="http://picsum.photos/640/480?v=1" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" /></div>
<div class="o-ratio u-4:3"><img data-load-src="http://picsum.photos/640/480?v=2" alt="" src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" /></div>
</div>
<h4 class="c-heading -h3">Using o-ratio & background-image</h3>
<h4 class="t-h3">Using o-ratio & background-image</h3>
<div style="width: 480px; max-width: 100%;">
<div class="o-ratio u-16:9" data-load-style="background-size: cover; background-position: center; background-image: url(http://picsum.photos/640/480?v=1);"></div>
@@ -52,9 +52,9 @@
</section>
<section>
<h3 class="c-heading -h3">Relative to scroll</h3>
<h3 class="t-h3">Relative to scroll</h3>
<h4 class="c-heading -h3">Using o-ratio & img</h3>
<h4 class="t-h3">Using o-ratio & img</h3>
<div style="width: 640px; max-width: 100%;">
<div class="o-ratio u-4:3">
@@ -74,7 +74,7 @@
</div>
</div>
<h4 class="c-heading -h3">Using o-ratio & background-image</h3>
<h4 class="t-h3">Using o-ratio & background-image</h3>
<div style="width: 480px; max-width: 100%;">
<div style="background-size: cover; background-position: center;" class="o-ratio u-16:9" data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/640/480?v=1"></div>
@@ -84,7 +84,7 @@
<div style="background-size: cover; background-position: center;" class="o-ratio u-16:9" data-scroll data-scroll-call="lazyLoad, Scroll, main" data-src="http://picsum.photos/640/480?v=5"></div>
</div>
<h4 class="c-heading -h3">Using SVG viewport for ratio</h3>
<h4 class="t-h3">Using SVG viewport for ratio</h3>
<div style="width: 480px; max-width: 100%;">
<img

View File

@@ -50,7 +50,17 @@
<main data-scroll-section>
<div class="o-container">
<h1 class="c-heading -h1">Hello</h1>
<h1 class="t-h1">Hello</h1>
<div class="t-wysiwyg">
<h2>Crucifix de charrue de cibouleau.</h2>
<p>Cossin de ciarge de sacristi de calvaire de <strong>bâtard de caltor</strong> de mosus d'étole de boswell de sacrament de sapristi de gériboire de Jésus de plâtre.</p>
<ul>
<li>Viande à chien de saint-cimonaque.</li>
<li>Sacristi de calvince de charogne de saint-ciarge.</li>
</ul>
<h3>Calvaire de gériboire de saintes fesses.</h3>
<p>Viande à chien de boswell de sacristi de patente à gosse de mangeux d'marde de <i>Jésus de plâtre de gériboire de purée de charrue</i> de christie de torrieux de cimonaque de saint-ciarge de bâtard de maudite marde de cochonnerie.</p>
</div>
</div>
</main>