Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
name: tests
on:
- push

jobs:
build:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- 14.x
- 18.x
- 19.x
- 20.x
- 24.x
- latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- run: npm install-test
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@ faucet-pipeline-core version history
====================================


v3.0.0
------

_2025-03-02_

notable changes for end users:

* bumped Node requirement to v18 or later, dropping support for obsolete versions
* reduced number of dependencies

notable changes for developers:

* switched to named instead of default exports

this might affect users of utilities like `resolvePath` and `FileFinder`

* ensured dynamic imports are asynchronous

this might affect users of utilities like `loadExtension`

* `promisify` was removed entirely after being deprecated for a while


v2.1.0
------

_2025-10-26_

notable changes for end users:

* added support for faucet-pipeline-assets and faucet-pipeline-css
* dropped support for Node 19 and below

no significant changes for developers


v2.0.0
------

Expand Down
10 changes: 6 additions & 4 deletions bin/faucet
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/usr/bin/env node
"use strict";

let faucet = require("../lib");
let parseCLI = require("../lib/cli");
let { faucetDispatch } = require("../lib");
let { parseCLI } = require("../lib/cli");

let { referenceDir, config, options } = parseCLI();
faucet(referenceDir, config, options);
parseCLI().
then(({ referenceDir, config, options }) => {
faucetDispatch(referenceDir, config, options);
});
64 changes: 46 additions & 18 deletions lib/cli.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use strict";

let readConfig = require("./config");
let { readConfig } = require("./config");
let { abort, repr } = require("./util");
let parseArgs = require("minimist");
let { parseArgs } = require("node:util");

let HELP = `
Usage:
Expand All @@ -27,27 +27,55 @@ Options:
serve generated files via HTTP with live reloading
`.trim();

module.exports = function parseCLI(argv = process.argv.slice(2), help = HELP) {
argv = parseArgs(argv, {
boolean: ["watch", "fingerprint", "sourcemaps", "compact"],
alias: {
c: "config",
w: "watch",
h: "help"
exports.parseCLI = async function parseCLI() {
let { values } = parseArgs({
options: {
help: {
type: "boolean",
short: "h",
default: false
},
config: {
type: "string",
short: "c"
},
watch: {
type: "boolean",
short: "w",
default: false
},
fingerprint: {
type: "boolean",
default: false
},
sourcemaps: {
type: "boolean",
default: false
},
compact: {
type: "boolean",
default: false
},
serve: {
type: "string"
},
liveserve: {
type: "string"
}
}
});

if(argv.help) {
abort(help, 0);
if(values.help) {
abort(HELP, 0);
}

let options = {
watch: argv.watch,
fingerprint: argv.fingerprint,
sourcemaps: argv.sourcemaps,
compact: argv.compact,
serve: argv.serve,
liveserve: argv.liveserve
watch: values.watch,
fingerprint: values.fingerprint,
sourcemaps: values.sourcemaps,
compact: values.compact,
serve: values.serve,
liveserve: values.liveserve
};

if(options.watch && options.fingerprint) { // for convenience
Expand All @@ -56,6 +84,6 @@ module.exports = function parseCLI(argv = process.argv.slice(2), help = HELP) {
}

let rootDir = process.cwd();
let { referenceDir, config } = readConfig(rootDir, argv.config);
let { referenceDir, config } = await readConfig(rootDir, values.config);
return { referenceDir, config, options };
};
4 changes: 2 additions & 2 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

let path = require("path");

module.exports = function readConfig(rootDir, filepath = "faucet.config.js") {
exports.readConfig = async function readConfig(rootDir, filepath = "faucet.config.js") {
let configPath = path.resolve(rootDir, filepath);
return {
referenceDir: path.dirname(configPath),
config: require(configPath)
config: await require(configPath)
};
};
20 changes: 11 additions & 9 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

let server = require("./server");
let { pluginsByBucket } = require("./plugins");
let AssetManager = require("./manager");
let resolvePath = require("./util/resolve");
let { AssetManager } = require("./manager");
let { resolvePath } = require("./util/resolve");
let { abort, repr } = require("./util");
let SerializedRunner = require("./util/runner");
let { SerializedRunner } = require("./util/runner");
let browserslist = require("browserslist");

module.exports = async function faucetDispatch(referenceDir, config,
exports.faucetDispatch = async function faucetDispatch(referenceDir, config,
{ watch, fingerprint, sourcemaps, compact, serve, liveserve }) {
config = await config;

Expand All @@ -22,7 +22,7 @@ module.exports = async function faucetDispatch(referenceDir, config,
browsers = [browsers];
}

let plugins = pluginsByBucket(config);
let plugins = await pluginsByBucket(config);
// initialize plugins with corresponding configuration
let buckets = Object.keys(plugins).reduce((memo, bucket) => {
memo[bucket] = plugins[bucket].map(({ plugin, config }) => {
Expand All @@ -40,8 +40,10 @@ module.exports = async function faucetDispatch(referenceDir, config,

if(watch) {
makeWatcher(config.watchDirs, referenceDir).
on("edit", filepaths => {
runner.rerun(filepaths);
then(watcher => {
watcher.on("edit", filepaths => {
runner.rerun(filepaths);
});
});
}

Expand All @@ -60,8 +62,8 @@ function buildStep(plugins) {
then(() => files);
}

function makeWatcher(watchDirs, referenceDir) {
let niteOwl = require("nite-owl");
async function makeWatcher(watchDirs, referenceDir) {
let niteOwl = await require("nite-owl");

if(watchDirs) {
watchDirs = watchDirs.map(dir => resolvePath(dir, referenceDir,
Expand Down
8 changes: 4 additions & 4 deletions lib/manager.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"use strict";

let Manifest = require("./manifest");
let createFile = require("./util/files");
let resolvePath = require("./util/resolve");
let { Manifest } = require("./manifest");
let { createFile } = require("./util/files");
let { resolvePath } = require("./util/resolve");
let { reportFileStatus, abort, generateFingerprint } = require("./util");
let path = require("path");

module.exports = class AssetManager {
exports.AssetManager = class AssetManager {
constructor(referenceDir, { manifestConfig, fingerprint, exitOnError } = {}) {
this.referenceDir = referenceDir;
this.fingerprint = fingerprint;
Expand Down
6 changes: 3 additions & 3 deletions lib/manifest.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"use strict";

let createFile = require("./util/files");
let resolvePath = require("./util/resolve");
let { createFile } = require("./util/files");
let { resolvePath } = require("./util/resolve");
let { abort } = require("./util");
let path = require("path");

module.exports = class Manifest {
exports.Manifest = class Manifest {
constructor(referenceDir, { target, key, value, baseURI, webRoot } = {}) {
if(value && (baseURI || webRoot)) {
abort("ERROR: `value` must not be used with `baseURI` and/or `webRoot`");
Expand Down
51 changes: 31 additions & 20 deletions lib/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ let { loadExtension, abort, repr } = require("./util");

// common plugins included for convenience
let DEFAULTS = [{
key: "assets",
bucket: "static",
plugin: "faucet-pipeline-assets"
}, {
key: "js",
bucket: "scripts",
plugin: "faucet-pipeline-js"
}, {
key: "css",
bucket: "styles",
plugin: "faucet-pipeline-css"
}, {
key: "sass",
bucket: "styles",
Expand All @@ -33,47 +41,50 @@ module.exports = {

// returns plugin functions grouped by bucket and filtered by relevance (based
// on configuration)
function pluginsByBucket(config, defaults) {
let plugins = determinePlugins(config.plugins, defaults);
async function pluginsByBucket(config, defaults) {
let plugins = await determinePlugins(config.plugins, defaults);
let buckets = [...BUCKETS].reduce((memo, bucket) => {
memo[bucket] = [];
return memo;
}, {});
Object.entries(plugins).forEach(([key, _plugin]) => {
for(let [key, _plugin] of Object.entries(plugins)) {
let pluginConfig = config[key];
if(!pluginConfig) {
return;
continue;
}

let { bucket, plugin } = _plugin;
if(!plugin.call) {
({ plugin } = await loadPlugin(plugin));
}
buckets[bucket].push({
plugin: plugin.call ? plugin : loadPlugin(plugin).plugin,
plugin,
config: pluginConfig
});
});
}
return buckets;
}

// `plugins` is an array of plugins, each either a package identifier or a
// `{ key, bucket, plugin }` object`, with `key` being the configuration key and
// `plugin` being either a function or a package identifier
function determinePlugins(plugins = [], defaults = DEFAULTS) {
async function determinePlugins(plugins = [], defaults = DEFAULTS) {
let registry = {};
// NB: default plugins are resolved lazily because eager loading would
// result in them becoming a hard dependency rather than a convenience
// preset - however, that requires us to duplicate the respective
// configuration keys and buckets here
defaults.forEach(plugin => {
registerPlugin(registry, plugin, false);
});
plugins.forEach(plugin => {
registerPlugin(registry, plugin, true);
});
for(let plugin of defaults) {
await registerPlugin(registry, plugin, false);
}
for(let plugin of plugins) {
await registerPlugin(registry, plugin, true);
}
return registry;
}

function registerPlugin(registry, _plugin, eager) {
let { key, bucket, plugin } = resolvePlugin(_plugin, eager);
async function registerPlugin(registry, _plugin, eager) {
let { key, bucket, plugin } = await resolvePlugin(_plugin, eager);
// NB: default plugins may be overridden
if(registry[key] && !DEFAULT_KEYS.has(key)) {
abort(`ERROR: duplicate plugin key ${repr(key, false)}`);
Expand All @@ -84,14 +95,14 @@ function registerPlugin(registry, _plugin, eager) {
}
}

function resolvePlugin(_plugin, eager) {
async function resolvePlugin(_plugin, eager) {
if(_plugin.substr) { // package identifier
_plugin = loadPlugin(_plugin);
_plugin = await loadPlugin(_plugin);
}

let { key, bucket, plugin } = _plugin;
if(eager && plugin.substr && (!key || !bucket)) { // auto-configuration
let _plugin = loadPlugin(plugin);
let _plugin = await loadPlugin(plugin);
plugin = _plugin.plugin;
// local configuration takes precedence
key = key || _plugin.key;
Expand All @@ -104,13 +115,13 @@ function resolvePlugin(_plugin, eager) {
return { key, bucket, plugin };
}

function loadPlugin(pkg) {
async function loadPlugin(pkg) {
let fail = prop => abort(`ERROR: invalid plugin ${repr(pkg)}; ` +
`missing ${repr(prop, false)}`);
let {
key = fail("key"),
bucket = fail("bucket"),
plugin = fail("plugin")
} = loadExtension(pkg, "ERROR: missing plugin");
} = await loadExtension(pkg, "ERROR: missing plugin");
return { key, bucket, plugin };
}
Loading