Skip to content

Commit

Permalink
feat(EntryFilesAnalyser): implement digraph-js
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxken committed Aug 16, 2024
1 parent bd410ea commit a35b1dd
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 44 deletions.
14 changes: 13 additions & 1 deletion docs/api/EntryFilesAnalyser.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,20 @@ Default files extensions are `.js`, `.cjs`, `.mjs` and `.node`

```ts
declare class EntryFilesAnalyser {
public dependencies: DiGraph;

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?: {
fileOptions?: RuntimeFileOptions;
rootPath?: string | URL;
}
): 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
103 changes: 78 additions & 25 deletions src/EntryFilesAnalyser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ 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
Expand All @@ -21,49 +26,87 @@ export class EntryFilesAnalyser {

async* analyse(
entryFiles,
analyseFileOptions
options = {}
) {
this.analyzedDeps = new Set();
const { fileOptions, rootPath = null } = options;
this.#rootPath = rootPath === null ? null : fileURLToPathExtended(rootPath);
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),
fileOptions
);
}
}

#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 +115,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 +145,11 @@ export class EntryFilesAnalyser {
}
}
}

function fileURLToPathExtended(
file
) {
return file instanceof URL ?
fileURLToPath(file) :
file;
}
40 changes: 36 additions & 4 deletions test/EntryFilesAnalyser.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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
Expand All @@ -23,7 +24,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 +48,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 +75,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 +101,45 @@ 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", async() => {
const entryFilesAnalyser = new EntryFilesAnalyser();
const entryUrl = new URL("recursive/A.js", FIXTURE_URL);

const generator = entryFilesAnalyser.analyse(
[entryUrl],
{
rootPath: FIXTURE_URL
}
);
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")
]
);
});
});

// 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);
25 changes: 17 additions & 8 deletions types/api.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Third-party
import { DiGraph } from "digraph-js";
import { Statement } from "meriyah";

// Internal
import {
Warning,
WarningName
} from "./warnings.js";
import { Statement } from "meriyah";

export {
AstAnalyser,
Expand Down Expand Up @@ -122,11 +126,6 @@ declare class AstAnalyser {
): ReportOnFile;
}

interface EntryFilesAnalyserOptions {
astAnalyzer?: AstAnalyser;
loadExtensions?: (defaults: string[]) => string[];
}

declare class SourceFile {
constructor(source: string, options: any);
addDependency(
Expand All @@ -144,16 +143,26 @@ declare class SourceFile {
walk(node: any): "skip" | null;
}

interface EntryFilesAnalyserOptions {
astAnalyzer?: AstAnalyser;
loadExtensions?: (defaults: string[]) => string[];
}

declare class EntryFilesAnalyser {
public dependencies: DiGraph;

constructor(options?: EntryFilesAnalyserOptions);

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

declare class JsSourceParser implements SourceParser {
Expand Down

0 comments on commit a35b1dd

Please sign in to comment.