UNPKG

@nodesecure/js-x-ray

Version:
199 lines 7.49 kB
// 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