From 48bd911804550e5724ca8bd93d890cdadccfa7ce Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Tue, 22 Mar 2022 16:08:29 -0400 Subject: [PATCH 1/5] 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()`. --- build/tasks/concats.js | 6 ++--- build/tasks/scripts.js | 8 +++---- build/tasks/styles.js | 6 ++--- build/tasks/svgs.js | 6 ++--- build/utils/template.js | 50 +++++++++++++++++++++++++++++++++++------ build/watch.js | 12 +++++----- 6 files changed, 63 insertions(+), 25 deletions(-) diff --git a/build/tasks/concats.js b/build/tasks/concats.js index 687583a..00eccc1 100644 --- a/build/tasks/concats.js +++ b/build/tasks/concats.js @@ -2,7 +2,7 @@ import loconfig from '../../loconfig.json'; 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, @@ -98,8 +98,8 @@ export default async function concatFiles(globOptions = null, concatOptions = nu console.time(timeLabel); try { - includes = includes.map((path) => template(path)); - outfile = template(outfile); + includes = resolve(includes); + outfile = resolve(outfile); let files; diff --git a/build/tasks/scripts.js b/build/tasks/scripts.js index 36b9000..66102a3 100644 --- a/build/tasks/scripts.js +++ b/build/tasks/scripts.js @@ -1,7 +1,7 @@ import loconfig from '../../loconfig.json'; 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'; @@ -67,12 +67,12 @@ export default async function compileScripts(esBuildOptions = null) { 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\'' diff --git a/build/tasks/styles.js b/build/tasks/styles.js index 47d4eba..16e5a8b 100644 --- a/build/tasks/styles.js +++ b/build/tasks/styles.js @@ -2,7 +2,7 @@ import loconfig from '../../loconfig.json'; import message from '../utils/message.js'; import notification from '../utils/notification.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 { basename } from 'node:path'; import { promisify } from 'node:util'; @@ -106,8 +106,8 @@ export default async function compileStyles(sassOptions = null, postcssOptions = console.time(timeLabel); try { - infile = template(infile); - outfile = template(outfile); + infile = resolve(infile); + outfile = resolve(outfile); let result = await sassRender(Object.assign({}, sassOptions, { file: infile, diff --git a/build/tasks/svgs.js b/build/tasks/svgs.js index c67c4fc..6fd6b2f 100644 --- a/build/tasks/svgs.js +++ b/build/tasks/svgs.js @@ -1,7 +1,7 @@ import loconfig from '../../loconfig.json'; 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'; @@ -60,8 +60,8 @@ export default async function compileSVGs(mixerOptions = null) { console.time(timeLabel); try { - includes = includes.map((path) => template(path)); - outfile = template(outfile); + includes = resolve(includes); + outfile = resolve(outfile); const result = await mixer(includes, mixerOptions); diff --git a/build/utils/template.js b/build/utils/template.js index 2d9b633..fa0d2a8 100644 --- a/build/utils/template.js +++ b/build/utils/template.js @@ -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 diff --git a/build/watch.js b/build/watch.js index ef2f6c6..d1b9086 100644 --- a/build/watch.js +++ b/build/watch.js @@ -3,7 +3,7 @@ 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 template from './utils/template.js'; +import resolve from './utils/template.js'; import server from 'browser-sync'; import { join } from 'node:path'; @@ -71,10 +71,12 @@ server.watch( // Watch source concats server.watch( - tasks.concats.reduce( - (patterns, { includes }) => patterns.concat(includes), - [] - ).map((path) => template(path)) + resolve( + tasks.concats.reduce( + (patterns, { includes }) => patterns.concat(includes), + [] + ) + ) ).on('change', () => { concatFiles(...developmentConcatFilesArgs); }); From 86f88c3f147b9afc9f7ed35b9a437e04354352b9 Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Tue, 22 Mar 2022 16:10:18 -0400 Subject: [PATCH 2/5] 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" } } ``` --- build/watch.js | 215 +++++++++++++++++++++++++++++++------------------ 1 file changed, 136 insertions(+), 79 deletions(-) diff --git a/build/watch.js b/build/watch.js index d1b9086..d6d4f6d 100644 --- a/build/watch.js +++ b/build/watch.js @@ -3,46 +3,12 @@ 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 message from './utils/message.js'; +import notification from './utils/notification.js'; import resolve from './utils/template.js'; -import server from 'browser-sync'; +import browserSync from 'browser-sync'; import { join } from 'node:path'; -const { paths, tasks } = loconfig; - -// 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, // and generate spritesheets on first hit concatFiles(...developmentConcatFilesArgs); @@ -50,51 +16,142 @@ compileScripts(...developmentScriptsArgs); compileStyles(...developmentStylesArgs); compileSVGs(...developmentSVGsArgs); -// Reload on any changes to views or processed files -server.watch( - [ - ...paths.views, - 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 source scripts -server.watch( - [ - join(paths.scripts.src, '**/*.js'), - ] -).on('change', () => { - compileScripts(...developmentScriptsArgs); +// 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 source concats -server.watch( - resolve( - tasks.concats.reduce( - (patterns, { includes }) => patterns.concat(includes), - [] +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 + server.watch( + resolve( + tasks.concats.reduce( + (patterns, { includes }) => patterns.concat(includes), + [] + ) ) - ) -).on('change', () => { - concatFiles(...developmentConcatFilesArgs); -}); + ).on('change', () => { + concatFiles(...developmentConcatFilesArgs); + }); -// Watch source styles -server.watch( - [ - join(paths.styles.src, '**/*.scss'), - ] -).on('change', () => { - compileStyles(...developmentStylesArgs); -}); + // 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); -}); + // 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 URI for the BrowserSync server + if (typeof paths.url === 'string' && paths.url.length > 0) { + // Use proxy + config.proxy = paths.url; + } else if (typeof paths.dest === 'string' && paths.dest.length > 0) { + // Use base directory + config.server = { + baseDir: paths.dest + }; + } + + Object.assign(config, resolve(options)); + + 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' + ); +} From 0cfb3fbc7d181cb42bf3bff8fbeda4054c34c775 Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Tue, 22 Mar 2022 16:17:49 -0400 Subject: [PATCH 3/5] 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" } } } ``` --- .gitignore | 1 + build/tasks/concats.js | 2 +- build/tasks/scripts.js | 2 +- build/tasks/styles.js | 2 +- build/tasks/svgs.js | 2 +- build/utils/config.js | 64 +++++++++++++++++++++++++++++++++++++++++ build/utils/template.js | 2 +- build/watch.js | 6 ++-- 8 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 build/utils/config.js diff --git a/.gitignore b/.gitignore index 4165750..1d27d65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .DS_Store Thumbs.db +loconfig.*.json diff --git a/build/tasks/concats.js b/build/tasks/concats.js index 00eccc1..af26a43 100644 --- a/build/tasks/concats.js +++ b/build/tasks/concats.js @@ -1,4 +1,4 @@ -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'; diff --git a/build/tasks/scripts.js b/build/tasks/scripts.js index 66102a3..e730385 100644 --- a/build/tasks/scripts.js +++ b/build/tasks/scripts.js @@ -1,4 +1,4 @@ -import loconfig from '../../loconfig.json'; +import loconfig from '../utils/config.js'; import message from '../utils/message.js'; import notification from '../utils/notification.js'; import resolve from '../utils/template.js'; diff --git a/build/tasks/styles.js b/build/tasks/styles.js index 16e5a8b..f358ff8 100644 --- a/build/tasks/styles.js +++ b/build/tasks/styles.js @@ -1,4 +1,4 @@ -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, { pluginsMap as postcssPluginsMap } from '../utils/postcss.js'; diff --git a/build/tasks/svgs.js b/build/tasks/svgs.js index 6fd6b2f..7672384 100644 --- a/build/tasks/svgs.js +++ b/build/tasks/svgs.js @@ -1,4 +1,4 @@ -import loconfig from '../../loconfig.json'; +import loconfig from '../utils/config.js'; import message from '../utils/message.js'; import notification from '../utils/notification.js'; import resolve from '../utils/template.js'; diff --git a/build/utils/config.js b/build/utils/config.js new file mode 100644 index 0000000..8fa0f86 --- /dev/null +++ b/build/utils/config.js @@ -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'); +} diff --git a/build/utils/template.js b/build/utils/template.js index fa0d2a8..a42bfd1 100644 --- a/build/utils/template.js +++ b/build/utils/template.js @@ -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 diff --git a/build/watch.js b/build/watch.js index d6d4f6d..384c756 100644 --- a/build/watch.js +++ b/build/watch.js @@ -1,8 +1,8 @@ -import loconfig from '../loconfig.json'; 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'; @@ -124,9 +124,7 @@ function createServerOptions({ }; } - Object.assign(config, resolve(options)); - - return config; + return merge(config, resolve(options)); } /** From 5010560ee359eb24f29c6ad55b28131f963a18df Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Wed, 23 Mar 2022 13:13:46 -0400 Subject: [PATCH 4/5] Improve HTTPS/Proxy URL support in watch.js Added logic to prepend "https://" to the proxy URL if missing. --- build/watch.js | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/build/watch.js b/build/watch.js index 384c756..94e071c 100644 --- a/build/watch.js +++ b/build/watch.js @@ -9,6 +9,9 @@ import resolve from './utils/template.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); @@ -113,18 +116,29 @@ function createServerOptions({ notify: false }; - // Resolve the URI for the BrowserSync server - if (typeof paths.url === 'string' && paths.url.length > 0) { + // Resolve the URL for the BrowserSync server + if (isNonEmptyString(paths.url)) { // Use proxy config.proxy = paths.url; - } else if (typeof paths.dest === 'string' && paths.dest.length > 0) { + } else if (isNonEmptyString(paths.dest)) { // Use base directory config.server = { baseDir: paths.dest }; } - return merge(config, resolve(options)); + 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; } /** @@ -153,3 +167,29 @@ function createViewsArray(views) { '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); +} From 7f452f1fcce2aa62afc7efc3975795a0ae15c87f Mon Sep 17 00:00:00 2001 From: Chauncey McAskill Date: Wed, 23 Mar 2022 13:14:26 -0400 Subject: [PATCH 5/5] Add example of loconfig.local.json --- .gitignore | 1 + loconfig.example.json | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100755 loconfig.example.json diff --git a/.gitignore b/.gitignore index 1d27d65..2e88e73 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules .DS_Store Thumbs.db loconfig.*.json +!loconfig.example.json diff --git a/loconfig.example.json b/loconfig.example.json new file mode 100755 index 0000000..bd7ea91 --- /dev/null +++ b/loconfig.example.json @@ -0,0 +1,8 @@ +{ + "server": { + "https": { + "key": "~/.config/valet/Certificates/{% paths.url %}.key", + "cert": "~/.config/valet/Certificates/{% paths.url %}.crt" + } + } +}