@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
199 lines • 7.49 kB
JavaScript
// Import Node.js Dependencies
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
// Import Third-party Dependencies
import combineAsyncIterators from "combine-async-iterators";
import { DiGraph } from "digraph-js";
// Import Internal Dependencies
import { AstAnalyser } from "./AstAnalyser.js";
import { TsSourceParser } from "./parsers/TsSourceParser.js";
import { JsSourceParser } from "./parsers/JsSourceParser.js";
import { DefaultCollectableSet } from "./CollectableSet.js";
// CONSTANTS
const kDefaultExtensions = [
...Array.from(JsSourceParser.FileExtensions).map((ext) => ext.slice(1)),
...Array.from(TsSourceParser.FileExtensions).map((ext) => ext.slice(1)),
"node"
];
export class EntryFilesAnalyser {
static Parsers = {
js: new JsSourceParser(),
ts: new TsSourceParser()
};
#rootPath = null;
#depPathCache = new Map();
#packageDependencies;
astAnalyzer;
allowedExtensions;
dependencies;
ignoreENOENT;
stats = {
numberOfImportsDetected: 0,
numberOfFilesProcessed: 0
};
uniqueImports = new Set();
constructor(options = {}) {
const { astAnalyzer = new AstAnalyser({
collectables: [
new DefaultCollectableSet("dependency")
]
}), loadExtensions, rootPath = null, ignoreENOENT = false, packageDependencies = [] } = options;
this.astAnalyzer = astAnalyzer;
if (this.astAnalyzer.getCollectableSet("dependency") === void 0) {
throw new Error("astAnalyzer instance must have a 'dependency' collectable");
}
const rawAllowedExtensions = loadExtensions
? loadExtensions(kDefaultExtensions)
: kDefaultExtensions;
this.allowedExtensions = new Set(rawAllowedExtensions);
this.#rootPath = rootPath === null ?
null : fileURLToPathExtended(rootPath);
this.ignoreENOENT = ignoreENOENT;
this.#packageDependencies = new Set(packageDependencies);
}
async *analyse(entryFiles, options = {}) {
this.dependencies = new DiGraph();
this.stats.numberOfImportsDetected = 0;
this.stats.numberOfFilesProcessed = 0;
this.uniqueImports.clear();
this.#depPathCache.clear();
const generators = [];
for (const entryFile of new Set(entryFiles)) {
const normalizedEntryFile = this.#normalizeAndCleanEntryFile(entryFile);
if (this.ignoreENOENT &&
!await this.#fileExists(normalizedEntryFile)) {
continue;
}
generators.push(this.#analyseFile(normalizedEntryFile, this.#getRelativeFilePath(normalizedEntryFile), options));
}
if (generators.length > 0) {
yield* combineAsyncIterators(...generators);
}
this.stats.numberOfImportsDetected = this.uniqueImports.size;
}
#normalizeAndCleanEntryFile(file) {
let normalizedEntryFile = path.normalize(fileURLToPathExtended(file));
if (this.#rootPath !== null && !path.isAbsolute(normalizedEntryFile)) {
normalizedEntryFile = path.join(this.#rootPath, normalizedEntryFile);
}
return normalizedEntryFile;
}
#getRelativeFilePath(file) {
return this.#rootPath ?
path.relative(this.#rootPath, file) :
file;
}
#getParserFromFileExtension(file) {
const fileExtension = path.extname(file);
if (JsSourceParser.FileExtensions.has(fileExtension)) {
return EntryFilesAnalyser.Parsers.js;
}
else if (TsSourceParser.FileExtensions.has(fileExtension)) {
return EntryFilesAnalyser.Parsers.ts;
}
return void 0;
}
async *#analyseFile(file, relativeFile, options) {
// Skip declaration files as they are not meant to be analysed
if (file.includes("d.ts")) {
return;
}
this.dependencies.addVertex({
id: relativeFile,
adjacentTo: [],
body: {}
});
const { metadata = {}, fileMetadata = () => {
return {};
}, finalize: userFinalize, ...runtimeOptions } = options;
const finalMetadata = Object.assign(structuredClone(metadata), fileMetadata(file));
let fileDependencies = new Set();
const report = await this.astAnalyzer.analyseFile(file, {
...runtimeOptions,
metadata: finalMetadata,
customParser: this.#getParserFromFileExtension(file),
finalize: (sourceFile) => {
fileDependencies = new Set(sourceFile.dependencies.keys());
userFinalize?.(sourceFile);
}
});
this.stats.numberOfFilesProcessed++;
yield { file: relativeFile, ...report };
if (!report.ok) {
return;
}
const depFiles = await Promise.all(Array.from(fileDependencies)
.filter((name) => !this.#packageDependencies.has(name))
.map((name) => this.#getInternalDepPath(path.join(path.dirname(file), name))));
const generators = [];
for (const depFile of depFiles) {
if (depFile === null) {
continue;
}
this.uniqueImports.add(depFile);
const depRelativeFile = this.#getRelativeFilePath(depFile);
if (!this.dependencies.hasVertex(depRelativeFile)) {
this.dependencies.addVertex({
id: depRelativeFile,
adjacentTo: [],
body: {}
});
generators.push(this.#analyseFile(depFile, depRelativeFile, options));
}
this.dependencies.addEdge({ from: relativeFile, to: depRelativeFile });
}
if (generators.length > 0) {
yield* combineAsyncIterators(...generators);
}
}
#getInternalDepPath(filePath) {
const cached = this.#depPathCache.get(filePath);
if (cached !== undefined) {
return cached;
}
const promise = this.#resolveInternalDepPath(filePath);
this.#depPathCache.set(filePath, promise);
return promise;
}
async #resolveInternalDepPath(filePath) {
const fileExtension = path.extname(filePath);
if (fileExtension === "") {
for (const ext of this.allowedExtensions) {
const depPathWithExt = `${filePath}.${ext}`;
const fileExist = await this.#fileExists(depPathWithExt);
if (fileExist) {
return depPathWithExt;
}
}
}
else {
if (!this.allowedExtensions.has(fileExtension.slice(1))) {
return null;
}
const fileExist = await this.#fileExists(filePath);
if (fileExist) {
return filePath;
}
}
return null;
}
async #fileExists(filePath) {
try {
await fs.access(filePath, fs.constants.R_OK);
return true;
}
catch (error) {
if (error.code !== "ENOENT") {
throw error;
}
return false;
}
}
}
function fileURLToPathExtended(file) {
return file instanceof URL ?
fileURLToPath(file) :
file;
}
//# sourceMappingURL=EntryFilesAnalyser.js.map