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

Start refactor boilerplate

This commit is contained in:
Deven Caron
2024-10-24 13:25:19 -04:00
parent b4ee0955c3
commit 989d8539ef
110 changed files with 1197 additions and 7085 deletions

View File

@@ -1 +0,0 @@
defaults

8
.gitignore vendored
View File

@@ -5,9 +5,7 @@ loconfig.*.json
!loconfig.example.json
.prettierrc
www/assets/scripts/*
!www/assets/scripts/.gitkeep
www/assets/styles/*
!www/assets/styles/.gitkeep
assets.json
dist
www
.env

View File

@@ -1,11 +0,0 @@
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 bumpVersions from './tasks/versions.js';
concatFiles();
compileScripts();
compileStyles();
compileSVGs();
bumpVersions();

View File

@@ -1,25 +0,0 @@
/**
* @file Provides simple user configuration options.
*/
import loconfig from '../../loconfig.json' with { type: 'json' };
import { merge } from '../utils/index.js';
let usrconfig;
try {
usrconfig = await import('../../loconfig.local.json', {
with: { type: 'json' },
});
usrconfig = usrconfig.default;
merge(loconfig, usrconfig);
} catch (err) {
// do nothing
}
export default loconfig;
export {
loconfig,
};

View File

@@ -1,162 +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} [1][5][6]
* - {@link https://npmjs.com/package/globby globby} [2][5]
* - {@link https://npmjs.com/package/fast-glob fast-glob} [3]
* - {@link https://npmjs.com/package/glob glob} [1][4][5]
*
* Notes:
*
* - [1] The library's function accepts only a single pattern.
* - [2] The library's function accepts only an array of patterns.
* - [3] The library's function accepts either a single pattern
* or an array of patterns.
* - [4] The library's function does not return a Promise but will be
* wrapped in a function that does return a Promise.
* - [5] The library's function will be wrapped in a function that
* supports a single pattern and an array of patterns.
* - [6] The library's function returns files and directories but will be
* preconfigured to return only files.
*/
import { promisify } from 'node:util';
/**
* @callback GlobFn
*
* @param {string|string[]} patterns - A string pattern
* or an array of string patterns.
* @param {object} options
*
* @returns {Promise<string[]>}
*/
/**
* @typedef {object} GlobOptions
*/
/**
* @type {GlobFn|undefined} The discovered glob function.
*/
let glob;
/**
* @type {string[]} A list of packages to attempt import.
*/
const candidates = [
'tiny-glob',
'globby',
'fast-glob',
'glob',
];
try {
glob = await importGlob();
} catch (err) {
// do nothing
}
/**
* @type {boolean} Whether a glob function was discovered (TRUE) or not (FALSE).
*/
const supportsGlob = (typeof glob === 'function');
/**
* Imports the first available glob function.
*
* @throws {TypeError} If no glob library was found.
*
* @returns {GlobFn}
*/
async function importGlob() {
for (let name of candidates) {
try {
let globModule = await import(name);
if (typeof globModule.default !== 'function') {
throw new TypeError(`Expected ${name} to be a function`);
}
/**
* Wrap the function to ensure
* a common pattern.
*/
switch (name) {
case 'tiny-glob':
/** [1][5] */
return createArrayableGlob(
/** [6] */
createPresetGlob(globModule.default, {
filesOnly: true
})
);
case 'globby':
/** [2][5] - If `patterns` is a string, wraps into an array. */
return (patterns, options) => globModule.default([].concat(patterns), options);
case 'glob':
/** [1][5] */
return createArrayableGlob(
/** [4] */
promisify(globModule.default)
);
default:
return globModule.default;
}
} catch (err) {
// swallow this error; skip to the next candidate.
}
}
throw new TypeError(
`No glob library was found, expected one of: ${candidates.join(', ')}`
);
}
/**
* Creates a wrapper function for the glob function
* to provide support for arrays of patterns.
*
* @param {function} globFn - The glob function.
*
* @returns {GlobFn}
*/
function createArrayableGlob(globFn) {
return (patterns, options) => {
/** [2] If `patterns` is a string, wraps into an array. */
patterns = [].concat(patterns);
const globs = patterns.map((pattern) => globFn(pattern, options));
return Promise.all(globs).then((files) => {
return [].concat.apply([], files);
});
};
}
/**
* Creates a wrapper function for the glob function
* to define new default options.
*
* @param {function} globFn - The glob function.
* @param {GlobOptions} presets - The glob function options to preset.
*
* @returns {GlobFn}
*/
function createPresetGlob(globFn, presets) {
return (patterns, options) => globFn(patterns, Object.assign({}, presets, options));
}
export default glob;
export {
glob,
supportsGlob,
};

View File

@@ -1,61 +0,0 @@
/**
* @file Provides a decorator for console messages.
*/
import kleur from 'kleur';
/**
* Outputs a message to the console.
*
* @param {string} text - The message to output.
* @param {string} [type] - The type of message.
* @param {string} [timerID] - The console time label to output.
*/
function message(text, type, timerID) {
switch (type) {
case 'success':
console.log('✅ ', kleur.bgGreen().black(text));
break;
case 'chore':
console.log('🧹 ', kleur.bgGreen().black(text));
break;
case 'notice':
console.log(' ', kleur.bgBlue().black(text));
break;
case 'error':
console.log('❌ ', kleur.bgRed().black(text));
break;
case 'warning':
console.log('⚠️ ', kleur.bgYellow().black(text));
break;
case 'waiting':
console.log('⏱ ', kleur.blue().italic(text));
if (timerID != null) {
console.timeLog(timerID);
timerID = null;
}
break;
default:
console.log(text);
break;
}
if (timerID != null) {
console.timeEnd(timerID);
}
console.log('');
}
export default message;
export {
message,
};

View File

@@ -1,51 +0,0 @@
/**
* @file Provides a decorator for cross-platform notification.
*/
import notifier from 'node-notifier';
/**
* Sends a cross-platform native notification.
*
* Wraps around node-notifier to assign default values.
*
* @param {string|object} options - The notification options or a message.
* @param {string} options.title - The notification title.
* @param {string} options.message - The notification message.
* @param {string} options.icon - The notification icon.
* @param {function} callback - The notification callback.
* @return {void}
*/
function notification(options, callback) {
if (typeof options === 'string') {
options = {
message: options
};
} else if (!options.title && !options.message) {
throw new TypeError(
'Notification expects at least a \'message\' parameter'
);
}
if (typeof options.icon === 'undefined') {
options.icon = 'https://user-images.githubusercontent.com/4596862/54868065-c2aea200-4d5e-11e9-9ce3-e0013c15f48c.png';
}
// If notification does not use a callback,
// shorten the wait before timing out.
if (typeof callback === 'undefined') {
if (typeof options.wait === 'undefined') {
if (typeof options.timeout === 'undefined') {
options.timeout = 5;
}
}
}
notifier.notify(options, callback);
}
export default notification;
export {
notification,
};

View File

@@ -1,139 +0,0 @@
/**
* @file If available, returns the PostCSS Processor creator and
* any the Autoprefixer PostCSS plugin.
*/
/**
* @typedef {import('autoprefixer').autoprefixer.Options} AutoprefixerOptions
*/
/**
* @typedef {import('postcss').AcceptedPlugin} AcceptedPlugin
*/
/**
* @typedef {import('postcss').Postcss} Postcss
*/
/**
* @typedef {import('postcss').ProcessOptions} ProcessOptions
*/
/**
* @typedef {import('postcss').Processor} Processor
*/
/**
* @typedef {AcceptedPlugin[]} PluginList
*/
/**
* @typedef {object<string, AcceptedPlugin>} PluginMap
*/
/**
* @typedef {PluginList|PluginMap} PluginCollection
*/
/**
* @typedef {object} PostCSSOptions
*
* @property {ProcessOptions} processor - The `Processor#process()` options.
* @property {AutoprefixerOptions} autoprefixer - The `autoprefixer()` options.
*/
/**
* @type {Postcss|undefined} postcss - The discovered PostCSS function.
* @type {AcceptedPlugin|undefined} autoprefixer - The discovered Autoprefixer function.
*/
let postcss, autoprefixer;
try {
postcss = await import('postcss');
postcss = postcss.default;
autoprefixer = await import('autoprefixer');
autoprefixer = autoprefixer.default;
} catch (err) {
// do nothing
}
/**
* @type {boolean} Whether PostCSS was discovered (TRUE) or not (FALSE).
*/
const supportsPostCSS = (typeof postcss === 'function');
/**
* @type {PluginList} A list of supported plugins.
*/
const pluginsList = [
autoprefixer,
];
/**
* @type {PluginMap} A map of supported plugins.
*/
const pluginsMap = {
'autoprefixer': autoprefixer,
};
/**
* Attempts to create a PostCSS Processor with the given plugins and options.
*
* @param {PluginCollection} pluginsListOrMap - A list or map of plugins.
* If a map of plugins, the plugin name looks up `options`.
* @param {PostCSSOptions} options - The PostCSS wrapper options.
*
* @returns {Processor|null}
*/
function createProcessor(pluginsListOrMap, options)
{
if (!postcss) {
return null;
}
const plugins = parsePlugins(pluginsListOrMap, options);
return postcss(plugins);
}
/**
* Parses the PostCSS plugins and options.
*
* @param {PluginCollection} pluginsListOrMap - A list or map of plugins.
* If a map of plugins, the plugin name looks up `options`.
* @param {PostCSSOptions} options - The PostCSS wrapper options.
*
* @returns {PluginList}
*/
function parsePlugins(pluginsListOrMap, options)
{
if (Array.isArray(pluginsListOrMap)) {
return pluginsListOrMap;
}
/** @type {PluginList} */
const plugins = [];
for (let [ name, plugin ] of Object.entries(pluginsListOrMap)) {
if (name in options) {
plugin = plugin[name](options[name]);
}
plugins.push(plugin);
}
return plugins;
}
export default postcss;
export {
autoprefixer,
createProcessor,
parsePlugins,
pluginsList,
pluginsMap,
postcss,
supportsPostCSS,
};

View File

@@ -1,105 +0,0 @@
/**
* @file Provides simple template tags.
*/
import loconfig from './config.js';
import {
escapeRegExp,
flatten
} from '../utils/index.js';
const templateData = flatten({
paths: loconfig.paths
});
/**
* Replaces all template tags from a map of keys and values.
*
* If replacement pairs contain a mix of substrings, regular expressions,
* and functions, regular expressions are executed last.
*
* @param {*} 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.
*/
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.
*/
function resolveValue(input, data = templateData) {
const tags = [];
if (data !== templateData) {
data = flatten(data);
}
for (let tag in data) {
tags.push(escapeRegExp(tag));
}
if (tags.length === 0) {
return input;
}
const search = new RegExp('\\{%\\s*(' + tags.join('|') + ')\\s*%\\}', 'g');
return input.replace(search, (match, key) => {
let value = data[key];
switch (typeof value) {
case 'function':
/**
* Retrieve the offset of the matched substring `args[0]`
* and the whole string being examined `args[1]`.
*/
let args = Array.prototype.slice.call(arguments, -2);
return value.call(data, match, args[0], args[1]);
case 'string':
case 'number':
return value;
}
return '';
});
}
export default resolve;
export {
resolve,
resolveValue,
};

View File

@@ -1,144 +0,0 @@
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,
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(globOptions = null, concatOptions = null) {
if (supportsGlob) {
if (globOptions == null) {
globOptions = productionGlobOptions;
} else if (
globOptions !== false &&
globOptions !== developmentGlobOptions &&
globOptions !== productionGlobOptions
) {
globOptions = merge({}, defaultGlobOptions, globOptions);
}
}
if (concatOptions == null) {
concatOptions = productionConcatOptions;
} else if (
concatOptions !== developmentConcatOptions &&
concatOptions !== productionConcatOptions
) {
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,
label = null
}) => {
if (!label) {
label = basename(outfile || 'undefined');
}
const timeLabel = `${label} concatenated in`;
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolve(includes);
outfile = resolve(outfile);
if (supportsGlob && globOptions) {
includes = await glob(includes, globOptions);
}
if (concatOptions.removeDuplicates) {
includes = includes.map((path) => normalize(path));
includes = [ ...new Set(includes) ];
}
await concat(includes, outfile);
if (includes.length) {
message(`${label} concatenated`, 'success', timeLabel);
} else {
message(`${label} is empty`, 'notice', timeLabel);
}
} catch (err) {
message(`Error concatenating ${label}`, 'error');
message(err);
notification({
title: `${label} concatenation failed 🚨`,
message: `${err.name}: ${err.message}`
});
}
});
};

View File

@@ -1,113 +0,0 @@
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';
/**
* @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(esBuildOptions = null) {
if (esBuildOptions == null) {
esBuildOptions = productionESBuildOptions;
} else if (
esBuildOptions !== developmentESBuildOptions &&
esBuildOptions !== productionESBuildOptions
) {
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 = '',
outfile = '',
label = null
}) => {
if (!label) {
label = basename(outdir || outfile || 'undefined');
}
const timeLabel = `${label} compiled in`;
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolve(includes);
if (outdir) {
outdir = resolve(outdir);
} else if (outfile) {
outfile = resolve(outfile);
} else {
throw new TypeError(
'Expected \'outdir\' or \'outfile\''
);
}
await esbuild.build(Object.assign({}, esBuildOptions, {
entryPoints: includes,
outdir,
outfile,
}));
message(`${label} compiled`, 'success', timeLabel);
} catch (err) {
// errors managments (already done in esbuild)
notification({
title: `${label} compilation failed 🚨`,
message: `${err.errors[0].text} in ${err.errors[0].location.file} line ${err.errors[0].location.line}`
});
}
});
};

View File

@@ -1,242 +0,0 @@
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 * as sass from 'sass';
import { PurgeCSS } from 'purgecss';
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 = {
sourceMapIncludeSources: true,
sourceMap: true,
};
export const developmentSassOptions = Object.assign({}, defaultSassOptions, {
style: 'expanded',
});
export const productionSassOptions = Object.assign({}, defaultSassOptions, {
style: '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,
false
];
export const productionStylesArgs = [
productionSassOptions,
productionPostCSSOptions,
true
];
/**
* 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(sassOptions = null, postcssOptions = null, purge = true) {
if (sassOptions == null) {
sassOptions = productionSassOptions;
} else if (
sassOptions !== developmentSassOptions &&
sassOptions !== productionSassOptions
) {
sassOptions = merge({}, defaultSassOptions, sassOptions);
}
if (supportsPostCSS) {
if (postcssOptions == null) {
postcssOptions = productionPostCSSOptions;
} else if (
postcssOptions !== false &&
postcssOptions !== developmentPostCSSOptions &&
postcssOptions !== productionPostCSSOptions
) {
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,
label = null
}) => {
const filestem = basename((outfile || 'undefined'), '.css');
const timeLabel = `${label || `${filestem}.css`} compiled in`;
console.time(timeLabel);
try {
infile = resolve(infile);
outfile = resolve(outfile);
let result = sass.compile(infile, sassOptions);
if (supportsPostCSS && postcssOptions) {
if (typeof postcssProcessor === 'undefined') {
postcssProcessor = createProcessor(
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 ${label || `${filestem}.css`}`, 'warning');
warnings.forEach((warn) => {
message(warn.toString());
});
}
}
}
try {
await writeFile(outfile, result.css).then(() => {
// Purge CSS once file exists.
if (outfile && purge) {
purgeUnusedCSS(outfile, `${label || `${filestem}.css`}`);
}
});
if (result.css) {
message(`${label || `${filestem}.css`} compiled`, 'success', timeLabel);
} else {
message(`${label || `${filestem}.css`} is empty`, 'notice', timeLabel);
}
} catch (err) {
message(`Error compiling ${label || `${filestem}.css`}`, 'error');
message(err);
notification({
title: `${label || `${filestem}.css`} save failed 🚨`,
message: `Could not save stylesheet to ${label || `${filestem}.css`}`
});
}
if (result.map) {
try {
await writeFile(outfile + '.map', result.map.toString());
} catch (err) {
message(`Error compiling ${label || `${filestem}.css.map`}`, 'error');
message(err);
notification({
title: `${label || `${filestem}.css.map`} save failed 🚨`,
message: `Could not save sourcemap to ${label || `${filestem}.css.map`}`
});
}
}
} catch (err) {
message(`Error compiling ${label || `${filestem}.scss`}`, 'error');
message(err.formatted || err);
notification({
title: `${label || `${filestem}.scss`} compilation failed 🚨`,
message: (err.formatted || `${err.name}: ${err.message}`)
});
}
});
};
/**
* Purge unused styles from CSS files.
*
* @async
*
* @param {string} outfile - The path of a css file
* If missing the function stops.
* @param {string} label - The CSS file label or name.
* @return {Promise}
*/
async function purgeUnusedCSS(outfile, label) {
const contentFiles = loconfig.tasks.purgeCSS?.content;
if (!Array.isArray(contentFiles) || !contentFiles.length) {
return;
}
label = label ?? basename(outfile);
const timeLabel = `${label} purged in`;
console.time(timeLabel);
const purgeCSSResults = await (new PurgeCSS()).purge({
content: contentFiles,
css: [ outfile ],
defaultExtractor: content => content.match(/[a-z0-9_\-\\\/\@]+/gi) || [],
fontFaces: true,
keyframes: true,
safelist: {
// Keep all except .u-gc-* | .u-margin-* | .u-padding-*
standard: [ /^(?!.*\b(u-gc-|u-margin|u-padding)).*$/ ]
},
variables: true,
})
for (let result of purgeCSSResults) {
await writeFile(outfile, result.css)
message(`${label} purged`, 'chore', timeLabel);
}
}

View File

@@ -1,152 +0,0 @@
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 as resolveTemplate } from '../helpers/template.js';
import { merge } from '../utils/index.js';
import {
basename,
dirname,
extname,
resolve,
} from 'node:path';
import commonPath from 'common-path';
import mixer from 'svg-mixer';
import slugify from 'url-slug';
const basePath = loconfig?.paths?.svgs?.src
? resolve(loconfig.paths.svgs.src)
: null;
/**
* @const {object} defaultMixerOptions - The default shared Mixer options.
*/
export const defaultMixerOptions = {
spriteConfig: {
usages: false,
},
};
/**
* @const {object} developmentMixerOptions - The predefined Mixer options for development.
* @const {object} productionMixerOptions - The predefined Mixer options for production.
*/
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(mixerOptions = null) {
if (mixerOptions == null) {
mixerOptions = productionMixerOptions;
} else if (
mixerOptions !== developmentMixerOptions &&
mixerOptions !== productionMixerOptions
) {
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,
label = null
}) => {
if (!label) {
label = basename(outfile || 'undefined');
}
const timeLabel = `${label} compiled in`;
console.time(timeLabel);
try {
if (!Array.isArray(includes)) {
includes = [ includes ];
}
includes = resolveTemplate(includes);
outfile = resolveTemplate(outfile);
if (supportsGlob && basePath) {
includes = await glob(includes);
includes = [ ...new Set(includes) ];
const common = commonPath(includes);
if (common.commonDir) {
common.commonDir = resolve(common.commonDir);
}
/**
* Generates the `<symbol id>` attribute and prefix any
* SVG files in subdirectories according to the paths
* common base path.
*
* Example for SVG source path `./assets/images/sprite`:
*
* | Path | ID |
* | ------------------------------------ | --------- |
* | `./assets/images/sprite/foo.svg` | `foo` |
* | `./assets/images/sprite/baz/qux.svg` | `baz-qux` |
*
* @param {string} path - The absolute path to the file.
* @param {string} [query=''] - A query string.
* @return {string} The symbol ID.
*/
mixerOptions.generateSymbolId = (path, query = '') => {
let dirName = dirname(path)
.replace(common.commonDir ?? basePath, '')
.replace(/^\/|\/$/, '')
.replace('/', '-');
if (dirName) {
dirName += '-';
}
const fileName = basename(path, extname(path));
const decodedQuery = decodeURIComponent(decodeURIComponent(query));
return `${dirName}${fileName}${slugify(decodedQuery)}`;
};
}
const result = await mixer(includes, {
...mixerOptions,
});
await result.write(outfile);
message(`${label} compiled`, 'success', timeLabel);
} catch (err) {
message(`Error compiling ${label}`, 'error');
message(err);
notification({
title: `${label} compilation failed 🚨`,
message: `${err.name}: ${err.message}`
});
}
});
};

View File

@@ -1,441 +0,0 @@
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 {
createReadStream,
createWriteStream,
} from 'node:fs';
import {
mkdir,
rename,
rm,
readFile,
writeFile,
} from 'node:fs/promises';
import {
basename,
dirname,
} from 'node:path';
import readline from 'node:readline';
export const REGEXP_SEMVER = /^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
/**
* @typedef {object} VersionOptions
* @property {string|number|null} prettyPrint - A string or number to insert
* white space into the output JSON string for readability purposes.
* @property {string} versionFormat - The version number format.
* @property {string|RegExp} versionKey - Either:
* - A string representing the JSON field name assign the version number to.
*
* Explicit:
*
* ```json
* "key": "json:version"
* ```
*
* Implicit:
*
* ```json
* "key": "version"
* ```
*
* - A `RegExp` object or regular expression string prefixed with `regexp:`.
*
* ```json
* "key": "regexp:(?<=^const ASSETS_VERSION = ')(?<version>\\d+)(?=';$)"
* ```
*
* ```js
* key: new RegExp('(?<=^const ASSETS_VERSION = ')(?<version>\\d+)(?=';$)')
* ```
*
* ```js
* key: /(?<=^const ASSETS_VERSION = ')(?<version>\d+)(?=';$)/
* ```
*/
/**
* @const {VersionOptions} defaultVersionOptions - The default shared version options.
* @const {VersionOptions} developmentVersionOptions - The predefined version options for development.
* @const {VersionOptions} productionVersionOptions - The predefined version options for production.
*/
export const defaultVersionOptions = {
prettyPrint: 4,
versionFormat: 'timestamp',
versionKey: 'version',
};
export const developmentVersionOptions = Object.assign({}, defaultVersionOptions);
export const productionVersionOptions = Object.assign({}, defaultVersionOptions);
/**
* @const {object} developmentVersionFilesArgs - The predefined `bumpVersion()` options for development.
* @const {object} productionVersionFilesArgs - The predefined `bumpVersion()` options for production.
*/
export const developmentVersionFilesArgs = [
developmentVersionOptions,
];
export const productionVersionFilesArgs = [
productionVersionOptions,
];
/**
* Bumps version numbers in file.
*
* @async
* @param {object} [versionOptions=null] - Customize the version options.
* If `null`, default production options are used.
* @return {Promise}
*/
export default async function bumpVersions(versionOptions = null) {
if (versionOptions == null) {
versionOptions = productionVersionOptions;
} else if (
versionOptions !== developmentVersionOptions &&
versionOptions !== productionVersionOptions
) {
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,
...options
}) => {
if (!label) {
label = basename(outfile || 'undefined');
}
options.pretty = (options.pretty ?? versionOptions.prettyPrint);
options.format = (options.format ?? versionOptions.versionFormat);
options.key = (options.key ?? versionOptions.versionKey);
if (queue.has(outfile)) {
queue.get(outfile).then(() => handleBumpVersion(outfile, label, options));
} else {
queue.set(outfile, handleBumpVersion(outfile, label, options));
}
});
};
/**
* Creates a formatted version number or string.
*
* @param {string} format - The version format.
* @param {?string} [oldValue] - The old version value.
* @return {string|number}
* @throws TypeError If the format or value are invalid.
*/
function createVersionNumber(format, oldValue = null) {
let [ type, modifier ] = format.split(':', 2);
switch (type) {
case 'hex':
case 'hexadecimal':
try {
modifier = Number.parseInt(modifier);
if (Number.isNaN(modifier)) {
modifier = 6;
}
return randomBytes(modifier).toString('hex');
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "hexadecimal"`,
{ cause: err }
);
}
case 'inc':
case 'increment':
try {
if (modifier === 'semver') {
return incrementSemVer(oldValue, [ 'buildmetadata', 'patch' ]);
}
return incrementNumber(oldValue, modifier);
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "increment"`,
{ cause: err }
);
}
case 'regex':
case 'regexp':
try {
return new RegExp(modifier);
} catch (err) {
throw new TypeError(
`${err.message} for \'format\' type "regexp"`,
{ cause: err }
);
}
case 'timestamp':
return Date.now().valueOf();
}
throw new TypeError(
'Expected \'format\' to be either "timestamp", "increment", or "hexadecimal"'
);
}
/**
* @async
* @param {string} outfile
* @param {string} label
* @param {object} options
* @return {Promise}
*/
async function handleBumpVersion(outfile, label, options) {
const timeLabel = `${label} bumped in`;
console.time(timeLabel);
try {
options.key = parseVersionKey(options.key);
if (options.key instanceof RegExp) {
await handleBumpVersionWithRegExp(outfile, label, options);
} else {
await handleBumpVersionInJson(outfile, label, options);
}
message(`${label} bumped`, 'success', timeLabel);
} catch (err) {
message(`Error bumping ${label}`, 'error');
message(err);
notification({
title: `${label} bumping failed 🚨`,
message: `${err.name}: ${err.message}`
});
}
}
/**
* Changes the version number for the provided JSON key in file.
*
* @async
* @param {string} outfile
* @param {string} label
* @param {object} options
* @param {string} options.key
* @return {Promise}
*/
async function handleBumpVersionInJson(outfile, label, options) {
outfile = resolve(outfile);
let json;
try {
json = JSON.parse(await readFile(outfile, { encoding: 'utf8' }));
} catch (err) {
json = {};
message(`${label} is a new file`, 'notice');
await mkdir(dirname(outfile), { recursive: true });
}
json[options.key] = createVersionNumber(options.format, json?.[options.key]);
return await writeFile(
outfile,
JSON.stringify(json, null, options.pretty),
{ encoding: 'utf8' }
);
}
/**
* Changes the version number for the provided RegExp in file.
*
* @async
* @param {string} outfile
* @param {string} label
* @param {object} options
* @param {RegExp} options.key
* @return {Promise}
*/
async function handleBumpVersionWithRegExp(outfile, label, options) {
outfile = resolve(outfile);
const bckfile = `${outfile}~`;
await rename(outfile, bckfile);
try {
const rl = readline.createInterface({
input: createReadStream(bckfile),
});
let newVersion = null;
const writeStream = createWriteStream(outfile, { encoding: 'utf8' });
rl.on('line', (line) => {
const found = line.match(options.key);
if (found) {
const groups = (found.groups ?? {});
const oldVersion = (groups.build ?? groups.version ?? found[1] ?? found[0]);
const newVersion = createVersionNumber(options.format, oldVersion);
const replacement = found[0].replace(oldVersion, newVersion);
line = line.replace(found[0], replacement);
}
writeStream.write(line + "\n");
});
await events.once(rl, 'close');
await rm(bckfile);
} catch (err) {
await rm(outfile, { force: true });
await rename(bckfile, outfile);
throw err;
}
}
/**
* Increments the given integer.
*
* @param {string|int} value - The number to increment.
* @param {string|int} [increment=1] - The amount to increment by.
* @return {int}
* @throws TypeError If the version number is invalid.
*/
function incrementNumber(value, increment = 1) {
const version = Number.parseInt(value);
if (Number.isNaN(version)) {
throw new TypeError(
`Expected an integer version number, received [${value}]`
);
}
increment = Number.parseInt(increment);
if (Number.isNaN(increment)) {
throw new TypeError(
'Expected an integer increment number'
);
}
return (version + increment);
}
/**
* Increments the given SemVer version number.
*
* @param {string} value - The version to mutate.
* @param {string|string[]} [target] - The segment to increment, one of:
* 'major', 'minor', 'patch', ~~'prerelease'~~, 'buildmetadata'.
* @param {string|int} [increment=1] - The amount to increment by.
* @return {string}
* @throws TypeError If the version or target are invalid.
*/
function incrementSemVer(value, target = 'patch', increment = 1) {
const found = value.match(REGEXP_SEMVER);
if (!found) {
throw new TypeError(
`Expected a SemVer version number, received [${value}]`
);
}
if (Array.isArray(target)) {
for (const group of target) {
if (found.groups[group] != null) {
target = group;
break;
}
}
}
if (!target || !found.groups[target]) {
throw new TypeError(
`Expected a supported SemVer segment, received [${target}]`
);
}
const segments = {
'major': '',
'minor': '.',
'patch': '.',
'prerelease': '-',
'buildmetadata': '+',
};
let replacement = '';
for (const [ segment, delimiter ] of Object.entries(segments)) {
if (found.groups?.[segment] != null) {
const newVersion = (segment === target)
? incrementNumber(found.groups[segment], increment)
: found.groups[segment];
replacement += `${delimiter}${newVersion}`;
}
}
return value.replace(found[0], replacement);
}
/**
* Parses the version key.
*
* @param {*} key - The version key.
* @return {string|RegExp}
*/
function parseVersionKey(key) {
if (key instanceof RegExp) {
return key;
}
if (typeof key !== 'string') {
throw new TypeError(
'Expected \'key\' to be either a string or a RegExp'
);
}
const delimiter = key.indexOf(':');
if (delimiter === -1) {
// Assumes its a JSON key
return key;
}
const type = key.slice(0, delimiter);
const value = key.slice(delimiter + 1);
switch (type) {
case 'json':
return value;
case 'regex':
case 'regexp':
return new RegExp(value);
}
throw new TypeError(
'Expected \'key\' type to be either "json" or "regexp"'
);
}

View File

@@ -1,115 +0,0 @@
/**
* @file Provides generic functions and constants.
*/
/**
* @type {RegExp} - Match all special characters.
*/
const regexUnescaped = /[\[\]\{\}\(\)\-\*\+\?\.\,\\\^\$\|\#\s]/g;
/**
* Quotes regular expression characters.
*
* @param {string} str - The input string.
* @return {string} Returns the quoted (escaped) string.
*/
function escapeRegExp(str) {
return str.replace(regexUnescaped, '\\$&');
}
/**
* Creates a new object with all nested object properties
* concatenated into it recursively.
*
* Nested keys are flattened into a property path:
*
* ```js
* {
* a: {
* b: {
* c: 1
* }
* },
* d: 1
* }
* ```
*
* ```js
* {
* "a.b.c": 1,
* "d": 1
* }
* ```
*
* @param {object} input - The object to flatten.
* @param {string} prefix - The parent key prefix.
* @param {object} target - The object that will receive the flattened properties.
* @return {object} Returns the `target` object.
*/
function flatten(input, prefix, target = {}) {
for (const key in input) {
const field = (prefix ? prefix + '.' + key : key);
if (isObjectLike(input[key])) {
flatten(input[key], field, target);
} else {
target[field] = input[key];
}
}
return target;
}
/**
* Determines whether the passed value is an `Object`.
*
* @param {*} value - The value to be checked.
* @return {boolean} Returns `true` if the value is an `Object`,
* otherwise `false`.
*/
function isObjectLike(value) {
return (value != null && typeof value === 'object');
}
/**
* Creates a new object with all nested object properties
* merged into it recursively.
*
* @param {object} target - The target object.
* @param {object[]} ...sources - The source object(s).
* @throws {TypeError} If the target and source are the same.
* @return {object} Returns the `target` object.
*/
function merge(target, ...sources) {
for (const source of sources) {
if (target === source) {
throw new TypeError(
'Cannot merge, target and source are the same'
);
}
for (const key in source) {
if (source[key] != null) {
if (isObjectLike(source[key]) && isObjectLike(target[key])) {
merge(target[key], source[key]);
continue;
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
target[key] = target[key].concat(source[key]);
continue;
}
}
target[key] = source[key];
}
}
return target;
}
export {
escapeRegExp,
flatten,
isObjectLike,
merge,
regexUnescaped,
};

View File

@@ -1,199 +0,0 @@
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 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';
// Match a URL protocol.
const regexUrlStartsWithProtocol = /^[a-z0-9\-]:\/\//i;
// Build scripts, compile styles, concat files,
// and generate spritesheets on first hit
concatFiles(...developmentConcatFilesArgs);
compileScripts(...developmentScriptsArgs);
compileStyles(...developmentStylesArgs);
compileSVGs(...developmentSVGsArgs);
// Create a new BrowserSync instance
const server = browserSync.create();
// 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}`
});
}
});
configureServer(server, loconfig);
/**
* 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);
// 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
if (tasks.concats?.length) {
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,
ghostMode: 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);
}

View File

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

View File

@@ -1,72 +0,0 @@
{
"paths": {
"url": "locomotive-boilerplate.test",
"src": "./assets",
"dest": "./www",
"images": {
"src": "./assets/images"
},
"styles": {
"src": "./assets/styles",
"dest": "./www/assets/styles"
},
"scripts": {
"src": "./assets/scripts",
"dest": "./www/assets/scripts"
},
"svgs": {
"src": "./assets/svgs",
"dest": "./www/assets/images"
},
"views": {
"src": "./www/"
}
},
"tasks": {
"concats": [
{
"includes": [
"{% paths.scripts.src %}/vendors/*.js"
],
"outfile": "{% paths.scripts.dest %}/vendors.js"
}
],
"scripts": [
{
"includes": [
"{% paths.scripts.src %}/app.js"
],
"outfile": "{% paths.scripts.dest %}/app.js"
}
],
"styles": [
{
"infile": "{% paths.styles.src %}/critical.scss",
"outfile": "{% paths.styles.dest %}/critical.css"
},
{
"infile": "{% paths.styles.src %}/main.scss",
"outfile": "{% paths.styles.dest %}/main.css"
}
],
"svgs": [
{
"includes": [
"{% paths.svgs.src %}/*.svg"
],
"outfile": "{% paths.svgs.dest %}/sprite.svg"
}
],
"purgeCSS": {
"content": [
"./www/**/*.html",
"./assets/scripts/**/*"
]
},
"versions": [
{
"outfile": "./assets.json"
}
]
}
}

5586
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,39 +10,22 @@
"npm": ">=10"
},
"scripts": {
"start": "node --no-warnings build/watch.js",
"build": "node --no-warnings build/build.js"
"start": "npm run dev",
"dev": "vite serve",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"locomotive-scroll": "^5.0.0-beta.21",
"modujs": "^1.4.2",
"modularload": "^1.2.6",
"svg4everybody": "^2.1.9"
"locomotive-scroll": "^5.0.0-beta.21"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
"browser-sync": "^3.0.2",
"common-path": "^1.0.1",
"concat": "^1.0.3",
"esbuild": "^0.24.0",
"kleur": "^4.1.5",
"node-notifier": "^10.0.1",
"postcss": "^8.4.47",
"purgecss": "^6.0.0",
"sass": "^1.79.5",
"svg-mixer": "^2.4.0",
"tiny-glob": "^0.2.9"
},
"overrides": {
"browser-sync": {
"ua-parser-js": "^1.0.33"
},
"svg-mixer": {
"micromatch": "^4.0.8",
"postcss": "^8.4.38"
},
"svg-mixer-utils": {
"anymatch": "^3.1.3"
}
"@types/node": "^22.7.7",
"dotenv": "^16.4.5",
"fast-glob": "^3.3.2",
"path": "^0.12.7",
"sass-embedded": "^1.80.3",
"typescript": "^5.6.3",
"url": "^0.11.4",
"vite": "^5.4.9"
}
}

View File

@@ -0,0 +1,10 @@
.c-carousel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}

View File

@@ -0,0 +1,11 @@
class Carousel extends HTMLElement {
constructor() {
super();
console.log('Carousel constructor');
}
connectedCallback() {
console.log('Carousel connected');
}
}
console.log('Carousel loaded');
customElements.define('c-carousel', Carousel);

View File

@@ -0,0 +1,11 @@
class Header extends HTMLElement {
constructor() {
super();
console.log('Header constructor');
}
connectedCallback() {
console.log('Header connected');
}
}
customElements.define('c-header', Header);

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

70
src/index.html Normal file
View File

@@ -0,0 +1,70 @@
<!doctype html>
<html class="is-loading" lang="en" data-page="home">
<head>
<meta charset="utf-8">
<title>Locomotive Boilerplate</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<meta name="msapplication-TileColor" content="#ffffff">
<link rel="manifest" href="site.webmanifest">
<link rel="apple-touch-icon" sizes="180x180" href="assets/images/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="assets/images/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicons/favicon-16x16.png">
<link rel="mask-icon" href="assets/images/favicons/safari-pinned-tab.svg" color="#000000">
<!-- For a dark mode managment and svg favicon add this in your favicon.svg -->
<!--
<style>
path {
fill: #000;
}
@media (prefers-color-scheme: dark) {
path {
fill: #fff;
}
}
</style>
-->
<!-- <link rel="icon" href="assets/images/favicons/favicon.svg"> -->
<!-- Preload Fonts -->
<link rel="preload" href="/fonts/SourceSans3-Bold.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/SourceSans3-BoldIt.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/SourceSans3-Regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/SourceSans3-RegularIt.woff2" as="font" type="font/woff2" crossorigin>
<!-- <link rel="stylesheet" href="../src/styles/main.scss"> -->
</head>
<body>
<header>
<a href="/">
<h1>Locomotive Boilerplate</h1>
</a>
<nav>
<ul>
<li><a href="images.html">Images</a></li>
<li><a href="form.html" data-load="customTransition">Form</a></li>
<li><a href="grid.html">Grid</a></li>
</ul>
</nav>
</header>
<main>
<div>
<h2>Home</h2>
<p>Welcome to the Locomotive Boilerplate.</p>
</div>
</main>
<footer>
<p>Made with <a href="https://github.com/locomotivemtl/locomotive-boilerplate"
title="Locomotive Boilerplate" target="_blank" rel="noopener">🚂</a></p>
</footer>
<script src="/scripts/app.ts" type="module"></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 123 B

1
src/scripts/app.ts Normal file
View File

@@ -0,0 +1 @@
console.log('app.ts')

5
src/styles/main.scss Normal file
View File

@@ -0,0 +1,5 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

Some files were not shown because too many files have changed in this diff Show More