Skip to content

Commit

Permalink
feat(EntryFilesAnalyser): implement digraph-js (#293)
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxken authored Aug 16, 2024
1 parent bd410ea commit 6c6279c
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 46 deletions.
14 changes: 13 additions & 1 deletion docs/api/EntryFilesAnalyser.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The constructor options is described by the following TS interface
interface EntryFilesAnalyserOptions {
astAnalyzer?: AstAnalyser;
loadExtensions?: (defaults: string[]) => string[];
rootPath?: string | URL;
}
```

Expand All @@ -30,8 +31,19 @@ Default files extensions are `.js`, `.cjs`, `.mjs` and `.node`

```ts
declare class EntryFilesAnalyser {
public astAnalyzer: AstAnalyser;
public allowedExtensions: Set<string>;
public dependencies: DiGraph<VertexDefinition<VertexBody>>;

constructor(options?: EntryFilesAnalyserOptions);
analyse(entryFiles: (string | URL)[]): AsyncGenerator<ReportOnFile & { url: string }>;

/**
* Asynchronously analyze a set of entry files yielding analysis reports.
*/
analyse(
entryFiles: Iterable<string | URL>,
options?: RuntimeFileOptions
): AsyncGenerator<ReportOnFile & { file: string }>;
}
```

Expand Down
12 changes: 6 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ function runASTAnalysis(
options = Object.create(null)
) {
process.emitWarning(
'The runASTAnalysis API is deprecated and will be removed in v8. Please use the AstAnalyser class instead.',
"The runASTAnalysis API is deprecated and will be removed in v8. Please use the AstAnalyser class instead.",
{
code: 'DeprecationWarning',
detail: 'The runASTAnalysis API is deprecated and will be removed in v8. Please use the AstAnalyser class instead.'
code: "DeprecationWarning",
detail: "The runASTAnalysis API is deprecated and will be removed in v8. Please use the AstAnalyser class instead."
}
);

Expand Down Expand Up @@ -43,10 +43,10 @@ async function runASTAnalysisOnFile(
options = {}
) {
process.emitWarning(
'The runASTAnalysisOnFile API is deprecated and will be removed in v8. Please use the AstAnalyser class instead.',
"The runASTAnalysisOnFile API is deprecated and will be removed in v8. Please use the AstAnalyser class instead.",
{
code: 'DeprecationWarning',
detail: 'The runASTAnalysisOnFile API is deprecated and will be removed in v8. Please use the AstAnalyser class instead.'
code: "DeprecationWarning",
detail: "The runASTAnalysisOnFile API is deprecated and will be removed in v8. Please use the AstAnalyser class instead."
}
);

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"dependencies": {
"@nodesecure/estree-ast-utils": "^1.3.1",
"@nodesecure/sec-literal": "^1.2.0",
"digraph-js": "^2.2.3",
"estree-walker": "^3.0.1",
"frequency-set": "^1.0.2",
"is-minified-code": "^2.0.0",
Expand Down
115 changes: 87 additions & 28 deletions src/EntryFilesAnalyser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,116 @@ import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

// Import Third-party Dependencies
import { DiGraph } from "digraph-js";

// Import Internal Dependencies
import { AstAnalyser } from "./AstAnalyser.js";

// CONSTANTS
const kDefaultExtensions = ["js", "cjs", "mjs", "node"];

export class EntryFilesAnalyser {
#rootPath = null;

constructor(options = {}) {
this.astAnalyzer = options.astAnalyzer ?? new AstAnalyser();
const rawAllowedExtensions = options.loadExtensions
? options.loadExtensions(kDefaultExtensions)
const {
astAnalyzer = new AstAnalyser(),
loadExtensions,
rootPath = null
} = options;

this.astAnalyzer = astAnalyzer;
const rawAllowedExtensions = loadExtensions
? loadExtensions(kDefaultExtensions)
: kDefaultExtensions;

this.allowedExtensions = new Set(rawAllowedExtensions);
this.#rootPath = options.rootPath === null ?
null : fileURLToPathExtended(rootPath);
}

async* analyse(
entryFiles,
analyseFileOptions
options = {}
) {
this.analyzedDeps = new Set();
this.dependencies = new DiGraph();

for (const entryFile of new Set(entryFiles)) {
const normalizedEntryFile = path.normalize(
fileURLToPathExtended(entryFile)
);

for (const file of entryFiles) {
yield* this.#analyseFile(file, analyseFileOptions);
yield* this.#analyseFile(
normalizedEntryFile,
this.#getRelativeFilePath(normalizedEntryFile),
options
);
}
}

#getRelativeFilePath(file) {
return this.#rootPath ? path.relative(this.#rootPath, file) : file;
}

async* #analyseFile(
file,
relativeFile,
options
) {
const filePath = file instanceof URL ? fileURLToPath(file) : file;
const report = await this.astAnalyzer.analyseFile(file, options);

yield { url: filePath, ...report };
this.dependencies.addVertex({
id: relativeFile,
adjacentTo: [],
body: {}
});

const report = await this.astAnalyzer.analyseFile(
file,
options
);
yield { file: relativeFile, ...report };

if (!report.ok) {
return;
}

for (const [name] of report.dependencies) {
const depPath = await this.#getInternalDepPath(
name,
path.dirname(filePath)
const depFile = await this.#getInternalDepPath(
path.join(path.dirname(file), name)
);
if (depFile === null) {
continue;
}

if (depPath && !this.analyzedDeps.has(depPath)) {
this.analyzedDeps.add(depPath);

yield* this.#analyseFile(depPath, options);
const depRelativeFile = this.#getRelativeFilePath(depFile);
if (!this.dependencies.hasVertex(depRelativeFile)) {
this.dependencies.addVertex({
id: depRelativeFile,
adjacentTo: [],
body: {}
});

yield* this.#analyseFile(
depFile,
depRelativeFile,
options
);
}

this.dependencies.addEdge({
from: relativeFile, to: depRelativeFile
});
}
}

async #getInternalDepPath(name, basePath) {
const depPath = path.join(basePath, name);
const existingExt = path.extname(name);
async #getInternalDepPath(
filePath
) {
const fileExtension = path.extname(filePath);

if (existingExt === "") {
if (fileExtension === "") {
for (const ext of this.allowedExtensions) {
const depPathWithExt = `${depPath}.${ext}`;
const depPathWithExt = `${filePath}.${ext}`;

const fileExist = await this.#fileExists(depPathWithExt);
if (fileExist) {
Expand All @@ -72,22 +121,24 @@ export class EntryFilesAnalyser {
}
}
else {
if (!this.allowedExtensions.has(existingExt.slice(1))) {
if (!this.allowedExtensions.has(fileExtension.slice(1))) {
return null;
}

const fileExist = await this.#fileExists(depPath);
const fileExist = await this.#fileExists(filePath);
if (fileExist) {
return depPath;
return filePath;
}
}

return null;
}

async #fileExists(path) {
async #fileExists(
filePath
) {
try {
await fs.access(path, fs.constants.R_OK);
await fs.access(filePath, fs.constants.R_OK);

return true;
}
Expand All @@ -100,3 +151,11 @@ export class EntryFilesAnalyser {
}
}
}

function fileURLToPathExtended(
file
) {
return file instanceof URL ?
fileURLToPath(file) :
file;
}
57 changes: 53 additions & 4 deletions test/EntryFilesAnalyser.spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Import Node.js Dependencies
import { describe, it } from "node:test";
import assert from "node:assert";
import path from "node:path";
import { fileURLToPath } from "node:url";

// Import Internal Dependencies
import { EntryFilesAnalyser, AstAnalyser } from "../index.js";

const FIXTURE_URL = new URL("fixtures/entryFiles/", import.meta.url);
const FIXTURE_URL_PATH = fileURLToPath(FIXTURE_URL);

describe("EntryFilesAnalyser", () => {
it("should analyze internal dependencies recursively", async(t) => {
Expand All @@ -23,7 +25,7 @@ describe("EntryFilesAnalyser", () => {
const reports = await fromAsync(generator);

assert.deepEqual(
reports.map((report) => report.url),
reports.map((report) => report.file),
[
entryUrl,
new URL("deps/dep1.js", FIXTURE_URL),
Expand All @@ -47,7 +49,7 @@ describe("EntryFilesAnalyser", () => {
const reports = await fromAsync(generator);

assert.deepEqual(
reports.map((report) => report.url),
reports.map((report) => report.file),
[
entryUrl,
new URL("deps/invalidDep.js", FIXTURE_URL),
Expand All @@ -74,7 +76,7 @@ describe("EntryFilesAnalyser", () => {
const reports = await fromAsync(generator);

assert.deepEqual(
reports.map((report) => report.url),
reports.map((report) => report.file),
[
entryUrl,
new URL("deps/default.js", FIXTURE_URL),
Expand All @@ -100,14 +102,61 @@ describe("EntryFilesAnalyser", () => {
const reports = await fromAsync(generator);

assert.deepEqual(
reports.map((report) => report.url),
reports.map((report) => report.file),
[
entryUrl,
new URL("deps/default.jsx", FIXTURE_URL),
new URL("deps/dep.jsx", FIXTURE_URL)
].map((url) => fileURLToPath(url))
);
});

it("should detect recursive dependencies using DiGraph (with rootPath)", async() => {
const entryFilesAnalyser = new EntryFilesAnalyser({
rootPath: FIXTURE_URL
});
const entryUrl = new URL("recursive/A.js", FIXTURE_URL);

const generator = entryFilesAnalyser.analyse(
[entryUrl]
);
await fromAsync(generator);

assert.deepEqual(
[...entryFilesAnalyser.dependencies.findCycles()],
[
["recursive/A.js", "recursive/B.js"].map((str) => path.normalize(str))
]
);

assert.deepEqual(
[
...entryFilesAnalyser.dependencies.getDeepChildren(
path.normalize("recursive/A.js"), 1
)
],
[
path.normalize("recursive/B.js")
]
);
});

it("should detect recursive dependencies using DiGraph but without rootPath everything is absolute", async() => {
const entryFilesAnalyser = new EntryFilesAnalyser();
const entryUrl = new URL("recursive/A.js", FIXTURE_URL);

const generator = entryFilesAnalyser.analyse(
[entryUrl]
);
await fromAsync(generator);

for (const [from, to] of [...entryFilesAnalyser.dependencies.findCycles()]) {
assert.ok(path.isAbsolute(from));
assert.ok(path.isAbsolute(to));
assert.ok(from.startsWith(FIXTURE_URL_PATH));
assert.ok(to.startsWith(FIXTURE_URL_PATH));
}
});
});

// TODO: replace with Array.fromAsync when droping Node.js 20
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/entryFiles/recursive/A.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { bar } from "./B.js";

export const foo = "bar";
console.log(bar);
4 changes: 4 additions & 0 deletions test/fixtures/entryFiles/recursive/B.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { foo } from "./A.js";

export const bar = "foo";
console.log(foo);
Loading

0 comments on commit 6c6279c

Please sign in to comment.