Skip to content
Open
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
135 changes: 135 additions & 0 deletions lib/TracePlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Aman Thakur @jhonsnow456
*/

"use strict";

/** @typedef {import("./Resolver")} Resolver */
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */

/**
* @typedef {object} TraceRequest
* @property {string} path resolving directory
* @property {string} request request string
* @property {string} query query string
* @property {string} fragment fragment string
* @property {boolean} module is a module request
* @property {boolean} directory is a directory request
* @property {string=} descriptionFilePath path to description file if found
* @property {string=} relativePath relative path from description file
*/

/**
* @typedef {object} TraceEntry
* @property {string} hook which pipeline hook fired
* @property {TraceRequest} request snapshot of the request at this step
* @property {string | false | null} result what this step produced (null = skipped)
* @property {number} timestamp Date.now() when the step ran
*/

/**
* A collector that stores structured trace entries during resolution.
*/
class TraceCollector {
constructor() {
/** @type {TraceEntry[]} */
this._entries = [];
}

/**
* @param {string} hook hook name
* @param {ResolveRequest} request the request object
* @param {string | false | null} result result from this step
* @returns {void}
*/
_addEntry(hook, request, result) {
/** @type {TraceRequest} */
const requestSnapshot = {
path: request.path || "",
request: request.request || "",
query: request.query || "",
fragment: request.fragment || "",
module: Boolean(request.module),
directory: Boolean(request.directory),
};
if (request.descriptionFilePath) {
requestSnapshot.descriptionFilePath = request.descriptionFilePath;
}
if (request.relativePath) {
requestSnapshot.relativePath = request.relativePath;
}

this._entries.push({
hook,
request: requestSnapshot,
result: result === undefined ? null : result,
timestamp: Date.now(),
});
}

/**
* Returns all collected trace entries.
* @returns {TraceEntry[]} entries
*/
getEntries() {
return this._entries;
}

/**
* Clears all collected entries.
* @returns {void}
*/
clear() {
this._entries.length = 0;
}
}

/**
* A plugin that records structured trace entries for every resolution step.
* Tap into `resolveStep` and `result` hooks to build a machine-readable
* trace of the resolution pipeline.
* @example
* const trace = TracePlugin.createCollector();
* const resolver = ResolverFactory.createResolver({
* fileSystem: cachedFs,
* plugins: [new TracePlugin(trace)],
* });
* resolver.resolve({}, __dirname, "lodash/merge", {}, (err, result) => {
* console.log(trace.getEntries());
* });
*/
class TracePlugin {
/**
* @param {TraceCollector} collector collector to write entries into
*/
constructor(collector) {
this.collector = collector;
}

/**
* Creates a new TraceCollector instance.
* @returns {TraceCollector} collector
*/
static createCollector() {
return new TraceCollector();
}

/**
* @param {Resolver} resolver the resolver
* @returns {void}
*/
apply(resolver) {
const { collector } = this;

resolver.hooks.resolveStep.tap("TracePlugin", (hook, request) => {
collector._addEntry(/** @type {string} */ (hook.name), request, null);
});

resolver.hooks.result.tap("TracePlugin", (request) => {
collector._addEntry("result", request, request.path);
});
}
}

module.exports = TracePlugin;
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ module.exports = mergeExports(resolve, {
get LogInfoPlugin() {
return require("./LogInfoPlugin");
},
get TracePlugin() {
return require("./TracePlugin");
},
get TsconfigPathsPlugin() {
return require("./TsconfigPathsPlugin");
},
Expand Down
202 changes: 202 additions & 0 deletions test/trace-plugin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"use strict";

const assert = require("assert");
const fs = require("fs");

const path = require("path");
const { CachedInputFileSystem, ResolverFactory, TracePlugin } = require("../");
const { describe, it } = require("./_runner");

const fixtures = path.join(__dirname, "fixtures");
const nodeFileSystem = new CachedInputFileSystem(fs, 4000);

describe("TracePlugin", () => {
it("records trace entries for a successful resolve", (t, done) => {
const trace = TracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new TracePlugin(trace)],
});

resolver.resolve({}, fixtures, "./a.js", {}, (err, result) => {
if (err) return done(err);
assert.ok(result);
const entries = trace.getEntries();
assert.ok(entries.length > 0);
done();
});
});

it("each entry has hook, request, result, and timestamp", (t, done) => {
const trace = TracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new TracePlugin(trace)],
});

resolver.resolve({}, fixtures, "./a.js", {}, (err) => {
if (err) return done(err);
const entries = trace.getEntries();
for (const entry of entries) {
assert.strictEqual(typeof entry.hook, "string");
assert.ok(entry.request);
assert.strictEqual(typeof entry.request.path, "string");
assert.strictEqual(typeof entry.request.request, "string");
assert.strictEqual(typeof entry.request.query, "string");
assert.strictEqual(typeof entry.request.fragment, "string");
assert.strictEqual(typeof entry.request.module, "boolean");
assert.strictEqual(typeof entry.request.directory, "boolean");
assert.strictEqual(typeof entry.timestamp, "number");
assert.ok(entry.timestamp > 0);
}
done();
});
});

it("includes a result entry with the resolved path", (t, done) => {
const trace = TracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new TracePlugin(trace)],
});

resolver.resolve({}, fixtures, "./a.js", {}, (err, _result) => {
if (err) return done(err);
const entries = trace.getEntries();
const resultEntry = entries.find((entry) => entry.hook === "result");
assert.ok(resultEntry);
assert.strictEqual(resultEntry.result, path.join(fixtures, "a.js"));
done();
});
});

it("records module flag for bare module requests", (t, done) => {
const trace = TracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new TracePlugin(trace)],
});

resolver.resolve({}, fixtures, "m1/a", {}, (err, _result) => {
if (err) return done(err);
assert.ok(_result);
const entries = trace.getEntries();
const moduleEntry = entries.find((entry) => entry.request.module);
assert.ok(moduleEntry, "expected at least one entry with module=true");
done();
});
});

it("records directory flag for directory requests", (t, done) => {
const trace = TracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new TracePlugin(trace)],
});

resolver.resolve({}, fixtures, "./main-field-self/", {}, (err, _result) => {
if (err) return done(err);
assert.ok(_result);
const entries = trace.getEntries();
const dirEntry = entries.find((entry) => entry.request.directory);
assert.ok(dirEntry, "expected at least one entry with directory=true");
done();
});
});

it("collector.clear() removes all entries", (t, done) => {
const trace = TracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new TracePlugin(trace)],
});

resolver.resolve({}, fixtures, "./a.js", {}, (err) => {
if (err) return done(err);
assert.ok(trace.getEntries().length > 0);
trace.clear();
assert.strictEqual(trace.getEntries().length, 0);
done();
});
});

it("entries have timestamps that are non-decreasing", (t, done) => {
const trace = TracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new TracePlugin(trace)],
});

resolver.resolve({}, fixtures, "./a.js", {}, (err) => {
if (err) return done(err);
const entries = trace.getEntries();
for (let i = 1; i < entries.length; i++) {
assert.ok(
entries[i].timestamp >= entries[i - 1].timestamp,
`entry ${i} timestamp should be >= entry ${i - 1}`,
);
}
done();
});
});

it("works with resolveSync", () => {
const trace = TracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
useSyncFileSystemCalls: true,
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new TracePlugin(trace)],
});

const result = resolver.resolveSync({}, fixtures, "./a.js");
assert.ok(result);
const entries = trace.getEntries();
assert.ok(entries.length > 0);
const resultEntry = entries.find((entry) => entry.hook === "result");
assert.ok(resultEntry);
assert.strictEqual(resultEntry.result, path.join(fixtures, "a.js"));
});

it("works with resolvePromise", async () => {
const trace = TracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new TracePlugin(trace)],
});

const result = await resolver.resolvePromise({}, fixtures, "./a.js");
assert.ok(result);
const entries = trace.getEntries();
assert.ok(entries.length > 0);
const resultEntry = entries.find((entry) => entry.hook === "result");
assert.ok(resultEntry);
assert.strictEqual(resultEntry.result, path.join(fixtures, "a.js"));
});

it("captured from the index export", (t, done) => {
const { TracePlugin: IndexTracePlugin } = require("../");

const trace = IndexTracePlugin.createCollector();
const resolver = ResolverFactory.createResolver({
fileSystem: nodeFileSystem,
extensions: [".js"],
plugins: [new IndexTracePlugin(trace)],
});

resolver.resolve({}, fixtures, "./a.js", {}, (err, result) => {
if (err) return done(err);
assert.ok(result);
assert.ok(trace.getEntries().length > 0);
done();
});
});
});
Loading