mlld
Version:
mlld: a modular prompt scripting language
655 lines (649 loc) • 23.4 kB
JavaScript
import { logger } from './chunk-XGMRAGIT.mjs';
import { init_locationFormatter, locationFormatter_exports } from './chunk-OIIQ3NPY.mjs';
import { __name, __publicField, __toCommonJS } from './chunk-OMKLS24H.mjs';
import chalk from 'chalk';
import * as path from 'path';
// core/utils/sourceContextExtractor.ts
var _SourceContextExtractor = class _SourceContextExtractor {
constructor(fileSystem) {
__publicField(this, "fileSystem");
__publicField(this, "fileCache");
__publicField(this, "maxCacheSize");
__publicField(this, "cacheTtl");
this.fileSystem = fileSystem;
this.fileCache = /* @__PURE__ */ new Map();
this.maxCacheSize = 100;
this.cacheTtl = 6e4;
}
async extractContext(location, options = {}) {
const { contextLines = 2, maxLineLength = 120 } = options;
if (!location.file || !location.line) {
return null;
}
try {
const lines = await this.getFileLines(location.file);
return this.extractContextFromLines(lines, location, {
contextLines,
maxLineLength
});
} catch (error) {
return null;
}
}
/**
* Extract context from provided source content instead of reading from file
*/
extractContextFromSource(sourceContent, location, options = {}) {
if (!location.line) {
return null;
}
const lines = sourceContent.split("\n");
return this.extractContextFromLines(lines, location, options);
}
extractContextFromLines(lines, location, options = {}) {
const { contextLines = 2, maxLineLength = 120 } = options;
const errorLineIndex = location.line - 1;
if (errorLineIndex < 0 || errorLineIndex >= lines.length) {
return null;
}
const startLine = Math.max(0, errorLineIndex - contextLines);
const endLine = Math.min(lines.length - 1, errorLineIndex + contextLines);
const contextLines_ = [];
for (let i = startLine; i <= endLine; i++) {
let content = lines[i];
if (content.length > maxLineLength) {
const column = location.column || 1;
const start = Math.max(0, column - 40);
const end = Math.min(content.length, start + maxLineLength);
content = (start > 0 ? "..." : "") + content.slice(start, end) + (end < content.length ? "..." : "");
}
contextLines_.push({
number: i + 1,
content,
isErrorLine: i === errorLineIndex
});
}
return {
file: location.file,
lines: contextLines_,
errorLine: location.line,
errorColumn: location.column || 1
};
}
async getFileLines(filePath) {
const now = Date.now();
const cached = this.fileCache.get(filePath);
if (cached && now - cached.timestamp < this.cacheTtl) {
return cached.lines;
}
try {
const content = await this.fileSystem.readFile(filePath);
const lines = content.split("\n");
if (this.fileCache.size >= this.maxCacheSize) {
this.cleanCache();
}
this.fileCache.set(filePath, {
lines,
timestamp: now
});
return lines;
} catch (error) {
throw error;
}
}
cleanCache() {
const now = Date.now();
const entries = Array.from(this.fileCache.entries());
const expiredKeys = entries.filter(([_, cache]) => now - cache.timestamp >= this.cacheTtl).map(([key]) => key);
expiredKeys.forEach((key) => this.fileCache.delete(key));
if (this.fileCache.size >= this.maxCacheSize) {
const sortedEntries = entries.filter(([key]) => !expiredKeys.includes(key)).sort(([_, a], [__, b]) => a.timestamp - b.timestamp);
const toRemove = sortedEntries.slice(0, sortedEntries.length - this.maxCacheSize + 10);
toRemove.forEach(([key]) => this.fileCache.delete(key));
}
}
clearCache() {
this.fileCache.clear();
}
};
__name(_SourceContextExtractor, "SourceContextExtractor");
var SourceContextExtractor = _SourceContextExtractor;
var _SmartPathResolver = class _SmartPathResolver {
constructor(fileSystem) {
__publicField(this, "fileSystem");
__publicField(this, "projectRootCache", /* @__PURE__ */ new Map());
this.fileSystem = fileSystem;
}
/**
* Resolve a file path to the most appropriate display format
*/
async resolvePath(filePath, options = {}) {
const { basePath = process.cwd(), workingDirectory = process.cwd(), preferRelative = true, maxRelativeDepth = 3 } = options;
const absolutePath = path.resolve(filePath);
const projectRoot = await this.findProjectRoot(absolutePath, basePath);
const relativeFromCwd = path.relative(workingDirectory, absolutePath);
const relativeFromProject = projectRoot ? path.relative(projectRoot, absolutePath) : relativeFromCwd;
const isWithinProject = projectRoot !== null && !relativeFromProject.startsWith("..");
let displayPath;
let isRelative = false;
if (preferRelative && isWithinProject) {
const projectRelative = relativeFromProject.startsWith("./") ? relativeFromProject : `./${relativeFromProject}`;
const upLevels = (projectRelative.match(/\.\.\//g) || []).length;
if (upLevels <= maxRelativeDepth) {
displayPath = projectRelative;
isRelative = true;
} else {
displayPath = absolutePath;
}
} else if (preferRelative && !relativeFromCwd.startsWith("..")) {
const upLevels = (relativeFromCwd.match(/\.\.\//g) || []).length;
if (upLevels <= maxRelativeDepth) {
displayPath = relativeFromCwd.startsWith("./") ? relativeFromCwd : `./${relativeFromCwd}`;
isRelative = true;
} else {
displayPath = absolutePath;
}
} else {
displayPath = absolutePath;
}
return {
display: displayPath,
absolute: absolutePath,
relative: isWithinProject ? relativeFromProject : relativeFromCwd,
isRelative,
isWithinProject
};
}
/**
* Find the project root by looking for package.json, .git, or other indicators
*/
async findProjectRoot(startPath, fallbackBasePath) {
const cacheKey = startPath;
if (this.projectRootCache.has(cacheKey)) {
return this.projectRootCache.get(cacheKey) || null;
}
let currentDir = path.dirname(startPath);
const rootDir = path.parse(currentDir).root;
while (currentDir !== rootDir) {
const indicators = [
"package.json",
".git",
"pyproject.toml",
"Cargo.toml",
"go.mod",
"composer.json",
"pom.xml",
"build.gradle",
"Makefile",
"README.md"
];
for (const indicator of indicators) {
const indicatorPath = path.join(currentDir, indicator);
try {
if (await this.fileExists(indicatorPath)) {
this.projectRootCache.set(cacheKey, currentDir);
return currentDir;
}
} catch {
}
}
currentDir = path.dirname(currentDir);
}
this.projectRootCache.set(cacheKey, fallbackBasePath);
return fallbackBasePath;
}
/**
* Check if a file exists
*/
async fileExists(filePath) {
try {
await this.fileSystem.readFile(filePath);
return true;
} catch {
return false;
}
}
/**
* Clear the project root cache
*/
clearCache() {
this.projectRootCache.clear();
}
/**
* Format a file path for error display with line and column
*/
formatPathForDisplay(resolvedPath, line, column) {
let result = resolvedPath.display;
if (line !== void 0) {
result += `:${line}`;
if (column !== void 0) {
result += `:${column}`;
}
}
return result;
}
};
__name(_SmartPathResolver, "SmartPathResolver");
var SmartPathResolver = _SmartPathResolver;
// core/utils/enhancedLocationFormatter.ts
var _EnhancedLocationFormatter = class _EnhancedLocationFormatter {
constructor(fileSystem) {
__publicField(this, "pathResolver");
this.pathResolver = new SmartPathResolver(fileSystem);
}
async formatLocation(location, options = {}) {
const { useSmartPaths = true, ...pathOptions } = options;
if (!location) {
return {
display: "unknown location"
};
}
if ("start" in location && location.start) {
const startPos = location.start;
const filePath = startPos.filePath || location.filePath;
if (filePath) {
let displayPath = filePath;
let isRelative = false;
let isWithinProject = false;
if (useSmartPaths) {
try {
const resolvedPath = await this.pathResolver.resolvePath(filePath, pathOptions);
displayPath = resolvedPath.display;
isRelative = resolvedPath.isRelative;
isWithinProject = resolvedPath.isWithinProject;
} catch {
displayPath = filePath;
}
}
const parts = [
displayPath
];
if (startPos.line !== void 0) {
if (startPos.column !== void 0) {
parts.push(`${startPos.line}:${startPos.column}`);
} else {
parts.push(`line ${startPos.line}`);
}
}
return {
display: parts.join(":"),
file: filePath,
displayPath,
line: startPos.line,
column: startPos.column,
isRelative,
isWithinProject
};
}
}
if ("filePath" in location && location.filePath) {
let displayPath = location.filePath;
let isRelative = false;
let isWithinProject = false;
if (useSmartPaths) {
try {
const resolvedPath = await this.pathResolver.resolvePath(location.filePath, pathOptions);
displayPath = resolvedPath.display;
isRelative = resolvedPath.isRelative;
isWithinProject = resolvedPath.isWithinProject;
} catch {
displayPath = location.filePath;
}
}
const parts = [
displayPath
];
if (location.line !== void 0) {
if (location.column !== void 0) {
parts.push(`${location.line}:${location.column}`);
} else {
parts.push(`line ${location.line}`);
}
}
return {
display: parts.join(":"),
file: location.filePath,
displayPath,
line: location.line,
column: location.column,
isRelative,
isWithinProject
};
}
if ("line" in location && location.line !== void 0) {
const parts = [];
if (location.column !== void 0) {
parts.push(`line ${location.line}, column ${location.column}`);
} else {
parts.push(`line ${location.line}`);
}
return {
display: parts.join(""),
line: location.line,
column: location.column
};
}
return {
display: "unknown location"
};
}
async formatLocationForError(location, options = {}) {
const formatted = await this.formatLocation(location, options);
return formatted.display;
}
clearCache() {
this.pathResolver.clearCache();
}
};
__name(_EnhancedLocationFormatter, "EnhancedLocationFormatter");
var EnhancedLocationFormatter = _EnhancedLocationFormatter;
// core/utils/errorDisplayFormatter.ts
var _ErrorDisplayFormatter = class _ErrorDisplayFormatter {
constructor(fileSystem) {
__publicField(this, "sourceExtractor");
__publicField(this, "locationFormatter");
this.sourceExtractor = new SourceContextExtractor(fileSystem);
this.locationFormatter = new EnhancedLocationFormatter(fileSystem);
}
async formatError(error, options = {}) {
const { showSourceContext = true, contextLines = 2, maxLineLength = 120, useColors = true, useSmartPaths = true, basePath, workingDirectory = process.cwd() } = options;
const parts = [];
const errorHeader = this.formatErrorHeader(error, useColors);
parts.push(errorHeader);
if (error.details?.peggyFormatted) {
const parseErrorIndicator = useColors ? chalk.red.bold("\u2718 Parse Error") : "\u2718 Parse Error";
parts.push("\n" + parseErrorIndicator);
const sourceSection = this.extractPeggySourceSection(error.details.peggyFormatted, useColors);
if (sourceSection) {
parts.push(sourceSection);
}
}
if (showSourceContext && error.sourceLocation && !error.details?.peggyFormatted) {
logger.debug("[ErrorDisplay] Formatting source context:", {
sourceLocation: error.sourceLocation,
errorDetails: error.details,
hasFile: !!error.sourceLocation.filePath || !!error.details?.filePath
});
const formattedLocation = await this.locationFormatter.formatLocation(error.sourceLocation, {
useSmartPaths,
basePath,
workingDirectory,
preferRelative: true,
maxRelativeDepth: 3
});
logger.debug("[ErrorDisplay] Formatted location:", formattedLocation);
const sourceContent = error.sourceContent || error.details?.sourceContent;
if (sourceContent) {
const sourceContext = this.sourceExtractor.extractContextFromSource(sourceContent, {
display: formattedLocation.display,
file: formattedLocation.displayPath || formattedLocation.file || "<stdin>",
line: formattedLocation.line,
column: formattedLocation.column
}, {
contextLines,
maxLineLength
});
logger.debug("[ErrorDisplay] Source context from content:", {
hasContext: !!sourceContext,
sourceLength: sourceContent.length,
lines: sourceContext?.lines?.length
});
if (sourceContext) {
const contextDisplay = this.formatSourceContext(sourceContext, useColors, error.details?.mlldLocation);
parts.push(contextDisplay);
}
} else if (formattedLocation.file) {
const sourceContext = await this.sourceExtractor.extractContext({
display: formattedLocation.display,
file: formattedLocation.file,
line: formattedLocation.line,
column: formattedLocation.column
}, {
contextLines,
maxLineLength
});
logger.debug("[ErrorDisplay] Source context extracted:", {
hasContext: !!sourceContext,
file: formattedLocation.file,
lines: sourceContext?.lines?.length
});
if (sourceContext) {
const enhancedSourceContext = {
...sourceContext,
file: formattedLocation.displayPath || formattedLocation.file
// Use smart path for display
};
const contextDisplay = this.formatSourceContext(enhancedSourceContext, useColors, error.details?.mlldLocation);
parts.push(contextDisplay);
}
} else {
logger.debug("[ErrorDisplay] No file path in formatted location and no source content");
}
} else {
logger.debug("[ErrorDisplay] No source context requested or no sourceLocation:", {
showSourceContext,
hasSourceLocation: !!error.sourceLocation
});
}
const detailsDisplay = await this.formatErrorDetails(error, useColors, {
useSmartPaths,
basePath,
workingDirectory
});
if (detailsDisplay) {
parts.push(detailsDisplay);
}
if (error.details?.directiveTrace && error.details.directiveTrace.length > 0) {
const { DirectiveTraceFormatter } = await import('./DirectiveTraceFormatter-EHF7LN6H.mjs');
const traceFormatter = new DirectiveTraceFormatter();
const trace = traceFormatter.format(error.details.directiveTrace, useColors);
parts.push("\n" + trace);
}
if (error.details?.suggestion) {
const suggestion = useColors ? chalk.cyan(`\u{1F4A1} ${error.details.suggestion}`) : `\u{1F4A1} ${error.details.suggestion}`;
parts.push(suggestion);
}
return parts.join("\n\n");
}
formatErrorHeader(error, useColors) {
const errorName = error.name.replace("Error", "");
if (useColors) {
return chalk.red.bold(`${errorName}: ${error.message}`);
}
return `${errorName}: ${error.message}`;
}
formatSourceContext(context, useColors, mlldLocation) {
const parts = [];
if (context.file) {
const fileHeader = useColors ? chalk.blue.bold(` ${context.file}:${context.errorLine}:${context.errorColumn}`) : ` ${context.file}:${context.errorLine}:${context.errorColumn}`;
parts.push(fileHeader);
}
const maxLineNum = Math.max(...context.lines.map((l) => l.number));
const lineNumWidth = String(maxLineNum).length;
for (const line of context.lines) {
const lineNum = String(line.number).padStart(lineNumWidth, " ");
const prefix = ` ${lineNum} | `;
if (line.isErrorLine) {
const errorLineDisplay = useColors ? chalk.red(`${prefix}${line.content}`) : `${prefix}${line.content}`;
parts.push(errorLineDisplay);
const indicator = this.createErrorIndicator(prefix.length, context.errorColumn, line.content, useColors, mlldLocation);
if (indicator) {
parts.push(indicator);
}
} else {
const contextLineDisplay = useColors ? chalk.gray(`${prefix}${line.content}`) : `${prefix}${line.content}`;
parts.push(contextLineDisplay);
}
}
return parts.join("\n");
}
createErrorIndicator(prefixLength, column, lineContent, useColors, mlldLocation) {
if (column < 1) return null;
if (mlldLocation && mlldLocation.column && mlldLocation.length) {
const actualColumn = mlldLocation.column;
const errorLength = mlldLocation.length || 1;
const spaces2 = " ".repeat(prefixLength + actualColumn - 1);
const arrows = "^".repeat(Math.min(errorLength, lineContent.length - actualColumn + 1));
let hint = "";
if (mlldLocation.expectedToken) {
hint = ` Expected: ${mlldLocation.expectedToken}`;
}
return useColors ? `${spaces2}${chalk.red.bold(arrows)}${chalk.yellow(hint)}` : `${spaces2}${arrows}${hint}`;
}
const spaces = " ".repeat(prefixLength + column - 1);
const indicator = "^";
return useColors ? `${spaces}${chalk.red.bold(indicator)}` : `${spaces}${indicator}`;
}
extractPeggySourceSection(peggyFormatted, useColors) {
const lines = peggyFormatted.split("\n");
const sourceLines = [];
let inSourceSection = false;
for (const line of lines) {
if (line.trim().startsWith("-->")) {
sourceLines.push(line);
inSourceSection = true;
continue;
}
if (inSourceSection) {
if (line === "" && sourceLines.length > 2 && sourceLines[sourceLines.length - 1].includes("^")) {
break;
}
sourceLines.push(line);
}
}
return sourceLines.length > 0 ? sourceLines.join("\n") : null;
}
async formatErrorDetails(error, useColors, pathOptions) {
if (!error.details || typeof error.details !== "object") {
return null;
}
const relevantDetails = [];
for (const [key, value] of Object.entries(error.details)) {
if (key === "suggestion") continue;
if (key === "sourceContent") continue;
if (key === "peggyFormatted") continue;
if (key === "mlldLocation") continue;
if (value === void 0 || value === null) continue;
if (value && typeof value === "object" && ("line" in value || "filePath" in value)) {
try {
const formattedLocation = await this.locationFormatter.formatLocationForError(value, pathOptions);
relevantDetails.push(` ${key}: ${formattedLocation}`);
} catch {
const { formatLocationForError } = (init_locationFormatter(), __toCommonJS(locationFormatter_exports));
relevantDetails.push(` ${key}: ${formatLocationForError(value)}`);
}
} else {
relevantDetails.push(` ${key}: ${String(value)}`);
}
}
if (relevantDetails.length === 0) {
return null;
}
const header = useColors ? chalk.gray("Details:") : "Details:";
const details = useColors ? chalk.gray(relevantDetails.join("\n")) : relevantDetails.join("\n");
return `${header}
${details}`;
}
};
__name(_ErrorDisplayFormatter, "ErrorDisplayFormatter");
var ErrorDisplayFormatter = _ErrorDisplayFormatter;
// core/utils/errorFormatSelector.ts
var _ErrorFormatSelector = class _ErrorFormatSelector {
constructor(fileSystem) {
__publicField(this, "fileSystem");
__publicField(this, "formatter");
this.fileSystem = fileSystem;
if (fileSystem) {
this.formatter = new ErrorDisplayFormatter(fileSystem);
}
}
/**
* Format error for CLI display with colors and context
*/
async formatForCLI(error, options = {}) {
if (!this.formatter) {
return this.formatForAPI(error, options).formatted;
}
return await this.formatter.formatError(error, {
showSourceContext: options.useSourceContext ?? true,
useColors: options.useColors ?? true,
useSmartPaths: options.useSmartPaths ?? true,
basePath: options.basePath,
workingDirectory: options.workingDirectory || process.cwd(),
contextLines: options.contextLines || 2
});
}
/**
* Format error for API usage (no colors, structured data)
*/
formatForAPI(error, options = {}) {
const json = {
name: error.name,
message: error.message,
code: error.code,
severity: error.severity,
sourceLocation: error.sourceLocation,
details: error.details,
cause: error.cause instanceof Error ? {
name: error.cause.name,
message: error.cause.message
} : error.cause
};
let formatted = `${error.name}: ${error.message}`;
if (error.sourceLocation) {
if ("filePath" in error.sourceLocation && error.sourceLocation.filePath) {
let location = error.sourceLocation.filePath;
if (error.sourceLocation.line) {
location += `:${error.sourceLocation.line}`;
if (error.sourceLocation.column) {
location += `:${error.sourceLocation.column}`;
}
}
formatted += `
at ${location}`;
} else if ("line" in error.sourceLocation && error.sourceLocation.line) {
formatted += `
at line ${error.sourceLocation.line}`;
if (error.sourceLocation.column) {
formatted += `, column ${error.sourceLocation.column}`;
}
}
}
if (error.details?.suggestion) {
formatted += `
Suggestion: ${error.details.suggestion}`;
}
return {
formatted,
raw: error,
json
};
}
/**
* Auto-detect appropriate format based on environment
*/
async formatAuto(error, options = {}) {
const isTTY = process.stdout?.isTTY ?? false;
const hasColors = options.useColors ?? isTTY;
const hasContext = options.useSourceContext ?? isTTY;
if (hasColors && hasContext && this.formatter) {
const formatted = await this.formatForCLI(error, {
...options,
useColors: hasColors,
useSourceContext: hasContext
});
return {
formatted,
raw: error,
json: error.toJSON()
};
} else {
return this.formatForAPI(error, options);
}
}
};
__name(_ErrorFormatSelector, "ErrorFormatSelector");
var ErrorFormatSelector = _ErrorFormatSelector;
export { ErrorFormatSelector };
//# sourceMappingURL=chunk-JO67PGXR.mjs.map
//# sourceMappingURL=chunk-JO67PGXR.mjs.map