UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

1,856 lines (1,789 loc) 170 kB
import { lstatSync, readdirSync, readFileSync } from "node:fs"; import { basename, isAbsolute, join, matchesGlob, relative, resolve, } from "node:path"; import process from "node:process"; import { URL } from "node:url"; import { parse } from "@babel/parser"; import traverse from "@babel/traverse"; import { getScopedStaticValueByName, getStaticObjectProperty, resolveStaticValue, } from "./analyzerScope.js"; import { classifyMcpReference } from "./mcp.js"; import { isLocalHost, sanitizeMcpRefToken } from "./mcpDiscovery.js"; import { sanitizeBomPropertyValue, sanitizeBomUrl, } from "./propertySanitizer.js"; const IGNORE_DIRS = process.env.ASTGEN_IGNORE_DIRS ? process.env.ASTGEN_IGNORE_DIRS.split(",") : [ "venv", "docs", "test", "tests", "e2e", "examples", "cypress", "site-packages", "typings", "api_docs", "dev_docs", "types", "mock", "mocks", "jest-cache", "eslint-rules", "codemods", "flow-typed", "i18n", "coverage", ]; const IGNORE_FILE_PATTERN = new RegExp( process.env.ASTGEN_IGNORE_FILE_PATTERN || "(test|spec|mock|setup-jest|\\.d)\\.(js|ts|tsx)$|(?<!vite\\.|vue\\.)(conf|config)\\.(js|ts|tsx)$", "i", ); const normalizeAnalyzerPathForGlob = (filePath) => String(filePath || "").replaceAll("\\", "/"); const normalizeAnalyzerSearchOptions = (deepOrOptions = false) => { if (deepOrOptions && typeof deepOrOptions === "object") { return { deep: Boolean(deepOrOptions.deep), noIgnore: Boolean(deepOrOptions.noIgnore), exclude: Array.isArray(deepOrOptions.exclude) ? deepOrOptions.exclude : [], }; } return { deep: Boolean(deepOrOptions), noIgnore: false, exclude: [], }; }; const shouldExcludeAnalyzerPath = ( rootDir, filePath, excludePatterns, isDirectory = false, ) => { if (!excludePatterns?.length) { return false; } const normalizedAbsolutePath = normalizeAnalyzerPathForGlob( resolve(filePath), ); const normalizedRelativePath = normalizeAnalyzerPathForGlob( relative(rootDir, filePath), ); const candidatePaths = [ normalizedAbsolutePath, normalizedRelativePath, normalizedRelativePath ? `./${normalizedRelativePath}` : "", ].filter(Boolean); if (isDirectory) { candidatePaths.push( `${normalizedAbsolutePath}/`, normalizedRelativePath ? `${normalizedRelativePath}/` : "", normalizedRelativePath ? `./${normalizedRelativePath}/` : "", ); } return excludePatterns.some((pattern) => { const normalizedPattern = normalizeAnalyzerPathForGlob(pattern); return candidatePaths.some((candidatePath) => matchesGlob(candidatePath, normalizedPattern), ); }); }; const getAllFiles = ( deep, dir, extn, files, result, regex, rootDir, excludePatterns, noIgnore = false, ) => { files = files || readdirSync(dir); result = result || []; regex = regex || new RegExp(`\\${extn}$`); rootDir = rootDir || dir; excludePatterns = excludePatterns || []; for (let i = 0; i < files.length; i++) { if ( !noIgnore && (IGNORE_FILE_PATTERN.test(files[i]) || files[i].startsWith(".")) ) { continue; } const file = join(dir, files[i]); const fileStat = lstatSync(file); if (fileStat.isSymbolicLink()) { continue; } if ( shouldExcludeAnalyzerPath( rootDir, file, excludePatterns, fileStat.isDirectory(), ) ) { continue; } if (fileStat.isDirectory()) { // Ignore directories const dirName = basename(file); if ( !noIgnore && (dirName.startsWith(".") || dirName.startsWith("__") || IGNORE_DIRS.includes(dirName.toLowerCase())) ) { continue; } // We need to include node_modules in deep mode to track exports // Ignore only for non-deep analysis if (!noIgnore && !deep && dirName === "node_modules") { continue; } try { result = getAllFiles( deep, file, extn, readdirSync(file), result, regex, rootDir, excludePatterns, noIgnore, ); } catch (_error) { // ignore } } else { if (regex.test(file)) { result.push(file); } } } return result; }; const babelParserOptions = { sourceType: "unambiguous", allowImportExportEverywhere: true, allowAwaitOutsideFunction: true, allowNewTargetOutsideFunction: true, allowReturnOutsideFunction: true, allowSuperOutsideMethod: true, errorRecovery: true, allowUndeclaredExports: true, createImportExpressions: true, tokens: true, attachComment: false, plugins: [ "optionalChaining", "classProperties", "decorators-legacy", "exportDefaultFrom", "doExpressions", "numericSeparator", "dynamicImport", "jsx", "typescript", ], }; /** * Filter only references to (t|jsx?) or (less|scss) files for now. * Opt to use our relative paths. */ const setFileRef = ( allImports, allExports, src, file, pathnode, specifiers = [], isTypeOnly = false, ) => { const pathway = pathnode.value || pathnode.name; const sourceLoc = pathnode.loc?.start; if (!pathway) { return; } const fileRelativeLoc = relative(src, file); // remove unexpected extension imports if (/\.(svg|png|jpg|json|d\.ts)/.test(pathway)) { return; } const importedModules = specifiers .map((s) => s.imported?.name) .filter((v) => v !== undefined); const exportedModules = specifiers .map((s) => s.exported?.name) .filter((v) => v !== undefined); const occurrence = { importedAs: pathway, importedModules, exportedModules, isExternal: true, fileName: fileRelativeLoc, lineNumber: sourceLoc?.line ?? undefined, columnNumber: sourceLoc?.column ?? undefined, isTypeOnly, }; // replace relative imports with full path let moduleFullPath = pathway; let wasAbsolute = false; if (/\.\//g.test(pathway) || /\.\.\//g.test(pathway)) { moduleFullPath = resolve(file, "..", pathway); if (isAbsolute(moduleFullPath)) { moduleFullPath = relative(src, moduleFullPath); wasAbsolute = true; } if (!moduleFullPath.startsWith("node_modules/")) { occurrence.isExternal = false; } } allImports[moduleFullPath] = allImports[moduleFullPath] || new Set(); allImports[moduleFullPath].add(occurrence); // Handle module package name // Eg: zone.js/dist/zone will be referred to as zone.js in package.json if (!wasAbsolute && moduleFullPath.includes("/")) { const modPkg = moduleFullPath.split("/")[0]; allImports[modPkg] = allImports[modPkg] || new Set(); allImports[modPkg].add(occurrence); } if (exportedModules?.length) { moduleFullPath = moduleFullPath .replace("node_modules/", "") .replace("dist/", "") .replace(/\.(js|ts|cjs|mjs)$/g, "") .replace("src/", ""); allExports[moduleFullPath] = allExports[moduleFullPath] || new Set(); occurrence.exportedModules = exportedModules; allExports[moduleFullPath].add(occurrence); } }; const vueCleaningRegex = /<\/*script.*>|<style[\s\S]*style>|<\/*br>/gi; const vueTemplateRegex = /(<template[^>]*>)([\s\S]*)(<\/template>)/gi; const vueCommentRegex = /<!--[\s\S]*?-->/gi; const vueBindRegex = /(:\[)([\s\S]*?)(])/gi; const fileToParseableCode = (file) => { let code = readFileSync(file, "utf-8"); if (file.endsWith(".vue") || file.endsWith(".svelte")) { code = code .replace(vueCommentRegex, (match) => match.replaceAll(/\S/g, " ")) .replace( vueCleaningRegex, (match) => `${match.replaceAll(/\S/g, " ").substring(1)};`, ) .replace( vueBindRegex, (_match, grA, grB, grC) => grA.replaceAll(/\S/g, " ") + grB + grC.replaceAll(/\S/g, " "), ) // Strip the entire <template> block content to spaces (preserving newlines // for accurate line numbers). Vue-specific binding attributes such as // `:prop-name`, `@update:model-value` and `v-*` directives are not valid // JSX and cause Babel to throw even when errorRecovery is enabled, so the // template HTML must be blanked out before AST parsing. .replace( vueTemplateRegex, (_match, grA, grB, grC) => grA.replaceAll(/\S/g, " ") + grB.replaceAll(/[^\n]/g, " ") + grC.replaceAll(/\S/g, " "), ); } return code; }; const isWasmPath = (modulePath) => typeof modulePath === "string" && /\.wasm([?#].*)?$/i.test(modulePath); const getStringValue = (astNode) => { if (!astNode) { return undefined; } if (astNode.type === "StringLiteral") { return astNode.value; } if ( astNode.type === "TemplateLiteral" && astNode.expressions.length === 0 && astNode.quasis.length === 1 ) { return astNode.quasis[0].value.cooked; } return undefined; }; const ANGULAR_WORKSPACE_MANIFEST_NAMES = new Set([ "angular.json", "workspace.json", "project.json", ]); const ANGULAR_RESOURCE_METADATA_KEYS = new Set([ "styleUrl", "styleUrls", "templateUrl", ]); const ANGULAR_INLINE_TEMPLATE_METADATA_KEYS = new Set(["template"]); const ANGULAR_ICON_METADATA_KEYS = new Set([ "collapsedIcon", "expandedIcon", "icon", ]); const ANGULAR_PACKAGE_STRING_KEYS = new Set([ "builder", "executor", "loadChildren", ]); const ANGULAR_WORKSPACE_PACKAGE_STRING_KEYS = new Set([ "assets", "builder", "executor", "includePaths", "polyfills", "plugins", "scripts", "styles", ]); const ANGULAR_STYLE_FILE_EXTENSIONS = [".css", ".less", ".sass", ".scss"]; const ANGULAR_STYLE_REFERENCE_PATTERNS = [ /@(?:import|use|forward)\s+(?:url\(\s*)?['"]([^'"\n)]+)['"]/g, /url\(\s*['"](~?[^'"\n)]+)['"]\s*\)/g, ]; const ANGULAR_CONFIG_EVIDENCE_FILE_NAMES = new Set([ "karma.conf.js", "karma.conf.cjs", "karma.conf.mjs", "protractor.conf.js", "protractor.conf.cjs", "protractor.conf.mjs", ]); const VUE_CONFIG_FILE_NAMES = new Set([ "vite.config.cjs", "vite.config.js", "vite.config.mjs", "vite.config.mts", "vite.config.ts", "vue.config.cjs", "vue.config.js", "vue.config.mjs", "vue.config.mts", "vue.config.ts", ]); const ANGULAR_SCRIPT_COMMAND_PACKAGES = [ { pattern: /(?:^|[\s;&|()])ng\s+test(?:\s|$)/, packages: ["karma"] }, { pattern: /(?:^|[\s;&|()])ng\s+e2e(?:\s|$)/, packages: ["protractor"] }, { pattern: /(?:^|[\s;&|()])ng\s+lint(?:\s|$)/, packages: ["codelyzer", "tslint"], }, { pattern: /(?:^|[\s;&|()])ng\s+(?:build|serve|run|xi18n)(?:\s|$)/, packages: ["@angular/compiler-cli", "typescript"], }, { pattern: /(?:^|[\s;&|()])ng(?:\s|$)/, packages: ["@angular/cli"] }, { pattern: /(?:^|[\s;&|()])ngc(?:\s|$)/, packages: ["@angular/compiler-cli"], }, { pattern: /(?:^|[\s;&|()])tsc(?:\s|$)/, packages: ["typescript"] }, { pattern: /(?:^|[\s;&|()])ttsc(?:\s|$)/, packages: ["ttypescript"] }, { pattern: /(?:^|[\s;&|()])ts-node(?:\s|\/|$)/, packages: ["ts-node"] }, { pattern: /(?:^|[\s;&|()])tslint(?:\s|$)/, packages: ["tslint"] }, { pattern: /(?:^|[\s;&|()])karma(?:\s|$)/, packages: ["karma"] }, { pattern: /(?:^|[\s;&|()])protractor(?:\s|$)/, packages: ["protractor"] }, { pattern: /(?:^|[\s;&|()])jasmine(?:\s|$)/, packages: ["jasmine"] }, { pattern: /(?:^|[\s;&|()])webpack-bundle-analyzer(?:\s|$)/, packages: ["webpack-bundle-analyzer"], }, { pattern: /(?:^|[\s;&|()])ng-packagr(?:\s|$)/, packages: ["ng-packagr"] }, { pattern: /(?:^|[\s;&|()])firebase-tools(?:\s|$)/, packages: ["firebase-tools"], }, { pattern: /(?:^|[\s;&|()])typedoc(?:\s|$)/, packages: ["typedoc"] }, { pattern: /(?:^|[\s;&|()])rimraf(?:\s|$)/, packages: ["rimraf"] }, { pattern: /(?:^|[\s;&|()])rollup(?:\s|$)/, packages: ["rollup"] }, ]; const ANGULAR_SCRIPT_EXECUTABLE_PACKAGE_MAP = { "@angular/cli": "@angular/cli", esbuild: "esbuild", "firebase-tools": "firebase-tools", jasmine: "jasmine", karma: "karma", lessc: "less", ng: "@angular/cli", "ng-packagr": "ng-packagr", ngc: "@angular/compiler-cli", postcss: "postcss-cli", protractor: "protractor", rimraf: "rimraf", rollup: "rollup", sass: "sass", tailwindcss: "tailwindcss", "ts-node": "ts-node", tsc: "typescript", tslint: "tslint", ttsc: "ttypescript", typedoc: "typedoc", vite: "vite", webpack: "webpack", "webpack-bundle-analyzer": "webpack-bundle-analyzer", }; const ANGULAR_PACKAGE_MANAGER_WRAPPER_COMMANDS = new Set([ "npx", "pnpm", "yarn", ]); const ANGULAR_EXECUTION_WRAPPER_COMMANDS = new Set(["node"]); const angularScriptSegmentSplitRegex = /&&|\|\||;|\|/; const angularShellTokenRegex = /"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'|\S+/g; const angularShellEnvAssignRegex = /^[A-Za-z_][A-Za-z0-9_]*=/; const ANGULAR_KARMA_CONFIG_PATTERNS = [ { pattern: /\bframeworks\s*:\s*\[[^\]]*['"]jasmine['"]/s, packages: ["karma-jasmine", "jasmine-core"], }, { pattern: /\bbrowsers\s*:\s*\[[^\]]*['"]Chrome/s, packages: ["karma-chrome-launcher"], }, { pattern: /\bbrowsers\s*:\s*\[[^\]]*['"]Firefox/s, packages: ["karma-firefox-launcher"], }, { pattern: /\bbrowsers\s*:\s*\[[^\]]*['"]Safari/s, packages: ["karma-safarinative-launcher"], }, { pattern: /\breporters\s*:\s*\[[^\]]*['"]coverage-istanbul['"]/s, packages: ["karma-coverage-istanbul-reporter"], }, { pattern: /\breporters\s*:\s*\[[^\]]*['"]html['"]/s, packages: ["karma-jasmine-html-reporter"], }, ]; const ANGULAR_PROTRACTOR_CONFIG_PATTERNS = [ { pattern: /\bframework\s*:\s*['"]jasmine['"]/, packages: ["jasmine-core", "jasminewd2"], }, { pattern: /\bts-node\/register\b/, packages: ["ts-node"] }, ]; const isAngularTsconfigFileName = (fileName) => /^tsconfig(?:[.-].*)?\.json$/i.test(fileName); const ANGULAR_TEMPLATE_PACKAGE_PATTERNS = [ { packageName: "@angular/router", pattern: /<\s*router-outlet\b|\brouterLink(?:Active)?\b/, }, { packageName: "@angular/common", pattern: /\*(ngIf|ngFor|ngSwitch)|\b(ngClass|ngStyle|ngTemplateOutlet)\b|\|\s*(async|date|currency|decimal|json|keyvalue|lowercase|number|percent|slice|titlecase|uppercase)\b/, }, { packageName: "@angular/forms", pattern: /\b(formControl|formControlName|formGroup|formArrayName|ngModel)\b/, }, { packageName: "@angular/material", pattern: /<\s*mat-[\w-]+\b|\bmat[A-Z][\w-]*\b|\bmat-[\w-]+\b/, }, { packageName: "@angular/cdk", pattern: /<\s*cdk-[\w-]+\b|\bcdk[A-Z][\w-]*\b|\bcdk-[\w-]+\b/, }, { packageName: "@ionic/angular", pattern: /<\s*ion-[\w-]+\b/, }, { packageName: "@fortawesome/angular-fontawesome", pattern: /<\s*fa-icon\b/, }, { packageName: "@fortawesome/fontawesome-free", pattern: /\bclass\s*=\s*["'][^"']*\b(?:fa|fas|far|fab|fa-solid|fa-regular|fa-brands)\b[^"']*\bfa-[\w-]+\b|\b(?:fa|fas|far|fab|fa-solid|fa-regular|fa-brands)\s+fa-[\w-]+\b/, }, { packageName: "ag-grid-angular", pattern: /<\s*ag-grid-angular\b/, }, { packageName: "bootstrap-icons", pattern: /\bclass\s*=\s*["'][^"']*\bbi\b[^"']*\bbi-[\w-]+\b|\bbi\s+bi-[\w-]+\b/, }, { packageName: "material-symbols", pattern: /\bfontSet\s*=\s*["'][^"']*material-symbols[^"']*["']|\bclass\s*=\s*["'][^"']*material-symbols[^"']*["']/, }, { packageName: "primeicons", pattern: /\bclass\s*=\s*["'][^"']*\bpi\b[^"']*\bpi-[\w-]+\b|\bpi\s+(?:pi-fw\s+)?pi-[\w-]+\b/, }, { packageName: "@ng-select/ng-select", pattern: /<\s*ng-select\b/, }, { packageName: "@swimlane/ngx-charts", pattern: /<\s*ngx-charts-[\w-]+\b/, }, ]; const angularLocalPathPrefixRegex = /^(\.|\/|\\|src\/|app\/|assets\/|styles\/|environments\/)/i; const angularUrlRegex = /^[a-z][a-z0-9+.-]*:/i; const angularBareFileRegex = /^[\w.-]+\.(css|eot|gif|html|ico|jpeg|jpg|js|json|less|mjs|png|sass|scss|svg|ts|ttf|webp|woff|woff2)$/i; const isAngularStyleUrlPackageReference = (reference) => { const normalizedReference = String(reference || "") .trim() .replaceAll("\\", "/"); return ( normalizedReference.startsWith("~") || normalizedReference.startsWith("node_modules/") ); }; const stripAngularRouteFragment = (reference) => String(reference || "") .split("#")[0] .split("?")[0] .trim(); const extractAngularPackageName = (reference, allowBare = true) => { let candidate = stripAngularRouteFragment(reference) .replace(/^~+/, "") .replace(/^\.\//, ""); if (!candidate || angularUrlRegex.test(candidate)) { return undefined; } candidate = candidate.replaceAll("\\", "/"); if (candidate.startsWith("node_modules/")) { candidate = candidate.slice("node_modules/".length); } else if (angularLocalPathPrefixRegex.test(candidate)) { return undefined; } if (!allowBare && !candidate.includes("/")) { return undefined; } if (candidate === "zone.js") { return candidate; } if (angularBareFileRegex.test(candidate)) { return undefined; } if (candidate.startsWith("@")) { const scopedMatch = candidate.match(/^(@[^/\s:]+\/[^/\s:]+)/); return scopedMatch?.[1]; } const unscopedMatch = candidate.match( /^([a-zA-Z0-9][a-zA-Z0-9._-]*)(?=\/|:|$)/, ); return unscopedMatch?.[1]; }; const stripQuotedShellToken = (token) => String(token || "") .trim() .replace(/^['"]|['"]$/g, ""); const getExecutableNameFromPathLikeToken = (token) => { let normalizedToken = stripQuotedShellToken(token) .replaceAll("\\", "/") .split(/[?#]/)[0] .trim(); if (!normalizedToken) { return undefined; } if (normalizedToken.startsWith("@")) { const scopedPackageMatch = normalizedToken.match(/^(@[^/\s]+\/[^/\s]+)/); return scopedPackageMatch?.[1]; } if (normalizedToken.startsWith("node_modules/.bin/")) { normalizedToken = normalizedToken.slice("node_modules/.bin/".length); } if (normalizedToken.includes("/")) { normalizedToken = basename(normalizedToken); } return normalizedToken.replace(/\.(cjs|cts|js|jsx|mjs|mts|ts|tsx)$/i, ""); }; const extractScriptSegmentExecutableName = (scriptSegment) => { const tokens = ( String(scriptSegment || "").match(angularShellTokenRegex) || [] ) .map((token) => stripQuotedShellToken(token)) .filter(Boolean); if (!tokens.length) { return undefined; } let index = 0; while ( index < tokens.length && angularShellEnvAssignRegex.test(tokens[index]) ) { index += 1; } if (index >= tokens.length) { return undefined; } const commandToken = tokens[index]; if (commandToken === "npm") { if (["exec", "x"].includes(tokens[index + 1])) { return { executableName: getExecutableNameFromPathLikeToken(tokens[index + 2]), packageHintOnly: true, }; } return undefined; } if (ANGULAR_PACKAGE_MANAGER_WRAPPER_COMMANDS.has(commandToken)) { if (["exec", "dlx"].includes(tokens[index + 1])) { return { executableName: getExecutableNameFromPathLikeToken(tokens[index + 2]), packageHintOnly: true, }; } if (commandToken === "npx") { return { executableName: getExecutableNameFromPathLikeToken(tokens[index + 1]), packageHintOnly: true, }; } return undefined; } if (ANGULAR_EXECUTION_WRAPPER_COMMANDS.has(commandToken)) { for ( let tokenIndex = index + 1; tokenIndex < tokens.length; tokenIndex += 1 ) { const value = tokens[tokenIndex]; if (!value || value.startsWith("-")) { continue; } if (value.startsWith("node_modules/.bin/") || value.includes("/.bin/")) { return { executableName: getExecutableNameFromPathLikeToken(value), packageHintOnly: false, }; } return undefined; } return undefined; } return { executableName: getExecutableNameFromPathLikeToken(commandToken), packageHintOnly: false, }; }; const addAngularExecutablePackageRef = ( allImports, allExports, src, file, executable, ) => { const executableName = executable?.executableName; if (!executableName) { return; } const packageName = ANGULAR_SCRIPT_EXECUTABLE_PACKAGE_MAP[executableName.toLowerCase()]; if (packageName) { setAngularPackageRef( allImports, allExports, src, file, packageName, undefined, ); return; } // For npx/npm exec/pnpm dlx/yarn dlx, executable token usually maps to a package. if (!executable.packageHintOnly) { return; } setSyntheticImportRef( allImports, allExports, src, file, `cdx:npm:bin/${executableName}`, [executableName], undefined, ); }; const setAngularPackageRef = ( allImports, allExports, src, file, reference, sourceLoc, allowBare = true, ) => { const packageName = extractAngularPackageName(reference, allowBare); if (!packageName) { return; } setSyntheticImportRef( allImports, allExports, src, file, packageName, [], sourceLoc, ); }; const addAngularTemplatePatternPackageRefs = ( allImports, allExports, src, file, content, baseSourceLoc, ) => { const lines = String(content || "").split(/\r?\n/); for (const [index, line] of lines.entries()) { for (const templatePattern of ANGULAR_TEMPLATE_PACKAGE_PATTERNS) { templatePattern.pattern.lastIndex = 0; if (templatePattern.pattern.test(line)) { setAngularPackageRef( allImports, allExports, src, file, templatePattern.packageName, baseSourceLoc ? { column: index === 0 ? baseSourceLoc.column : 0, line: baseSourceLoc.line + index, } : { column: 0, line: index + 1 }, ); } } } }; const getObjectPropertyKeyName = (propertyNode) => { if (!propertyNode) { return undefined; } return getMemberExpressionPropertyName(propertyNode); }; const getAngularStringNodes = (astNode) => { if (!astNode) { return []; } if (getStringValue(astNode)) { return [astNode]; } if (astNode.type === "ArrayExpression") { return (astNode.elements || []).filter((element) => getStringValue(element), ); } return []; }; const unwrapAwait = (astNode) => astNode?.type === "AwaitExpression" ? astNode.argument : astNode; const isImportMetaUrl = (astNode) => astNode?.type === "MemberExpression" && astNode.object?.type === "MetaProperty" && astNode.object.meta?.name === "import" && astNode.object.property?.name === "meta" && astNode.property?.type === "Identifier" && astNode.property.name === "url"; const getMemberExpressionPropertyName = (propertyNode) => { if (!propertyNode) { return undefined; } if (propertyNode.type === "Identifier") { return propertyNode.name; } if (propertyNode.type === "StringLiteral") { return propertyNode.value; } return undefined; }; const resolveWasmLiteralFromNode = (astNode, wasmBufferByVarName) => { const normalizedNode = unwrapAwait(astNode); const directLiteral = getStringValue(normalizedNode); if (isWasmPath(directLiteral)) { return directLiteral; } if (normalizedNode?.type === "Identifier") { return wasmBufferByVarName.get(normalizedNode.name); } if (normalizedNode?.type === "CallExpression") { if ( normalizedNode.callee?.type === "Identifier" && normalizedNode.callee.name === "fetch" && normalizedNode.arguments?.length ) { return resolveWasmLiteralFromNode( normalizedNode.arguments[0], wasmBufferByVarName, ); } } if (normalizedNode?.type === "NewExpression") { if ( normalizedNode.callee?.type === "Identifier" && normalizedNode.callee.name === "URL" && normalizedNode.arguments?.length ) { const urlLiteral = getStringValue(normalizedNode.arguments[0]); const baseArg = normalizedNode.arguments[1]; if (isWasmPath(urlLiteral) && (!baseArg || isImportMetaUrl(baseArg))) { return urlLiteral; } } } return undefined; }; const getWasmSourceFromInstantiateCall = (callNode, wasmBufferByVarName) => { if (callNode.callee?.type !== "MemberExpression") { return undefined; } const objectNode = callNode.callee.object; const propertyNode = callNode.callee.property; const calleeObjectName = getMemberExpressionPropertyName(objectNode); const calleePropertyName = getMemberExpressionPropertyName(propertyNode); if (calleeObjectName !== "WebAssembly") { return undefined; } if ( calleePropertyName !== "instantiate" && calleePropertyName !== "instantiateStreaming" && calleePropertyName !== "compile" && calleePropertyName !== "compileStreaming" ) { return undefined; } if (!callNode.arguments?.length) { return undefined; } return resolveWasmLiteralFromNode(callNode.arguments[0], wasmBufferByVarName); }; const getWasmSourceFromCallExpression = (callNode, wasmBufferByVarName) => { const wasmSourceFromInstantiate = getWasmSourceFromInstantiateCall( callNode, wasmBufferByVarName, ); if (wasmSourceFromInstantiate) { return wasmSourceFromInstantiate; } if ( callNode?.callee?.type === "Identifier" && ["fetch", "locateFile"].includes(callNode.callee.name) && callNode.arguments?.length ) { return resolveWasmLiteralFromNode( callNode.arguments[0], wasmBufferByVarName, ); } return undefined; }; const getNamedImportsFromObjectPattern = (idNode) => { const namedImports = []; if (idNode?.type !== "ObjectPattern") { return namedImports; } for (const prop of idNode.properties || []) { if (prop.type !== "ObjectProperty") { continue; } const keyName = getMemberExpressionPropertyName(prop.key); if (keyName) { namedImports.push(keyName); } } return namedImports; }; const setSyntheticImportRef = ( allImports, allExports, src, file, importPath, modules, sourceLoc, ) => { if (!importPath) { return; } const safeModules = modules || []; const syntheticSpecifiers = safeModules.map((moduleName) => ({ imported: { name: moduleName }, })); setFileRef( allImports, allExports, src, file, { value: importPath, loc: sourceLoc ? { start: sourceLoc } : undefined }, syntheticSpecifiers, ); }; const setSyntheticExportRef = ( allImports, allExports, src, file, importPath, modules, sourceLoc, ) => { if (!importPath) { return; } const safeModules = modules || []; const syntheticSpecifiers = safeModules.map((moduleName) => ({ exported: { name: moduleName }, })); setFileRef( allImports, allExports, src, file, { value: importPath, loc: sourceLoc ? { start: sourceLoc } : undefined }, syntheticSpecifiers, ); }; const getWasmExportMemberInfo = (astNode) => { if (!astNode) { return undefined; } if (astNode.type === "AssignmentExpression") { return getWasmExportMemberInfo(astNode.right); } if ( astNode.type !== "MemberExpression" || astNode.object?.type !== "Identifier" ) { return undefined; } return { aliasName: astNode.object.name, exportName: getMemberExpressionPropertyName(astNode.property), }; }; const getAssignmentTargetName = (astNode) => { if (!astNode) { return undefined; } if (astNode.type === "Identifier") { return astNode.name; } if ( astNode.type === "MemberExpression" && astNode.object?.type === "Identifier" && astNode.object.name === "Module" ) { return getMemberExpressionPropertyName(astNode.property); } return undefined; }; /** * Check AST tree for any (j|tsx?) files and set a file * references for any import, require or dynamic import files. */ const parseFileASTTree = (src, file, allImports, allExports) => { const ast = parse(fileToParseableCode(file), babelParserOptions); const wasmBufferByVarName = new Map(); const wasmResultByVarName = new Map(); const wasmInstanceByVarName = new Map(); const wasiConstructorAliases = new Set(["WASI"]); const wasiNamespaceAliases = new Set(); const wasiInstanceAliases = new Set(); const wasmPathLiterals = new Set(); const wasmExportAliases = new Set(["wasmExports"]); traverse.default(ast, { ImportDeclaration: (path) => { if (path?.node) { const isTypeOnly = path.node.importKind === "type" || path.node.importKind === "typeof" || (path.node.specifiers && path.node.specifiers.length > 0 && path.node.specifiers.every( (specifier) => specifier.importKind === "type" || specifier.importKind === "typeof", )); setFileRef( allImports, allExports, src, file, path.node.source, path.node.specifiers, isTypeOnly, ); const sourceValue = path.node.source?.value; if (sourceValue === "@angular/platform-browser/animations") { setSyntheticImportRef( allImports, allExports, src, file, "@angular/animations", [], path.node.loc?.start, ); } if (sourceValue === "@angular/platform-browser-dynamic") { setSyntheticImportRef( allImports, allExports, src, file, "@angular/compiler", [], path.node.loc?.start, ); } if ( sourceValue === "@angular/fire" || sourceValue?.startsWith("@angular/fire/") ) { setSyntheticImportRef( allImports, allExports, src, file, "firebase", [], path.node.loc?.start, ); } if (sourceValue === "node:wasi" || sourceValue === "wasi") { for (const specifier of path.node.specifiers || []) { if ( specifier.type === "ImportSpecifier" && specifier.imported?.name === "WASI" ) { wasiConstructorAliases.add(specifier.local?.name || "WASI"); } if (specifier.type === "ImportNamespaceSpecifier") { wasiNamespaceAliases.add(specifier.local?.name); } } } } }, // For require('') statements Identifier: (path) => { if ( path?.node && path.node.name === "require" && path.parent.type === "CallExpression" ) { setFileRef(allImports, allExports, src, file, path.parent.arguments[0]); } }, // Use for dynamic imports like routes.jsx CallExpression: (path) => { if (path?.node && path.node.callee.type === "Import") { setFileRef(allImports, allExports, src, file, path.node.arguments[0]); } const wasmSourceLiteral = getWasmSourceFromCallExpression( path?.node, wasmBufferByVarName, ); if (wasmSourceLiteral) { wasmPathLiterals.add(wasmSourceLiteral); setSyntheticImportRef( allImports, allExports, src, file, wasmSourceLiteral, [], path.node.loc?.start, ); } if ( path?.node?.callee?.type === "MemberExpression" && path.node.callee.object?.type === "Identifier" && wasiInstanceAliases.has(path.node.callee.object.name) ) { const methodName = getMemberExpressionPropertyName( path.node.callee.property, ); if (methodName === "start" || methodName === "initialize") { setSyntheticImportRef( allImports, allExports, src, file, "node:wasi", [methodName], path.node.loc?.start, ); } } }, ImportExpression: (path) => { if (path?.node?.source) { setFileRef(allImports, allExports, src, file, path.node.source); } }, VariableDeclarator: (path) => { const idNode = path?.node?.id; const initNode = unwrapAwait(path?.node?.init); if (!idNode || !initNode) { return; } if ( idNode.type === "Identifier" && initNode.type === "CallExpression" && initNode.callee?.type === "MemberExpression" ) { const calleePropertyName = getMemberExpressionPropertyName( initNode.callee.property, ); if ( calleePropertyName === "readFile" || calleePropertyName === "readFileSync" ) { const pathArg = initNode.arguments?.[0]; const wasmPath = getStringValue(pathArg); if (isWasmPath(wasmPath)) { wasmBufferByVarName.set(idNode.name, wasmPath); wasmPathLiterals.add(wasmPath); setSyntheticImportRef( allImports, allExports, src, file, wasmPath, [], path.node.loc?.start, ); } } const wasmSource = getWasmSourceFromInstantiateCall( initNode, wasmBufferByVarName, ); if (wasmSource) { wasmResultByVarName.set(idNode.name, wasmSource); wasmPathLiterals.add(wasmSource); setSyntheticImportRef( allImports, allExports, src, file, wasmSource, [], path.node.loc?.start, ); } if ( initNode.callee?.type === "MemberExpression" && initNode.callee.object?.type === "Identifier" && wasiNamespaceAliases.has(initNode.callee.object.name) && getMemberExpressionPropertyName(initNode.callee.property) === "WASI" ) { wasiInstanceAliases.add(idNode.name); setSyntheticImportRef( allImports, allExports, src, file, "node:wasi", ["WASI"], path.node.loc?.start, ); } } if ( idNode.type === "Identifier" && initNode.type === "CallExpression" && initNode.callee?.type === "Identifier" && wasiConstructorAliases.has(initNode.callee.name) ) { wasiInstanceAliases.add(idNode.name); setSyntheticImportRef( allImports, allExports, src, file, "node:wasi", ["WASI"], path.node.loc?.start, ); } if (idNode.type === "Identifier" && initNode.type === "NewExpression") { if ( initNode.callee?.type === "Identifier" && wasiConstructorAliases.has(initNode.callee.name) ) { wasiInstanceAliases.add(idNode.name); setSyntheticImportRef( allImports, allExports, src, file, "node:wasi", ["WASI"], path.node.loc?.start, ); } if ( initNode.callee?.type === "MemberExpression" && initNode.callee.object?.type === "Identifier" && wasiNamespaceAliases.has(initNode.callee.object.name) && getMemberExpressionPropertyName(initNode.callee.property) === "WASI" ) { wasiInstanceAliases.add(idNode.name); setSyntheticImportRef( allImports, allExports, src, file, "node:wasi", ["WASI"], path.node.loc?.start, ); } } if (idNode.type === "ObjectPattern") { if (initNode.type === "CallExpression") { const wasmSource = getWasmSourceFromInstantiateCall( initNode, wasmBufferByVarName, ); if (wasmSource) { wasmPathLiterals.add(wasmSource); for (const prop of idNode.properties || []) { if ( prop.type === "ObjectProperty" && getMemberExpressionPropertyName(prop.key) === "instance" && prop.value?.type === "Identifier" ) { wasmInstanceByVarName.set(prop.value.name, wasmSource); } } setSyntheticImportRef( allImports, allExports, src, file, wasmSource, [], path.node.loc?.start, ); } if ( initNode.callee?.type === "Identifier" && initNode.callee.name === "require" ) { const requiredModule = getStringValue(initNode.arguments?.[0]); if (requiredModule === "node:wasi" || requiredModule === "wasi") { for (const prop of idNode.properties || []) { if ( prop.type === "ObjectProperty" && getMemberExpressionPropertyName(prop.key) === "WASI" && prop.value?.type === "Identifier" ) { wasiConstructorAliases.add(prop.value.name); } } } } } if (initNode.type === "MemberExpression") { const exportNames = getNamedImportsFromObjectPattern(idNode); if (!exportNames.length) { return; } if ( initNode.object?.type === "MemberExpression" && initNode.object.object?.type === "Identifier" && getMemberExpressionPropertyName(initNode.object.property) === "instance" && getMemberExpressionPropertyName(initNode.property) === "exports" ) { const wasmSource = wasmResultByVarName.get( initNode.object.object.name, ); if (wasmSource) { setSyntheticImportRef( allImports, allExports, src, file, wasmSource, exportNames, path.node.loc?.start, ); } } if ( initNode.object?.type === "Identifier" && getMemberExpressionPropertyName(initNode.property) === "exports" ) { const wasmSource = wasmInstanceByVarName.get(initNode.object.name); if (wasmSource) { setSyntheticImportRef( allImports, allExports, src, file, wasmSource, exportNames, path.node.loc?.start, ); } } } } if ( idNode.type === "Identifier" && initNode.type === "MemberExpression" && initNode.object?.type === "Identifier" && getMemberExpressionPropertyName(initNode.property) === "instance" ) { const wasmSource = wasmResultByVarName.get(initNode.object.name); if (wasmSource) { wasmInstanceByVarName.set(idNode.name, wasmSource); } } if ( idNode.type === "Identifier" && initNode.type === "CallExpression" && initNode.callee?.type === "MemberExpression" && initNode.callee.object?.type === "Identifier" && initNode.callee.object.name === "WebAssembly" ) { const wasmSource = getWasmSourceFromInstantiateCall( initNode, wasmBufferByVarName, ); if (wasmSource) { wasmResultByVarName.set(idNode.name, wasmSource); wasmPathLiterals.add(wasmSource); } } }, AssignmentExpression: (path) => { const wasmExportMemberInfo = getWasmExportMemberInfo(path?.node?.right); if (!wasmExportMemberInfo?.exportName) { return; } if (!wasmExportAliases.has(wasmExportMemberInfo.aliasName)) { return; } if (!wasmPathLiterals.size) { return; } for (const wasmPath of wasmPathLiterals) { setSyntheticImportRef( allImports, allExports, src, file, wasmPath, [wasmExportMemberInfo.exportName], path.node.loc?.start, ); } const targetName = getAssignmentTargetName(path?.node?.left); if (!targetName) { return; } for (const wasmPath of wasmPathLiterals) { setSyntheticExportRef( allImports, allExports, src, file, wasmPath, [targetName], path.node.loc?.start, ); } }, NewExpression: (path) => { if (path?.node?.callee?.type === "Identifier") { if (wasiConstructorAliases.has(path.node.callee.name)) { setSyntheticImportRef( allImports, allExports, src, file, "node:wasi", ["WASI"], path.node.loc?.start, ); } } if ( path?.node?.callee?.type === "MemberExpression" && path.node.callee.object?.type === "Identifier" && wasiNamespaceAliases.has(path.node.callee.object.name) && getMemberExpressionPropertyName(path.node.callee.property) === "WASI" ) { setSyntheticImportRef( allImports, allExports, src, file, "node:wasi", ["WASI"], path.node.loc?.start, ); } }, ObjectProperty: (path) => { const keyName = getObjectPropertyKeyName(path?.node?.key); if (!keyName) { return; } if (ANGULAR_RESOURCE_METADATA_KEYS.has(keyName)) { for (const stringNode of getAngularStringNodes(path.node.value)) { setFileRef(allImports, allExports, src, file, stringNode); } } if (ANGULAR_PACKAGE_STRING_KEYS.has(keyName)) { for (const stringNode of getAngularStringNodes(path.node.value)) { const reference = getStringValue(stringNode); if (keyName === "loadChildren" && /^[./]/.test(reference || "")) { setFileRef(allImports, allExports, src, file, { value: stripAngularRouteFragment(reference), loc: stringNode.loc, }); continue; } setAngularPackageRef( allImports, allExports, src, file, reference, stringNode.loc?.start, ); } } if ( (ANGULAR_INLINE_TEMPLATE_METADATA_KEYS.has(keyName) || ANGULAR_ICON_METADATA_KEYS.has(keyName)) && Object.keys(allImports).some((importName) => isAngularPackageSignal(importName), ) ) { for (const stringNode of getAngularStringNodes(path.node.value)) { const content = getStringValue(stringNode); addAngularTemplatePatternPackageRefs( allImports, allExports, src, file, content, stringNode.loc?.start, ); } } }, // Use for export barrells ExportAllDeclaration: (path) => { const isTypeOnly = path.node.exportKind === "type" || path.node.exportKind === "typeof"; setFileRef( allImports, allExports, src, file, path.node.source, undefined, isTypeOnly, ); }, ExportNamedDeclaration: (path) => { // ensure there is a path export if (path?.node?.source) { const isTypeOnly = path.node.exportKind === "type" || path.node.exportKind === "typeof" || (path.node.specifiers && path.node.specifiers.length > 0 && path.node.specifiers.every( (specifier) => specifier.exportKind === "type" || specifier.exportKind === "typeof", )); setFileRef( allImports, allExports, src, file, path.node.source, path.node.specifiers, isTypeOnly, ); } }, }); }; /** * Return paths to all (j|tsx?) files. */ const getAllSrcJSAndTSFiles = (src, deep) => Promise.all( [ ".js", ".jsx", ".cjs", ".mjs", ".ts", ".tsx", ".mts", ".cts", ".vue", ".svelte", ].map((extension) => { const searchOptions = normalizeAnalyzerSearchOptions(deep); return getAllFiles( searchOptions.deep, src, extension, undefined, undefined, undefined, src, searchOptions.exclude, searchOptions.noIgnore, ); }), ); const getAngularWorkspaceManifestFiles = ( dir, deep, result = [], rootDir = dir, excludePatterns = [], noIgnore = false, ) => { let files; try { files = readdirSync(dir); } catch { return result; } for (const entry of files) { if (!noIgnore && entry.startsWith(".")) { continue; } const file = join(dir, entry); let fileStat; try { fileStat = lstatSync(file); } catch { continue; } if (fileStat.isSymbolicLink()) { continue; } if ( shouldExcludeAnalyzerPath( rootDir, file, excludePatterns, fileStat.isDirectory(), ) ) { continue; } if (fileStat.isDirectory()) { const dirName = basename(file); if ( !noIgnore && (dirName.startsWith("__") || IGNORE_DIRS.includes(dirName.toLowerCase()) || dirName === "node_modules") ) { continue; } getAngularWorkspaceManifestFiles( file, deep, result, rootDir, excludePatterns, noIgnore, ); continue; } if (ANGULAR_WORKSPACE_MANIFEST_NAMES.has(entry)) { result.push(file); } } return result; }; const getAngularEvidenceFiles = ( dir, deep, result = [], rootDir = dir, excludePatterns = [], noIgnore = false, ) => { let files; try { files = readdirSync(dir); } catch { return result; } for (const entry of files) { if (!noIgnore && entry.startsWith(".")) { continue; } const file = join(dir, entry); let fileStat; try { fileStat = lstatSync(file); } catch { continue; } if (fileStat.isSymbolicLink()) { continue; } if ( shouldExcludeAnalyzerPath( rootDir, file, excludePatterns, fileStat.isDirectory(), ) ) { continue; } if (fileStat.isDirectory()) { const dirName = basename(file); if ( !noIgnore && (dirName.startsWith("__") || IGNORE_DIRS.includes(dirName.toLowerCase()) || dirName === "node_modules" || (!deep && dirName === "dist")) ) { continue; } getAngularEvidenceFiles( file, deep, result, rootDir, excludePatterns, noIgnore, ); continue; } if ( entry === "package.json" || ANGULAR_CONFIG_EVIDENCE_FILE_NAMES.has(entry) || isAngularTsconfigFileName(entry) ) { result.push(file); } } return result; }; const parseAngularJsonFile = (file) => { try { return JSON.parse(readFileSync(file, "utf-8")); } catch { return undefined; } }; const parseAngularJsonLikeFile = (file) => { try { const content = readFileSync(file, "utf-8") .replace(/\/\*[\s\S]*?\*\//g, "") .replace(/^\s*\/\/.*$/gm, ""); return JSON.parse(content); } catch { return undefined; } }; const addAngularBuilderImpliedPackageRefs = (reference, refs) => { const normalizedReference = String(reference || "").trim(); if (!normalizedReference.includes(":")) { return; } const [builderPackage, builderTarget] = normalizedReference.split(":"); if ( builderPackage === "@angular-devkit/build-angular" || builderPackage === "@angular/build" ) { if ( [ "application", "browser", "browser-esbuild", "dev-server", "extract-i18n", "server", ].includes(builderTarget) ) { refs.add("@angular/compiler-cli"); refs.add("typescript"); } if (builderTarget === "karma") { refs.add("karma"); } if (builderTarget === "protractor") { refs.add("protractor"); } } if (builderPackage === "@angular-devkit/build-ng-packagr") { refs.add("ng-packagr"); } if (builderPackage === "@nguniversal/builders") { refs.add("@angular/compiler-cli"); refs.add("typescript"); } }; const isAngularPackageSignal = (packageName) => packageName === "@angular" || packageName?.startsWith("@angular/") || packageName?.startsWith("@angular-") || packageName === "@nx/angular" || packageName === "@schematics/angular"; const collectAngularWorkspacePackageRefs = (node, keyName, refs) => { if (typeof node === "string") { if (ANGULAR_WORKSPACE_PACKAGE_STRING_KEYS.has(keyName)) { const packageName = extractAngularPackageName(node, true); if (packageName) { refs.add(packageName); } if (keyName === "builder" || keyName === "executor") { addAngularBuilderImpliedPackageRefs(node, refs); } }