diff --git a/README.md b/README.md index 4cf9754..7b87711 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,8 @@ interface RuntimeOptions { module?: boolean; removeHTMLComments?: boolean; isMinified?: boolean; + initialize?: (sourceFile: SourceFile) => void; + finalize?: (sourceFile: SourceFile) => void; } ``` diff --git a/docs/api/AstAnalyser.md b/docs/api/AstAnalyser.md index 557325f..a4a7dfe 100644 --- a/docs/api/AstAnalyser.md +++ b/docs/api/AstAnalyser.md @@ -64,3 +64,29 @@ type ReportOnFile = { warnings: Warning[]; } ``` + +## Examples + +### `initialize`/`finalize` Hooks + +The `analyse` method allows for the integration of two hooks: `initialize` and `finalize`. +These hooks are triggered before and after the analysis process, respectively. + +Below is an example of how to use these hooks within the `AstAnalyser` class: + +```js +import { AstAnalyser } from "@nodesecure/js-x-ray"; + +const scanner = new AstAnalyser(); + +scanner.analyse("const foo = 'bar';", { + initialize(sourceFile) { + // Code to execute before analysis starts + sourceFile.tracer.trace("Starting analysis..."); + }, + finalize(sourceFile) { + // Code to execute after analysis completes + console.log("Analysis complete."); + } +}); +``` diff --git a/src/AstAnalyser.js b/src/AstAnalyser.js index 15e8a72..9a497f1 100644 --- a/src/AstAnalyser.js +++ b/src/AstAnalyser.js @@ -31,15 +31,23 @@ export class AstAnalyser { const { isMinified = false, module = true, - removeHTMLComments = false + removeHTMLComments = false, + initialize, + finalize } = options; const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), { isEcmaScriptModule: Boolean(module) }); - const source = new SourceFile(str, this.probesOptions); + if (initialize) { + if (typeof initialize !== "function") { + throw new TypeError("options.initialize must be a function"); + } + initialize(source); + } + // we walk each AST Nodes, this is a purely synchronous I/O walk(body, { enter(node) { @@ -55,6 +63,13 @@ export class AstAnalyser { } }); + if (finalize) { + if (typeof finalize !== "function") { + throw new TypeError("options.initialize must be a function"); + } + finalize(source); + } + return { ...source.getResult(isMinified), dependencies: source.dependencies, diff --git a/test/AstAnalyser.spec.js b/test/AstAnalyser.spec.js index 0c611e2..f41b411 100644 --- a/test/AstAnalyser.spec.js +++ b/test/AstAnalyser.spec.js @@ -6,6 +6,7 @@ import { readFileSync } from "node:fs"; // Import Internal Dependencies import { AstAnalyser } from "../src/AstAnalyser.js"; import { JsSourceParser } from "../src/JsSourceParser.js"; +import { SourceFile } from "../src/SourceFile.js"; import { customProbes, getWarningKind, @@ -174,9 +175,86 @@ describe("AstAnalyser", (t) => { assert.equal(result.warnings[0].kind, kWarningUnsafeDanger); assert.equal(result.warnings.length, 1); }); + + describe("hooks", () => { + describe("initialize", () => { + const analyser = new AstAnalyser(); + + it("should throw if initialize is not a function", () => { + assert.throws(() => { + analyser.analyse("const foo = 'bar';", { + initialize: "foo" + }); + }); + }); + + it("should call the initialize function", (t) => { + const initialize = t.mock.fn(); + + analyser.analyse("const foo = 'bar';", { + initialize + }); + + assert.strictEqual(initialize.mock.callCount(), 1); + }); + + it("should pass the source file as first argument", (t) => { + const initialize = t.mock.fn(); + + analyser.analyse("const foo = 'bar';", { + initialize + }); + + assert.strictEqual(initialize.mock.calls[0].arguments[0] instanceof SourceFile, true); + }); + }); + + describe("finalize", () => { + const analyser = new AstAnalyser(); + it("should throw if finalize is not a function", () => { + assert.throws(() => { + analyser.analyse("const foo = 'bar';", { + finalize: "foo" + }); + }); + }); + + it("should call the finalize function", (t) => { + const finalize = t.mock.fn(); + + analyser.analyse("const foo = 'bar';", { + finalize + }); + + assert.strictEqual(finalize.mock.callCount(), 1); + }); + + it("should pass the source file as first argument", (t) => { + const finalize = t.mock.fn(); + + analyser.analyse("const foo = 'bar';", { + finalize + }); + + assert.strictEqual(finalize.mock.calls[0].arguments[0] instanceof SourceFile, true); + }); + }); + + it("intialize should be called before finalize", () => { + const calls = []; + const analyser = new AstAnalyser(); + + analyser.analyse("const foo = 'bar';", { + initialize: () => calls.push("initialize"), + finalize: () => calls.push("finalize") + }); + + assert.deepEqual(calls, ["initialize", "finalize"]); + }); + }); }); - it("remove the packageName from the dependencies list", async() => { + it("remove the packageName from the dependencies list", async () => { const result = await getAnalyser().analyseFile( new URL("depName.js", FIXTURE_URL), { module: false, packageName: "foobar" } @@ -189,7 +267,7 @@ describe("AstAnalyser", (t) => { ); }); - it("should fail with a parsing error", async() => { + it("should fail with a parsing error", async () => { const result = await getAnalyser().analyseFile( new URL("parsingError.js", FIXTURE_URL), { module: false, packageName: "foobar" } @@ -248,8 +326,8 @@ describe("AstAnalyser", (t) => { it("should remove multiple HTML comments", () => { const preparedSource = getAnalyser().prepareSource( "\nconst yo = 'foo'\n", { - removeHTMLComments: true - }); + removeHTMLComments: true + }); assert.strictEqual(preparedSource, "\nconst yo = 'foo'\n"); }); diff --git a/types/api.d.ts b/types/api.d.ts index 255ce7b..51d372f 100644 --- a/types/api.d.ts +++ b/types/api.d.ts @@ -53,6 +53,8 @@ interface RuntimeOptions { * @default false */ isMinified?: boolean; + initialize?: (sourceFile: SourceFile) => void; + finalize?: (sourceFile: SourceFile) => void; } interface RuntimeFileOptions extends Omit {