@notjustcoders/ioc-arise
Version:
Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
172 lines • 7.59 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValueAnalyzer = void 0;
const path_1 = require("path");
const container_1 = require("../container");
const logger_1 = require("../utils/logger");
class ValueAnalyzer {
constructor(sourceDir, valuePattern) {
this.astParser = container_1.container.astParser;
this.sourceDir = sourceDir;
if (valuePattern) {
try {
this.valuePattern = new RegExp(valuePattern, 'i');
}
catch (error) {
logger_1.Logger.warn(`Invalid value pattern regex: ${valuePattern}. Using default behavior.`);
}
}
}
async collectTokens(filePaths) {
const valueNames = new Set();
const interfaces = new Set();
for (const filePath of filePaths) {
try {
const root = this.astParser.parseFile(filePath);
// Collect interfaces
const extractedInterfaces = this.astParser.extractInterfaces(root);
extractedInterfaces.forEach(i => interfaces.add(i));
// Collect type aliases (export type Foo = ...)
const typeAliases = this.astParser.extractTypeAliases(root);
for (const [alias] of typeAliases.entries()) {
interfaces.add(alias);
}
// Collect exported values
const exportedValues = this.astParser.findAllExportedValues(root);
for (const valueNode of exportedValues) {
const valueName = this.astParser.extractValueName(valueNode);
if (valueName) {
const hasValueAnnotation = this.hasValueAnnotation(root, valueNode);
const matchesValuePattern = this.valuePattern ? this.valuePattern.test(valueName) : false;
if (hasValueAnnotation || matchesValuePattern) {
valueNames.add(valueName);
// Also check if it has a type that might be an interface
const interfaceName = this.astParser.extractValueType(valueNode);
if (interfaceName) {
interfaces.add(interfaceName);
}
}
}
}
}
catch (error) {
logger_1.Logger.warn(`Warning: Could not collect tokens from ${filePath}:`, { error });
}
}
return { valueNames, interfaces };
}
async analyzeFiles(filePaths, tokens) {
const allValues = [];
let allInterfaces = tokens?.interfaces || new Set();
const fileASTMap = new Map();
if (!tokens) {
// First pass: Collect interfaces for type checking
for (const filePath of filePaths) {
try {
const root = this.astParser.parseFile(filePath);
fileASTMap.set(filePath, root);
const interfaces = this.astParser.extractInterfaces(root);
interfaces.forEach(interfaceName => allInterfaces.add(interfaceName));
}
catch (error) {
logger_1.Logger.warn(`Warning: Could not parse ${filePath}:`, { error });
}
}
}
// Second pass: Analyze exported const values
for (const filePath of filePaths) {
let root = fileASTMap.get(filePath);
if (!root) {
try {
root = this.astParser.parseFile(filePath);
}
catch (e) {
continue;
}
}
if (root) {
const values = await this.analyzeFileFromAST(filePath, root, allInterfaces);
allValues.push(...values);
}
}
return allValues;
}
async analyzeFileFromAST(filePath, root, allInterfaces) {
const values = [];
try {
const exportedValues = this.astParser.findAllExportedValues(root);
logger_1.Logger.debug(`Found ${exportedValues.length} exported values in ${filePath}`);
const jsDocScopes = this.astParser.extractJSDocComments(root);
for (const valueNode of exportedValues) {
const valueName = this.astParser.extractValueName(valueNode);
if (!valueName) {
continue;
}
const interfaceName = this.astParser.extractValueType(valueNode);
// Check if value should be included
// Values are detected by:
// 1. @value JSDoc annotation (default behavior)
// 2. Value name matching valuePattern regex (if configured)
const hasValueAnnotation = this.hasValueAnnotation(root, valueNode);
const matchesValuePattern = this.valuePattern ? this.valuePattern.test(valueName) : false;
if (!hasValueAnnotation && !matchesValuePattern) {
logger_1.Logger.debug(`Skipping value ${valueName}: no @value annotation and doesn't match pattern`);
continue;
}
// Calculate import path
const importPath = this.calculateImportPath(filePath);
// Determine token - use interface name if available, otherwise use value name
const token = interfaceName || valueName;
// Default to singleton; can be overridden with /** @scope transient */
const scope = jsDocScopes.get(valueName) || 'singleton';
values.push({
name: valueName,
filePath,
interfaceName,
importPath,
scope,
token,
});
}
}
catch (error) {
logger_1.Logger.warn(`Warning: Could not analyze values in ${filePath}:`, { error });
}
return values;
}
hasValueAnnotation(root, valueNode) {
try {
// Find all JSDoc comments in the file
const comments = root.findAll({
rule: {
kind: 'comment'
}
});
const valueRange = valueNode.range();
// Find the JSDoc comment that appears immediately before this value
for (const comment of comments) {
const commentText = comment.text();
if (commentText.includes('/**') && commentText.includes('@value')) {
const commentRange = comment.range();
// Check if the comment comes before the value (within a few lines)
if (commentRange.end.line < valueRange.start.line &&
valueRange.start.line - commentRange.end.line <= 2) {
return true;
}
}
}
return false;
}
catch {
return false;
}
}
calculateImportPath(filePath) {
const relativePath = (0, path_1.relative)(this.sourceDir, filePath);
const pathWithoutExt = relativePath.replace(/\.ts$/, '');
// Convert to relative import path (./path/to/file)
return `./${pathWithoutExt}`;
}
}
exports.ValueAnalyzer = ValueAnalyzer;
//# sourceMappingURL=value-analyzer.js.map