Skip to content
Draft
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
26 changes: 8 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 { abort, repr } = require("./util");
let { parseArgs } = require("node:util");
let path = require("node:path");

let HELP = `
Usage:
Expand Down Expand Up @@ -37,7 +37,8 @@ exports.parseCLI = async function parseCLI() {
},
config: {
type: "string",
short: "c"
short: "c",
default: "faucet.config.js"
},
watch: {
type: "boolean",
Expand Down Expand Up @@ -69,21 +70,10 @@ exports.parseCLI = async function parseCLI() {
abort(HELP, 0);
}

let options = {
watch: values.watch,
fingerprint: values.fingerprint,
sourcemaps: values.sourcemaps,
compact: values.compact,
serve: values.serve,
liveserve: values.liveserve
let configPath = path.resolve(process.cwd(), values.config);
return {
referenceDir: path.dirname(configPath),
config: await require(configPath),
options: values
};

if(options.watch && options.fingerprint) { // for convenience
console.error("you might consider disabling fingerprinting in watch " +
"mode to avoid littering your file system with obsolete bundles");
}

let rootDir = process.cwd();
let { referenceDir, config } = await readConfig(rootDir, values.config);
return { referenceDir, config, options };
};
11 changes: 0 additions & 11 deletions lib/config.js

This file was deleted.

21 changes: 10 additions & 11 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
let server = require("./server");
let { pluginsByBucket } = require("./plugins");
let { AssetManager } = require("./manager");
let { resolvePath } = require("./util/resolve");
let { abort, repr } = require("./util");
let { SerializedRunner } = require("./util/runner");
let { abort, repr, resolvePath } = require("./util");
let { SerializedRunner } = require("./runner");
let browserslist = require("browserslist");

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

let assetManager = new AssetManager(referenceDir, {
manifestConfig: config.manifest,
fingerprint,
Expand Down Expand Up @@ -39,12 +36,14 @@ exports.faucetDispatch = async function faucetDispatch(referenceDir, config,
let res = runner.run();

if(watch) {
makeWatcher(config.watchDirs, referenceDir).
then(watcher => {
watcher.on("edit", filepaths => {
runner.rerun(filepaths);
});
});
if(fingerprint) {
console.error("you might consider disabling fingerprinting in watch " +
"mode to avoid littering your file system with obsolete bundles");
}
let watcher = await makeWatcher(config.watchDirs, referenceDir);
watcher.on("edit", filepaths => {
runner.rerun(filepaths);
});
}

if(serve && liveserve) {
Expand Down
29 changes: 19 additions & 10 deletions lib/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

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

exports.AssetManager = class AssetManager {
constructor(referenceDir, { manifestConfig, fingerprint, exitOnError } = {}) {
Expand Down Expand Up @@ -43,18 +43,27 @@ exports.AssetManager = class AssetManager {
});
}

get packagesDir() {
let memo = this._packagesDir;
if(!memo) {
memo = this._packagesDir = this.resolvePath("./node_modules");
}
return memo;
}

_updateManifest(originalPath, actualPath, targetDir) {
let { referenceDir } = this;
originalPath = path.relative(referenceDir, originalPath);
actualPath = path.relative(referenceDir, actualPath);
return this.manifest.set(originalPath, actualPath, targetDir);
}
};

function generateFingerprint(filepath, data) {
let filename = path.basename(filepath);
let ext = filename.indexOf(".") === -1 ? "" : "." + filename.split(".").pop();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we not using path.extname here? (See potentially related discussion for faucet-static/static-images.)

(Though we might reasonably opt not to risk changing the implementation here, due to backwards-compatibility concerns.)

let name = ext.length === 0 ? filename : path.basename(filepath, ext);
let hash = generateHash(data);
return path.join(path.dirname(filepath), `${name}-${hash}${ext}`);
}

// exported for testing
exports.generateFingerprint = generateFingerprint;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
exports.generateFingerprint = generateFingerprint;
exports._generateFingerprint = generateFingerprint;

That makes it explicit; cf. _determinePlugins.


function generateHash(str) {
let hash = crypto.createHash("md5");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we perhaps change this to SHA-256? I realize that might be a backwards-compatibility concern though...

My results suggest that you should probably not be using MD5. MD5 is slower than SHA-256 and not as safe.

JavaScript hashing speed comparison: MD5 versus SHA-256

hash.update(str);
return hash.digest("hex");
}
3 changes: 1 addition & 2 deletions lib/manifest.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"use strict";

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

exports.Manifest = class Manifest {
Expand Down
21 changes: 2 additions & 19 deletions lib/util/runner.js → lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ exports.SerializedRunner = class SerializedRunner {

run(...args) {
if(!this._pending) { // prevent concurrent execution
this._pending = augment(this.asyncOp(...args)).
this._pending = this.asyncOp(...args).
finally(() => {
this._pending = null;
});
Expand All @@ -24,7 +24,7 @@ exports.SerializedRunner = class SerializedRunner {
let enqueue = this._pending;
let res = this.run(...args);
if(enqueue) {
this._queued = res = augment(res).
this._queued = res = res.
finally(() => {
this._queued = null;
}).
Expand All @@ -33,20 +33,3 @@ exports.SerializedRunner = class SerializedRunner {
return res;
}
};

function augment(promise) {
promise.finally = always;
return promise;
}

// poor man's `Promise#finally` polyfill
function always(fn) {
return this.
then(res => {
fn();
return res;
}, err => {
fn();
throw err;
});
}
File renamed without changes.
59 changes: 0 additions & 59 deletions lib/util/files/finder.js

This file was deleted.

42 changes: 25 additions & 17 deletions lib/util/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
"use strict";

let fs = require("fs");
let path = require("path");
Comment on lines +3 to 4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well do this while we're at it:

Suggested change
let fs = require("fs");
let path = require("path");
let fs = require("node:fs");
let path = require("node:path");

let crypto = require("crypto");

exports.abort = abort;
exports.repr = repr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I had intentionally moved exports to the top (yay hoisting) so you don't have to go searching for them in the middle of random implementation details. 🤷


// reports success or failure for a given file path (typically regarding
// compilation or write operations)
Expand All @@ -26,28 +23,39 @@ exports.loadExtension = async (pkg, errorMessage, supplier = pkg) => {
}
};

exports.generateFingerprint = (filepath, data) => {
let filename = path.basename(filepath);
let ext = filename.indexOf(".") === -1 ? "" : "." + filename.split(".").pop();
let name = ext.length === 0 ? filename : path.basename(filepath, ext);
let hash = generateHash(data);
return path.join(path.dirname(filepath), `${name}-${hash}${ext}`);
};

function abort(msg, code = 1) {
console.error(msg);
process.exit(code);
}
exports.abort = abort;

function repr(value, jsonify = true) {
if(jsonify) {
value = JSON.stringify(value);
}
return `\`${value}\``;
}
exports.repr = repr;

function generateHash(str) {
let hash = crypto.createHash("md5");
hash.update(str);
return hash.digest("hex");
}
exports.resolvePath = (filepath, referenceDir, { enforceRelative } = {}) => {
if(/^\.?\.\//.test(filepath)) { // starts with `./` or `../`
return path.resolve(referenceDir, filepath);
} else if(enforceRelative) {
abort(`ERROR: path must be relative: ${repr(filepath)}`);
} else { // attempt via Node resolution algorithm
try {
return require.resolve(filepath, { paths: [referenceDir] });
} catch(err) {
// attempt to resolve non-JavaScript package references by relying
// on typical package paths (simplistic approximation of Node's
// resolution algorithm)
let resolved = path.resolve(referenceDir, "node_modules", filepath);
try {
fs.statSync(resolved); // ensures file/directory exists
} catch(_err) {
abort(`ERROR: could not resolve ${repr(filepath)}`);
}
return resolved;
}
}
};
29 changes: 0 additions & 29 deletions lib/util/resolve.js

This file was deleted.

17 changes: 16 additions & 1 deletion test/test_manager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";

let { AssetManager } = require("../lib/manager");
let { AssetManager, generateFingerprint } = require("../lib/manager");
let { describe, it, before, after } = require("node:test");
let path = require("path");
let assert = require("assert");
Expand Down Expand Up @@ -51,3 +51,18 @@ describe("asset manager", () => {
}, /exit 1/);
});
});

describe("fingerprinting", () => {
it("generates a content-dependent hash", () => {
let fingerprint = generateFingerprint("/path/to/foo.js", "lorem ipsum");
assertSame(fingerprint, "/path/to/foo-80a751fde577028640c419000e33eba6.js");

fingerprint = generateFingerprint("/path/to/bar.js", "dolor sit amet");
assertSame(fingerprint, "/path/to/bar-7afed6210e0b8fce023f06abd4490fa0.js");
});

it("supports files without extension", () => {
let fingerprint = generateFingerprint("/path/to/baz", "lipsum");
assertSame(fingerprint, "/path/to/baz-8047cfaac755e5c7f77af066123980a5");
});
});
2 changes: 1 addition & 1 deletion test/test_runner.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";

let { SerializedRunner } = require("../lib/util/runner");
let { SerializedRunner } = require("../lib/runner");
let { describe, it } = require("node:test");
let { strictEqual: assertSame, deepStrictEqual: assertDeep } = require("assert");

Expand Down
Loading