archunit
Version:
ArchUnit TypeScript is an architecture testing library, to specify and assert architecture rules in your TypeScript app
256 lines • 14.1 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractGraph = exports.clearGraphCache = exports.getProjectFiles = exports.guessLocationOfTsconfig = void 0;
const typescript_1 = __importDefault(require("typescript"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const errors_1 = require("../error/errors");
const path_utils_1 = require("../util/path-utils");
const ts_helpers_1 = require("@zerollup/ts-helpers");
const import_kinds_helper_1 = require("../util/import-kinds-helper");
const __1 = require("..");
// Constant to control whether node_modules files should be excluded from the graph
// TODO: introduce a .archignore file instead a la .gitignore
const EXCLUDE_NODE_MODULES = true;
const EXCLUDE_DIST = true;
// Logger instance for debugging graph extraction
const logger = __1.sharedLogger;
const guessLocationOfTsconfig = (options) => {
logger?.debug(options?.logging, 'Starting tsconfig.json discovery from current directory');
const result = guessLocationOfTsconfigRecursively('.');
if (result) {
logger?.debug(options?.logging, `Found tsconfig.json at: ${result}`);
}
else {
logger?.warn(options?.logging, 'No tsconfig.json found in current directory tree');
}
return result;
};
exports.guessLocationOfTsconfig = guessLocationOfTsconfig;
const guessLocationOfTsconfigRecursively = (pathName, options) => {
logger?.debug(options?.logging, `Searching for tsconfig.json in directory: ${path_1.default.resolve(pathName)}`);
const dir = fs_1.default.readdirSync(pathName);
logger?.debug(options?.logging, `Directory contents: ${dir.join(', ')}`);
// First check if tsconfig exists in the current directory
const tsconfigFileName = dir.find((fileName) => path_1.default.basename(fileName) === 'tsconfig.json');
if (tsconfigFileName) {
const resolvedPath = path_1.default.resolve(pathName, 'tsconfig.json');
logger?.debug(options?.logging, `Found tsconfig.json at: ${resolvedPath}`);
return resolvedPath;
}
// If not, go up one level
const levelUp = path_1.default.resolve(pathName, '..');
logger?.debug(options?.logging, `tsconfig.json not found, moving up to: ${levelUp}`);
// Stop if we've reached the filesystem root
if (path_1.default.relative(levelUp, pathName) === '') {
logger?.debug(options?.logging, 'Reached filesystem root, stopping search');
return undefined;
}
// Continue recursively
return guessLocationOfTsconfigRecursively(levelUp);
};
const getProjectFiles = (rootDir, compilerHost, config, options) => {
logger?.debug(options?.logging, `Getting project files from root directory: ${rootDir}`);
logger?.debug(options?.logging, `Include patterns: ${JSON.stringify(config.include)}`);
logger?.debug(options?.logging, `Exclude patterns: ${JSON.stringify(config.exclude)}`);
if (!compilerHost.readDirectory) {
const error = 'compiler host is missing readDirectory method';
logger?.error(options?.logging, error);
throw new errors_1.TechnicalError(error);
}
const files = compilerHost.readDirectory(rootDir, ['ts', 'tsx'], // TODO: include js
config.exclude ?? [], config.include ?? []);
if (!files) {
const error = 'compiler could not resolve project files';
logger?.error(options?.logging, error);
throw new errors_1.TechnicalError(error);
}
logger?.info(options?.logging, `Found ${files.length} TypeScript files in project`);
logger?.debug(options?.logging, `Project files: ${files.slice(0, 10).join(', ')}${files.length > 10 ? `... and ${files.length - 10} more` : ''}`);
return files;
};
exports.getProjectFiles = getProjectFiles;
const graphCache = new Map();
const clearGraphCache = (options) => {
const cacheSize = graphCache.size;
graphCache.clear();
logger?.debug(options?.logging, `Cleared graph cache (previously contained ${cacheSize} entries)`);
};
exports.clearGraphCache = clearGraphCache;
const extractGraph = async (configFileName, options) => {
logger?.debug(options?.logging, `Starting graph extraction with config: ${configFileName || 'auto-detected'}`);
if (options?.clearCache) {
logger?.debug(options?.logging, 'Clearing graph cache');
(0, exports.clearGraphCache)();
}
const cachedResult = graphCache.get(configFileName);
if (cachedResult) {
logger?.debug(options?.logging, 'Using cached graph extraction result');
return cachedResult;
}
logger?.debug(options?.logging, 'No cached result found, computing new graph');
const computedResult = extractGraphUncached(configFileName, options);
graphCache.set(configFileName, computedResult);
const result = await computedResult;
logger?.info(options?.logging, `Graph extraction completed with ${result.length} edges`);
return result;
};
exports.extractGraph = extractGraph;
const extractGraphUncached = async (configFileName, options) => {
logger?.debug(options?.logging, 'Starting uncached graph extraction');
const configFile = configFileName ?? (0, exports.guessLocationOfTsconfig)(options);
if (!configFile) {
const error = 'Could not find configuration path';
logger?.error(options?.logging, error);
throw new errors_1.TechnicalError(error);
}
logger?.info(options?.logging, `Using TypeScript config file: ${configFile}`);
const config = typescript_1.default.readConfigFile(configFile, (path) => {
logger?.debug(options?.logging, `Reading config file: ${path}`);
return fs_1.default.readFileSync(path).toString();
});
if (config.error) {
logger?.error(options?.logging, `Invalid config file: ${config.error.messageText}`);
throw new errors_1.TechnicalError('invalid config path');
}
logger?.debug(options?.logging, 'Successfully parsed TypeScript configuration');
const parsedConfig = config.config;
logger?.debug(options?.logging, `Compiler options: ${JSON.stringify(parsedConfig, null, 2).slice(0, 500)}...`);
const rootDir = path_1.default.dirname(path_1.default.resolve(configFile));
logger?.info(options?.logging, `Project root directory: ${rootDir}`);
const compilerHost = typescript_1.default.createCompilerHost(parsedConfig);
logger?.debug(options?.logging, 'Created TypeScript compiler host');
const files = (0, exports.getProjectFiles)(rootDir, compilerHost, config?.config);
logger?.debug(options?.logging, 'Creating TypeScript program');
const program = typescript_1.default.createProgram({
rootNames: files ?? [],
options: parsedConfig,
host: compilerHost,
});
const sourceFiles = program.getSourceFiles();
logger?.info(options?.logging, `TypeScript program created with ${sourceFiles.length} source files`);
// Filter out files from node_modules for logging purposes
const projectFiles = sourceFiles.filter((sf) => !sf.fileName.includes('node_modules'));
const nodeModulesFiles = sourceFiles.filter((sf) => sf.fileName.includes('node_modules'));
logger?.debug(options?.logging, `Project source files: ${projectFiles.length}`);
logger?.debug(options?.logging, `Node modules files: ${nodeModulesFiles.length}`);
if (projectFiles.length === 0) {
logger?.warn(options?.logging, 'No project source files found - this might indicate a configuration issue');
}
const imports = [];
let processedFiles = 0;
let skippedImports = 0;
let erroredImports = 0;
logger?.debug(options?.logging, 'Starting import analysis');
for (const sourceFile of program.getSourceFiles()) {
const isProjectFile = !sourceFile.fileName.includes('node_modules');
if (isProjectFile) {
processedFiles++;
logger?.debug(options?.logging, `Processing file ${processedFiles}/${projectFiles.length}: ${path_1.default.relative(rootDir, sourceFile.fileName)}`);
}
typescript_1.default.forEachChild(sourceFile, (x) => {
if (!typescript_1.default.isImportDeclaration(x)) {
return;
}
try {
const normalizedSourceFileName = path_1.default.relative(rootDir, sourceFile.fileName);
const specifier = x.moduleSpecifier;
const module = specifier['text'];
if (module === undefined) {
logger?.debug(options?.logging, `Skipping import with undefined module specifier in ${normalizedSourceFileName}`);
skippedImports++;
return;
}
logger?.debug(options?.logging, `Processing import: "${module}" in ${normalizedSourceFileName}`);
const resolver = new ts_helpers_1.ImportPathsResolver(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parsedConfig.compilerOptions);
const suggestion = resolver.getImportSuggestions(module, path_1.default.dirname(normalizedSourceFileName));
const bestGuess = suggestion !== undefined ? suggestion[0] : undefined;
if (bestGuess && bestGuess !== module) {
logger?.debug(options?.logging, `Import path resolved from "${module}" to "${bestGuess}"`);
}
// TODO Might use some module resolution cache in future
const resolvedModule = typescript_1.default.resolveModuleName(bestGuess ?? module, sourceFile.fileName, parsedConfig, compilerHost).resolvedModule;
if (resolvedModule === undefined) {
logger?.debug(options?.logging, `Could not resolve module "${module}" from ${normalizedSourceFileName}`);
skippedImports++;
return;
}
const { resolvedFileName, isExternalLibraryImport } = resolvedModule;
const normalizedTargetFileName = path_1.default.relative(rootDir, resolvedFileName);
logger?.debug(options?.logging, `Resolved "${module}" to: ${normalizedTargetFileName} (external: ${isExternalLibraryImport})`);
// Skip node_modules files if configured so
if (EXCLUDE_NODE_MODULES &&
normalizedTargetFileName.startsWith('node_modules')) {
logger?.debug(options?.logging, `Excluding node_modules file: ${normalizedTargetFileName}`);
skippedImports++;
return;
}
// Skip dist files if configured so
if (EXCLUDE_DIST && normalizedTargetFileName.startsWith('dist')) {
logger?.debug(options?.logging, `Excluding dist file: ${normalizedTargetFileName}`);
skippedImports++;
return;
}
const importKinds = (0, import_kinds_helper_1.determineImportKinds)(x);
const edge = {
source: (0, path_utils_1.normalizeWindowsPaths)(normalizedSourceFileName),
target: (0, path_utils_1.normalizeWindowsPaths)(normalizedTargetFileName),
external: isExternalLibraryImport ?? false,
importKinds: importKinds,
};
imports.push(edge);
if (imports.length % 100 === 0) {
logger?.debug(options?.logging, `Processed ${imports.length} imports so far...`);
}
}
catch (importError) {
// Skip this import if there's an error processing it
erroredImports++;
logger?.warn(options?.logging, `Error processing import in ${path_1.default.relative(rootDir, sourceFile.fileName)}: ${importError}`);
}
});
}
// Add self-referencing edges for all project files to ensure they appear in the graph
// This is crucial for files that don't import other project files
logger?.debug(options?.logging, 'Adding self-referencing edges for all project files');
for (const sourceFile of projectFiles) {
const normalizedFileName = (0, path_utils_1.normalizeWindowsPaths)(path_1.default.relative(rootDir, sourceFile.fileName));
// Skip node_modules files if configured so
//console.log('resolvedFileName:', resolvedFileName);
//console.log('normalizedTargetFileName:', normalizedTargetFileName);
if (EXCLUDE_NODE_MODULES && normalizedFileName.startsWith('node_modules')) {
logger?.debug(options?.logging, `Excluding node_modules file: ${normalizedFileName}`);
skippedImports++;
continue;
}
// Skip dist files if configured so
if (EXCLUDE_DIST && normalizedFileName.startsWith('dist')) {
logger?.debug(options?.logging, `Excluding dist file: ${normalizedFileName}`);
skippedImports++;
continue;
}
// Add self-referencing edge for every project file
const selfEdge = {
source: normalizedFileName,
target: normalizedFileName,
external: false,
importKinds: [], // Self-reference has no import kinds
};
imports.push(selfEdge);
}
if (projectFiles.length === 0) {
logger?.warn(options?.logging, 'No project files found - this might indicate a configuration or file discovery issue');
}
logger?.info(options?.logging, `Import analysis completed:`);
logger?.info(options?.logging, ` - Total edges found: ${imports.length}`);
logger?.info(options?.logging, ` - Files processed: ${processedFiles}`);
logger?.info(options?.logging, ` - Imports skipped: ${skippedImports}`);
logger?.info(options?.logging, ` - Import errors: ${erroredImports}`);
return imports;
};
//# sourceMappingURL=extract-graph.js.map
;