Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(EntryFilesAnalyser): implement digraph-js #293

Merged
merged 1 commit into from
Aug 16, 2024
Merged
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
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
Loading