Merge pull request #106 from locomotivemtl/feature/local-config
Add support for loconfig.local.json
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
loconfig.*.json
|
||||||
|
!loconfig.example.json
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import loconfig from '../../loconfig.json';
|
import loconfig from '../utils/config.js';
|
||||||
import glob from '../utils/glob.js';
|
import glob from '../utils/glob.js';
|
||||||
import message from '../utils/message.js';
|
import message from '../utils/message.js';
|
||||||
import notification from '../utils/notification.js';
|
import notification from '../utils/notification.js';
|
||||||
import template from '../utils/template.js';
|
import resolve from '../utils/template.js';
|
||||||
import concat from 'concat';
|
import concat from 'concat';
|
||||||
import {
|
import {
|
||||||
basename,
|
basename,
|
||||||
@@ -98,8 +98,8 @@ export default async function concatFiles(globOptions = null, concatOptions = nu
|
|||||||
console.time(timeLabel);
|
console.time(timeLabel);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
includes = includes.map((path) => template(path));
|
includes = resolve(includes);
|
||||||
outfile = template(outfile);
|
outfile = resolve(outfile);
|
||||||
|
|
||||||
let files;
|
let files;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import loconfig from '../../loconfig.json';
|
import loconfig from '../utils/config.js';
|
||||||
import message from '../utils/message.js';
|
import message from '../utils/message.js';
|
||||||
import notification from '../utils/notification.js';
|
import notification from '../utils/notification.js';
|
||||||
import template from '../utils/template.js';
|
import resolve from '../utils/template.js';
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import { basename } from 'node:path';
|
import { basename } from 'node:path';
|
||||||
|
|
||||||
@@ -67,12 +67,12 @@ export default async function compileScripts(esBuildOptions = null) {
|
|||||||
console.time(timeLabel);
|
console.time(timeLabel);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
includes = includes.map((path) => template(path));
|
includes = resolve(includes);
|
||||||
|
|
||||||
if (outdir) {
|
if (outdir) {
|
||||||
outdir = template(outdir);
|
outdir = resolve(outdir);
|
||||||
} else if (outfile) {
|
} else if (outfile) {
|
||||||
outfile = template(outfile);
|
outfile = resolve(outfile);
|
||||||
} else {
|
} else {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
'Expected \'outdir\' or \'outfile\''
|
'Expected \'outdir\' or \'outfile\''
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import loconfig from '../../loconfig.json';
|
import loconfig from '../utils/config.js';
|
||||||
import message from '../utils/message.js';
|
import message from '../utils/message.js';
|
||||||
import notification from '../utils/notification.js';
|
import notification from '../utils/notification.js';
|
||||||
import postcss, { pluginsMap as postcssPluginsMap } from '../utils/postcss.js';
|
import postcss, { pluginsMap as postcssPluginsMap } from '../utils/postcss.js';
|
||||||
import template from '../utils/template.js';
|
import resolve from '../utils/template.js';
|
||||||
import { writeFile } from 'node:fs/promises';
|
import { writeFile } from 'node:fs/promises';
|
||||||
import { basename } from 'node:path';
|
import { basename } from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
@@ -106,8 +106,8 @@ export default async function compileStyles(sassOptions = null, postcssOptions =
|
|||||||
console.time(timeLabel);
|
console.time(timeLabel);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
infile = template(infile);
|
infile = resolve(infile);
|
||||||
outfile = template(outfile);
|
outfile = resolve(outfile);
|
||||||
|
|
||||||
let result = await sassRender(Object.assign({}, sassOptions, {
|
let result = await sassRender(Object.assign({}, sassOptions, {
|
||||||
file: infile,
|
file: infile,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import loconfig from '../../loconfig.json';
|
import loconfig from '../utils/config.js';
|
||||||
import message from '../utils/message.js';
|
import message from '../utils/message.js';
|
||||||
import notification from '../utils/notification.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 { basename } from 'node:path';
|
||||||
import mixer from 'svg-mixer';
|
import mixer from 'svg-mixer';
|
||||||
|
|
||||||
@@ -60,8 +60,8 @@ export default async function compileSVGs(mixerOptions = null) {
|
|||||||
console.time(timeLabel);
|
console.time(timeLabel);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
includes = includes.map((path) => template(path));
|
includes = resolve(includes);
|
||||||
outfile = template(outfile);
|
outfile = resolve(outfile);
|
||||||
|
|
||||||
const result = await mixer(includes, mixerOptions);
|
const result = await mixer(includes, mixerOptions);
|
||||||
|
|
||||||
|
|||||||
64
build/utils/config.js
Normal file
64
build/utils/config.js
Normal 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');
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* @file Provides simple template tags.
|
* @file Provides simple template tags.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import loconfig from '../../loconfig.json';
|
import loconfig from './config.js';
|
||||||
|
|
||||||
const templateData = flatten({
|
const templateData = flatten({
|
||||||
paths: loconfig.paths
|
paths: loconfig.paths
|
||||||
@@ -14,17 +14,53 @@ const templateData = flatten({
|
|||||||
* If replacement pairs contain a mix of substrings, regular expressions,
|
* If replacement pairs contain a mix of substrings, regular expressions,
|
||||||
* and functions, regular expressions are executed last.
|
* and functions, regular expressions are executed last.
|
||||||
*
|
*
|
||||||
* @param {string} input - The string being searched and replaced on.
|
* @param {*} input - The value being searched and replaced on.
|
||||||
* @param {object} data - An object in the form `{ 'from': 'to', … }`.
|
* 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.
|
* @return {string} Returns the translated string.
|
||||||
*/
|
*/
|
||||||
export default function template(input, data) {
|
export function resolveValue(input, data = templateData) {
|
||||||
const tags = [];
|
const tags = [];
|
||||||
|
|
||||||
if (data) {
|
if (data !== templateData) {
|
||||||
data = flatten(data);
|
data = flatten(data);
|
||||||
} else {
|
|
||||||
data = templateData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let tag in data) {
|
for (let tag in data) {
|
||||||
@@ -55,7 +91,7 @@ export default function template(input, data) {
|
|||||||
|
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new object with all nested object properties
|
* Creates a new object with all nested object properties
|
||||||
|
|||||||
255
build/watch.js
255
build/watch.js
@@ -1,47 +1,16 @@
|
|||||||
import loconfig from '../loconfig.json';
|
|
||||||
import concatFiles, { developmentConcatFilesArgs } from './tasks/concats.js';
|
import concatFiles, { developmentConcatFilesArgs } from './tasks/concats.js';
|
||||||
import compileScripts, { developmentScriptsArgs } from './tasks/scripts.js';
|
import compileScripts, { developmentScriptsArgs } from './tasks/scripts.js';
|
||||||
import compileStyles, { developmentStylesArgs } from './tasks/styles.js' ;
|
import compileStyles, { developmentStylesArgs } from './tasks/styles.js' ;
|
||||||
import compileSVGs, { developmentSVGsArgs } from './tasks/svgs.js';
|
import compileSVGs, { developmentSVGsArgs } from './tasks/svgs.js';
|
||||||
import template from './utils/template.js';
|
import loconfig, { merge } from './utils/config.js';
|
||||||
import server from 'browser-sync';
|
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';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
const { paths, tasks } = loconfig;
|
// Match a URL protocol.
|
||||||
|
const regexUrlStartsWithProtocol = /^[a-z0-9\-]:\/\//i;
|
||||||
// Convert view(s) to an array
|
|
||||||
switch (typeof paths.views) {
|
|
||||||
case 'string':
|
|
||||||
paths.views = [ paths.views ];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'object':
|
|
||||||
if (paths.views == null) {
|
|
||||||
paths.views = [];
|
|
||||||
} else if (!Array.isArray(paths.views)) {
|
|
||||||
paths.views = Object.values(paths.views);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverConfig = {
|
|
||||||
open: false,
|
|
||||||
notify: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve the URI for the Browsersync server
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Build scripts, compile styles, concat files,
|
// Build scripts, compile styles, concat files,
|
||||||
// and generate spritesheets on first hit
|
// and generate spritesheets on first hit
|
||||||
@@ -50,49 +19,177 @@ compileScripts(...developmentScriptsArgs);
|
|||||||
compileStyles(...developmentStylesArgs);
|
compileStyles(...developmentStylesArgs);
|
||||||
compileSVGs(...developmentSVGsArgs);
|
compileSVGs(...developmentSVGsArgs);
|
||||||
|
|
||||||
// Reload on any changes to views or processed files
|
// Create a new BrowserSync instance
|
||||||
server.watch(
|
const server = browserSync.create();
|
||||||
[
|
|
||||||
...paths.views,
|
|
||||||
join(paths.scripts.dest, '*.js'),
|
|
||||||
join(paths.styles.dest, '*.css'),
|
|
||||||
join(paths.svgs.dest, '*.svg'),
|
|
||||||
]
|
|
||||||
).on('change', server.reload);
|
|
||||||
|
|
||||||
// Watch source scripts
|
// Start the BrowserSync server
|
||||||
server.watch(
|
server.init(createServerOptions(loconfig), (err) => {
|
||||||
[
|
if (err) {
|
||||||
join(paths.scripts.src, '**/*.js'),
|
message('Error starting development server', 'error');
|
||||||
]
|
message(err);
|
||||||
).on('change', () => {
|
|
||||||
compileScripts(...developmentScriptsArgs);
|
notification({
|
||||||
|
title: 'Development server failed',
|
||||||
|
message: `${err.name}: ${err.message}`
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch source concats
|
configureServer(server, loconfig);
|
||||||
server.watch(
|
|
||||||
tasks.concats.reduce(
|
|
||||||
(patterns, { includes }) => patterns.concat(includes),
|
|
||||||
[]
|
|
||||||
).map((path) => template(path))
|
|
||||||
).on('change', () => {
|
|
||||||
concatFiles(...developmentConcatFilesArgs);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Watch source styles
|
/**
|
||||||
server.watch(
|
* Configures the BrowserSync options.
|
||||||
[
|
*
|
||||||
join(paths.styles.src, '**/*.scss'),
|
* @param {BrowserSync} server - The BrowserSync API.
|
||||||
]
|
* @param {object} loconfig - The project configset.
|
||||||
).on('change', () => {
|
* @param {object} loconfig.paths - The paths options.
|
||||||
compileStyles(...developmentStylesArgs);
|
* @param {object} loconfig.tasks - The tasks options.
|
||||||
});
|
* @return {void}
|
||||||
|
*/
|
||||||
|
function configureServer(server, { paths, tasks }) {
|
||||||
|
const views = createViewsArray(paths.views);
|
||||||
|
|
||||||
// Watch source SVGs
|
// Reload on any changes to views or processed files
|
||||||
server.watch(
|
server.watch(
|
||||||
[
|
[
|
||||||
join(paths.svgs.src, '*.svg'),
|
...views,
|
||||||
]
|
join(paths.scripts.dest, '*.js'),
|
||||||
).on('change', () => {
|
join(paths.styles.dest, '*.css'),
|
||||||
compileSVGs(...developmentSVGsArgs);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
8
loconfig.example.json
Executable file
8
loconfig.example.json
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"https": {
|
||||||
|
"key": "~/.config/valet/Certificates/{% paths.url %}.key",
|
||||||
|
"cert": "~/.config/valet/Certificates/{% paths.url %}.crt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user