From 55f52fa5d39a1527ea304927d35166a4ae56a5ba Mon Sep 17 00:00:00 2001 From: Jean <110341611+jean-michelet@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:05:27 +0200 Subject: [PATCH] Create EntryFilesAnalyzer class to analyze a set of entry files (#258) * feat: create EntryFilesAnalyzer class to analyze a set of entry files asynchronously, yielding analysis reports * fix: return early if existing extension is not allowed * refactor: avoid useless asignantions in tests files * fix: fixtures must end with 1 blank line * test: asserts all reports have been yielded at the end of tests * fix: JsDoc for EntryFilesAnalyzer.analyze should have string|Url union * types: add api types * fix: import order * fix: typo in mjs module, it is an externalDep * fix: typo in .cjs .js modules, it is an externalDep --- index.d.ts | 8 ++ src/EntryFilesAnalyser.js | 99 +++++++++++++++++++ test/EntryFilesAnalyser.spec.js | 95 ++++++++++++++++++ test/fixtures/entryFiles/deps/deepEntry.js | 3 + test/fixtures/entryFiles/deps/default.cjs | 1 + test/fixtures/entryFiles/deps/default.js | 1 + test/fixtures/entryFiles/deps/default.jsx | 7 ++ test/fixtures/entryFiles/deps/default.mjs | 1 + test/fixtures/entryFiles/deps/default.node | 1 + test/fixtures/entryFiles/deps/dep1.js | 2 + test/fixtures/entryFiles/deps/dep2.js | 3 + test/fixtures/entryFiles/deps/dep3.js | 3 + test/fixtures/entryFiles/deps/invalidDep.js | 1 + test/fixtures/entryFiles/deps/validDep.js | 1 + test/fixtures/entryFiles/entry.js | 2 + .../entryFiles/entryWithInvalidDep.js | 2 + .../entryWithRequireDepWithExtension.js | 2 + .../entryWithVariousDepExtensions.js | 5 + test/fixtures/entryFiles/shared.js | 1 + types/api.d.ts | 17 ++++ 20 files changed, 255 insertions(+) create mode 100644 src/EntryFilesAnalyser.js create mode 100644 test/EntryFilesAnalyser.spec.js create mode 100644 test/fixtures/entryFiles/deps/deepEntry.js create mode 100644 test/fixtures/entryFiles/deps/default.cjs create mode 100644 test/fixtures/entryFiles/deps/default.js create mode 100644 test/fixtures/entryFiles/deps/default.jsx create mode 100644 test/fixtures/entryFiles/deps/default.mjs create mode 100644 test/fixtures/entryFiles/deps/default.node create mode 100644 test/fixtures/entryFiles/deps/dep1.js create mode 100644 test/fixtures/entryFiles/deps/dep2.js create mode 100644 test/fixtures/entryFiles/deps/dep3.js create mode 100644 test/fixtures/entryFiles/deps/invalidDep.js create mode 100644 test/fixtures/entryFiles/deps/validDep.js create mode 100644 test/fixtures/entryFiles/entry.js create mode 100644 test/fixtures/entryFiles/entryWithInvalidDep.js create mode 100644 test/fixtures/entryFiles/entryWithRequireDepWithExtension.js create mode 100644 test/fixtures/entryFiles/entryWithVariousDepExtensions.js create mode 100644 test/fixtures/entryFiles/shared.js diff --git a/index.d.ts b/index.d.ts index e29aa50..cb75030 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,10 @@ import { AstAnalyser, + AstAnalyserOptions, + + EntryFilesAnalyser, + EntryFilesAnalyserOptions, + SourceParser, runASTAnalysis, runASTAnalysisOnFile, @@ -23,6 +28,9 @@ declare const warnings: Record { + it("should analyze internal dependencies recursively", async(t) => { + const entryFilesAnalyser = new EntryFilesAnalyser(); + const entryUrl = new URL("entry.js", FIXTURE_URL); + const deepEntryUrl = new URL("deps/deepEntry.js", FIXTURE_URL); + + t.mock.method(AstAnalyser.prototype, "analyseFile"); + const generator = entryFilesAnalyser.analyse([entryUrl, deepEntryUrl]); + + // First entry + await assertReport(generator, entryUrl, true); + await assertReport(generator, new URL("deps/dep1.js", FIXTURE_URL), true); + await assertReport(generator, new URL("shared.js", FIXTURE_URL), true); + await assertReport(generator, new URL("deps/dep2.js", FIXTURE_URL), true); + + // Second entry + await assertReport(generator, deepEntryUrl, true); + await assertReport(generator, new URL("deps/dep3.js", FIXTURE_URL), true); + + await assertAllReportsYielded(generator); + + // Check that shared dependencies are not analyzed several times + const calls = AstAnalyser.prototype.analyseFile.mock.calls; + assert.strictEqual(calls.length, 6); + }); + + it("should detect internal deps that failed to be analyzed", async() => { + const entryFilesAnalyser = new EntryFilesAnalyser(); + const entryUrl = new URL("entryWithInvalidDep.js", FIXTURE_URL); + + const generator = entryFilesAnalyser.analyse([entryUrl]); + + await assertReport(generator, entryUrl, true); + + const invalidDepReport = await generator.next(); + assert.ok(!invalidDepReport.value.ok); + assert.strictEqual(invalidDepReport.value.url, new URL("deps/invalidDep.js", FIXTURE_URL).pathname); + assert.strictEqual(invalidDepReport.value.warnings[0].kind, "parsing-error"); + + await assertReport(generator, new URL("deps/dep1.js", FIXTURE_URL), true); + await assertReport(generator, new URL("shared.js", FIXTURE_URL), true); + + await assertAllReportsYielded(generator); + }); + + it("should extends default extensions", async() => { + const entryFilesAnalyser = new EntryFilesAnalyser({ + loadExtensions: (exts) => [...exts, "jsx"] + }); + const entryUrl = new URL("entryWithVariousDepExtensions.js", FIXTURE_URL); + const generator = entryFilesAnalyser.analyse([entryUrl]); + + await assertReport(generator, entryUrl, true); + await assertReport(generator, new URL("deps/default.js", FIXTURE_URL), true); + await assertReport(generator, new URL("deps/default.cjs", FIXTURE_URL), true); + await assertReport(generator, new URL("deps/default.mjs", FIXTURE_URL), true); + await assertReport(generator, new URL("deps/default.node", FIXTURE_URL), true); + await assertReport(generator, new URL("deps/default.jsx", FIXTURE_URL), true); + + await assertAllReportsYielded(generator); + }); + + it("should override default extensions", async() => { + const entryFilesAnalyser = new EntryFilesAnalyser({ + loadExtensions: () => ["jsx"] + }); + const entryUrl = new URL("entryWithVariousDepExtensions.js", FIXTURE_URL); + const generator = entryFilesAnalyser.analyse([entryUrl]); + + await assertReport(generator, entryUrl, true); + await assertReport(generator, new URL("deps/default.jsx", FIXTURE_URL), true); + + await assertAllReportsYielded(generator); + }); + + async function assertReport(generator, expectedUrl, expectedOk) { + const report = await generator.next(); + assert.strictEqual(report.value.url, expectedUrl.pathname); + assert.strictEqual(report.value.ok, expectedOk); + } + + async function assertAllReportsYielded(generator) { + assert.strictEqual((await generator.next()).value, undefined); + } +}); diff --git a/test/fixtures/entryFiles/deps/deepEntry.js b/test/fixtures/entryFiles/deps/deepEntry.js new file mode 100644 index 0000000..13abb6b --- /dev/null +++ b/test/fixtures/entryFiles/deps/deepEntry.js @@ -0,0 +1,3 @@ +require("./dep1"); +require("./dep2"); +require("./dep3"); diff --git a/test/fixtures/entryFiles/deps/default.cjs b/test/fixtures/entryFiles/deps/default.cjs new file mode 100644 index 0000000..7c72f39 --- /dev/null +++ b/test/fixtures/entryFiles/deps/default.cjs @@ -0,0 +1 @@ +require('externalDep') diff --git a/test/fixtures/entryFiles/deps/default.js b/test/fixtures/entryFiles/deps/default.js new file mode 100644 index 0000000..7c72f39 --- /dev/null +++ b/test/fixtures/entryFiles/deps/default.js @@ -0,0 +1 @@ +require('externalDep') diff --git a/test/fixtures/entryFiles/deps/default.jsx b/test/fixtures/entryFiles/deps/default.jsx new file mode 100644 index 0000000..607e7bd --- /dev/null +++ b/test/fixtures/entryFiles/deps/default.jsx @@ -0,0 +1,7 @@ +import React from 'react' + +export default function Foo() { + return ( +
+ ) +} diff --git a/test/fixtures/entryFiles/deps/default.mjs b/test/fixtures/entryFiles/deps/default.mjs new file mode 100644 index 0000000..a2a416a --- /dev/null +++ b/test/fixtures/entryFiles/deps/default.mjs @@ -0,0 +1 @@ +import externalDep from 'externalDep'; diff --git a/test/fixtures/entryFiles/deps/default.node b/test/fixtures/entryFiles/deps/default.node new file mode 100644 index 0000000..2138e72 --- /dev/null +++ b/test/fixtures/entryFiles/deps/default.node @@ -0,0 +1 @@ +module.exports = require('some/addon'); diff --git a/test/fixtures/entryFiles/deps/dep1.js b/test/fixtures/entryFiles/deps/dep1.js new file mode 100644 index 0000000..8c3b81e --- /dev/null +++ b/test/fixtures/entryFiles/deps/dep1.js @@ -0,0 +1,2 @@ +require("../shared"); +require("externalDep"); diff --git a/test/fixtures/entryFiles/deps/dep2.js b/test/fixtures/entryFiles/deps/dep2.js new file mode 100644 index 0000000..3861a62 --- /dev/null +++ b/test/fixtures/entryFiles/deps/dep2.js @@ -0,0 +1,3 @@ +require("../shared"); +require("../shared.js"); +require("externalDep"); diff --git a/test/fixtures/entryFiles/deps/dep3.js b/test/fixtures/entryFiles/deps/dep3.js new file mode 100644 index 0000000..3861a62 --- /dev/null +++ b/test/fixtures/entryFiles/deps/dep3.js @@ -0,0 +1,3 @@ +require("../shared"); +require("../shared.js"); +require("externalDep"); diff --git a/test/fixtures/entryFiles/deps/invalidDep.js b/test/fixtures/entryFiles/deps/invalidDep.js new file mode 100644 index 0000000..4ce2a64 --- /dev/null +++ b/test/fixtures/entryFiles/deps/invalidDep.js @@ -0,0 +1 @@ +@invalidJs diff --git a/test/fixtures/entryFiles/deps/validDep.js b/test/fixtures/entryFiles/deps/validDep.js new file mode 100644 index 0000000..1b95931 --- /dev/null +++ b/test/fixtures/entryFiles/deps/validDep.js @@ -0,0 +1 @@ +require("externalDep"); diff --git a/test/fixtures/entryFiles/entry.js b/test/fixtures/entryFiles/entry.js new file mode 100644 index 0000000..e355112 --- /dev/null +++ b/test/fixtures/entryFiles/entry.js @@ -0,0 +1,2 @@ +require("./deps/dep1"); +require("./deps/dep2.js"); // keep extension for testing purpose diff --git a/test/fixtures/entryFiles/entryWithInvalidDep.js b/test/fixtures/entryFiles/entryWithInvalidDep.js new file mode 100644 index 0000000..3653908 --- /dev/null +++ b/test/fixtures/entryFiles/entryWithInvalidDep.js @@ -0,0 +1,2 @@ +require("./deps/invalidDep"); +require("./deps/dep1"); diff --git a/test/fixtures/entryFiles/entryWithRequireDepWithExtension.js b/test/fixtures/entryFiles/entryWithRequireDepWithExtension.js new file mode 100644 index 0000000..12aede0 --- /dev/null +++ b/test/fixtures/entryFiles/entryWithRequireDepWithExtension.js @@ -0,0 +1,2 @@ +require("./deps/dep1.js"); +require("./deps/dep1"); diff --git a/test/fixtures/entryFiles/entryWithVariousDepExtensions.js b/test/fixtures/entryFiles/entryWithVariousDepExtensions.js new file mode 100644 index 0000000..3550299 --- /dev/null +++ b/test/fixtures/entryFiles/entryWithVariousDepExtensions.js @@ -0,0 +1,5 @@ +require("./deps/default.js"); +require("./deps/default.cjs"); +require("./deps/default.mjs"); +require("./deps/default.node"); +require("./deps/default.jsx"); diff --git a/test/fixtures/entryFiles/shared.js b/test/fixtures/entryFiles/shared.js new file mode 100644 index 0000000..1b95931 --- /dev/null +++ b/test/fixtures/entryFiles/shared.js @@ -0,0 +1 @@ +require("externalDep"); diff --git a/types/api.d.ts b/types/api.d.ts index 27d7ac4..6d556d5 100644 --- a/types/api.d.ts +++ b/types/api.d.ts @@ -3,6 +3,9 @@ import { Statement } from "meriyah/dist/src/estree.js"; export { AstAnalyser, + AstAnalyserOptions, + EntryFilesAnalyser, + EntryFilesAnalyserOptions, SourceParser, runASTAnalysis, runASTAnalysisOnFile, @@ -101,5 +104,19 @@ declare class AstAnalyser { analyzeFile(pathToFile: string, options?: RuntimeFileOptions): Promise; } +interface EntryFilesAnalyserOptions { + astAnalyzer?: AstAnalyser; + loadExtensions?: (defaults: string[]) => string[]; +} + +declare class EntryFilesAnalyser { + constructor(options?: EntryFilesAnalyserOptions); + + /** + * Asynchronously analyze a set of entry files yielding analysis reports. + */ + analyse(entryFiles: (string | URL)[]): AsyncGenerator; +} + declare function runASTAnalysis(str: string, options?: RuntimeOptions & AstAnalyserOptions): Report; declare function runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions & AstAnalyserOptions): Promise;