@tw-enigma/core
Version:
CSS optimization engine for tw-enigma
1,398 lines (1,393 loc) • 1.23 MB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
// src/utils/logger.ts
import chalk from "chalk";
import { createWriteStream, existsSync, mkdirSync, statSync, unlinkSync } from "fs";
import { dirname } from "path";
import { gzipSync } from "zlib";
function parseLogLevel(level) {
if (!level) return LogLevel.INFO;
const upperLevel = level.toUpperCase();
switch (upperLevel) {
case "TRACE":
return LogLevel.TRACE;
case "DEBUG":
return LogLevel.DEBUG;
case "INFO":
return LogLevel.INFO;
case "WARN":
return LogLevel.WARN;
case "ERROR":
return LogLevel.ERROR;
case "FATAL":
return LogLevel.FATAL;
default:
return LogLevel.INFO;
}
}
function createFileOutputFromEnv() {
const filePath = process.env.ENIGMA_LOG_FILE;
if (!filePath) return void 0;
return {
filePath,
format: process.env.ENIGMA_LOG_FORMAT || "human",
maxSize: process.env.ENIGMA_LOG_MAX_SIZE ? parseInt(process.env.ENIGMA_LOG_MAX_SIZE) : void 0,
maxFiles: process.env.ENIGMA_LOG_MAX_FILES ? parseInt(process.env.ENIGMA_LOG_MAX_FILES) : void 0,
compress: process.env.ENIGMA_LOG_COMPRESS === "true"
};
}
function createLogger(component, _options) {
return logger.child(component, _options);
}
var LogLevel, LogLevelNames, LogLevelColors, Logger, logger;
var init_logger = __esm({
"src/utils/logger.ts"() {
"use strict";
LogLevel = {
TRACE: 0,
DEBUG: 1,
INFO: 2,
WARN: 3,
ERROR: 4,
FATAL: 5
};
LogLevelNames = {
[LogLevel.TRACE]: "TRACE",
[LogLevel.DEBUG]: "DEBUG",
[LogLevel.INFO]: "INFO",
[LogLevel.WARN]: "WARN",
[LogLevel.ERROR]: "ERROR",
[LogLevel.FATAL]: "FATAL"
};
LogLevelColors = {
[LogLevel.TRACE]: chalk.gray,
[LogLevel.DEBUG]: chalk.cyan,
[LogLevel.INFO]: chalk.blue,
[LogLevel.WARN]: chalk.yellow,
[LogLevel.ERROR]: chalk.red,
[LogLevel.FATAL]: chalk.magenta
};
Logger = class _Logger {
constructor(options = {}) {
this.progressStates = /* @__PURE__ */ new Map();
this.level = options.level ?? LogLevel.INFO;
this.verbose = options.verbose ?? false;
this.veryVerbose = options.veryVerbose ?? false;
this.quiet = options.quiet ?? false;
this.silent = options.silent ?? false;
this.outputFormat = options.outputFormat ?? "human";
this.colorize = options.colorize ?? true;
this.timestamp = options.timestamp ?? true;
this.component = options.component;
this.fileOutput = options.fileOutput;
this.enableProgressTracking = options.enableProgressTracking ?? false;
if (this.veryVerbose) {
this.verbose = true;
if (this.level > LogLevel.TRACE) {
this.level = LogLevel.TRACE;
}
} else if (this.verbose && this.level > LogLevel.DEBUG) {
this.level = LogLevel.DEBUG;
}
if (this.quiet) {
this.verbose = false;
this.veryVerbose = false;
if (this.level < LogLevel.WARN) {
this.level = LogLevel.WARN;
}
}
if (this.fileOutput) {
this.initializeFileOutput();
}
}
/**
* Initialize file output with rotation support
*/
initializeFileOutput() {
if (!this.fileOutput) return;
try {
const dir = dirname(this.fileOutput.filePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
this.rotateLogsIfNeeded();
this.fileStream = createWriteStream(this.fileOutput.filePath, {
flags: "a"
});
this.fileStream.on("error", (error) => {
console.error(`Logger file stream error: ${error.message}`);
this.fileStream = void 0;
});
} catch (error) {
console.error(
`Failed to initialize file output: ${error instanceof Error ? error.message : String(error)}`
);
this.fileStream = void 0;
}
}
/**
* Rotate log files if size limit exceeded
*/
rotateLogsIfNeeded() {
if (!this.fileOutput || !existsSync(this.fileOutput.filePath)) return;
const maxSize = this.fileOutput.maxSize ?? 10 * 1024 * 1024;
const maxFiles = this.fileOutput.maxFiles ?? 5;
const stats = statSync(this.fileOutput.filePath);
if (stats.size >= maxSize) {
this.rotateLogFiles(maxFiles);
}
}
/**
* Perform log file rotation with optional compression
*/
rotateLogFiles(maxFiles) {
if (!this.fileOutput) return;
const basePath = this.fileOutput.filePath;
const compress = this.fileOutput.compress ?? false;
if (this.fileStream) {
this.fileStream.end();
this.fileStream = void 0;
}
for (let i = maxFiles - 1; i >= 1; i--) {
const oldPath = `${basePath}.${i}${compress ? ".gz" : ""}`;
const newPath = `${basePath}.${i + 1}${compress ? ".gz" : ""}`;
if (existsSync(oldPath)) {
if (i === maxFiles - 1) {
unlinkSync(oldPath);
} else {
unlinkSync(newPath);
__require("fs").renameSync(oldPath, newPath);
}
}
}
if (existsSync(basePath)) {
const rotatedPath = `${basePath}.1`;
if (compress) {
const content = __require("fs").readFileSync(basePath);
const compressed = gzipSync(content);
__require("fs").writeFileSync(`${rotatedPath}.gz`, compressed);
unlinkSync(basePath);
} else {
__require("fs").renameSync(basePath, rotatedPath);
}
}
}
/**
* Set the minimum log level
*/
setLevel(level) {
this.level = level;
}
/**
* Enable or disable verbose logging
*/
setVerbose(verbose) {
this.verbose = verbose;
if (verbose && this.level > LogLevel.DEBUG) {
this.level = LogLevel.DEBUG;
}
}
/**
* Enable or disable very verbose logging
*/
setVeryVerbose(veryVerbose) {
this.veryVerbose = veryVerbose;
if (veryVerbose) {
this.verbose = true;
if (this.level > LogLevel.TRACE) {
this.level = LogLevel.TRACE;
}
}
}
/**
* Enable or disable quiet mode
*/
setQuiet(quiet) {
this.quiet = quiet;
if (quiet) {
this.verbose = false;
this.veryVerbose = false;
if (this.level < LogLevel.WARN) {
this.level = LogLevel.WARN;
}
}
}
/**
* Enable or disable silent mode
*/
setSilent(silent) {
this.silent = silent;
}
/**
* Set output format
*/
setOutputFormat(format) {
this.outputFormat = format;
}
/**
* Configure file output
*/
setFileOutput(options) {
if (this.fileStream) {
this.fileStream.end();
this.fileStream = void 0;
}
this.fileOutput = options;
this.initializeFileOutput();
}
/**
* Disable file output
*/
disableFileOutput() {
if (this.fileStream) {
this.fileStream.end();
this.fileStream = void 0;
}
this.fileOutput = void 0;
}
/**
* Start progress tracking for an operation
*/
startProgress(id, options) {
if (!this.enableProgressTracking) return;
this.progressStates.set(id, {
...options,
startTime: Date.now()
});
if (this.verbose) {
const label = options.label || id;
this.info(`\u{1F4CA} Starting ${label} (0/${options.total})`);
}
}
/**
* Update progress for an operation
*/
updateProgress(id, current, additionalInfo) {
if (!this.enableProgressTracking) return;
const progress = this.progressStates.get(id);
if (!progress) return;
progress.current = current;
const percentage = Math.round(current / progress.total * 100);
const elapsed = Date.now() - progress.startTime;
let message = `\u{1F4C8} ${progress.label || id}: ${current}/${progress.total}`;
if (progress.showPercentage !== false) {
message += ` (${percentage}%)`;
}
if (progress.showETA !== false && current > 0) {
const estimatedTotal = elapsed / current * progress.total;
const eta = Math.round((estimatedTotal - elapsed) / 1e3);
message += ` ETA: ${eta}s`;
}
if (additionalInfo) {
message += ` - ${additionalInfo}`;
}
if (this.verbose) {
this.debug(message);
}
}
/**
* Complete progress tracking for an operation
*/
completeProgress(id, summary) {
if (!this.enableProgressTracking) return;
const progress = this.progressStates.get(id);
if (!progress) return;
const elapsed = Date.now() - progress.startTime;
const duration = Math.round(elapsed / 1e3);
let message = `\u2705 Completed ${progress.label || id} (${progress.total} items in ${duration}s)`;
if (summary) {
message += ` - ${summary}`;
}
if (this.verbose) {
this.info(message);
}
this.progressStates.delete(id);
}
/**
* Log performance metrics
*/
performanceMetrics(operation, metrics, context) {
const extendedContext = {
...context,
operation,
processingTime: metrics.processingTime,
memoryUsage: metrics.memoryUsage.heapUsed,
fileCount: metrics.fileCount,
totalFileSize: metrics.totalFileSize,
optimizationRatio: metrics.optimizationRatio
};
const heapMB = Math.round(metrics.memoryUsage.heapUsed / 1024 / 1024);
let message = `\u26A1 ${operation} completed in ${metrics.processingTime}ms (heap: ${heapMB}MB)`;
if (metrics.fileCount) {
message += `, processed ${metrics.fileCount} files`;
}
if (metrics.totalFileSize) {
const sizeMB = Math.round(metrics.totalFileSize / 1024 / 1024 * 100) / 100;
message += `, total size: ${sizeMB}MB`;
}
if (metrics.optimizationRatio) {
const ratio = Math.round(metrics.optimizationRatio * 100);
message += `, optimization: ${ratio}%`;
}
if (this.veryVerbose) {
this.trace(message, extendedContext);
} else if (this.verbose) {
this.debug(message, extendedContext);
}
}
/**
* Log detailed file operation
*/
fileOperation(operation, filePath, details) {
if (!this.veryVerbose) return;
let message = `\u{1F4C1} ${operation}: ${filePath}`;
const context = { operation, filePath };
if (details?.size) {
const sizeKB = Math.round(details.size / 1024);
message += ` (${sizeKB}KB)`;
context.fileSize = details.size;
}
if (details?.processingTime) {
message += ` - ${details.processingTime}ms`;
context.processingTime = details.processingTime;
}
if (details?.result) {
message += ` \u2192 ${details.result}`;
}
this.trace(message, context);
}
/**
* Log step-by-step process details
*/
processStep(step, details, context) {
if (!this.verbose) return;
let message = `\u{1F504} ${step}`;
if (details) {
message += `: ${details}`;
}
this.debug(message, context);
}
/**
* Create a child logger with additional context
*/
child(component, options = {}) {
return new _Logger({
level: this.level,
verbose: this.verbose,
veryVerbose: this.veryVerbose,
quiet: this.quiet,
silent: this.silent,
outputFormat: this.outputFormat,
colorize: this.colorize,
timestamp: this.timestamp,
component,
fileOutput: this.fileOutput,
enableProgressTracking: this.enableProgressTracking,
...options
});
}
/**
* Check if a log level should be output
*/
shouldLog(level) {
if (this.silent) return false;
return level >= this.level;
}
/**
* Format timestamp
*/
getTimestamp() {
return (/* @__PURE__ */ new Date()).toISOString();
}
/**
* Create a structured log entry
*/
createLogEntry(level, message, context, error) {
const entry = {
level: LogLevelNames[level],
message,
timestamp: this.getTimestamp()
};
if (this.component) {
entry.component = this.component;
}
if (context && Object.keys(context).length > 0) {
entry.context = { ...context };
}
if (error) {
entry.error = {
name: error.name,
message: error.message,
stack: error.stack,
code: error.code
};
}
return entry;
}
/**
* Format log entry for human-readable output
*/
formatHuman(entry) {
const levelName = entry.level.padEnd(5);
const colorFn = LogLevelColors[LogLevel[entry.level]];
let output = "";
if (this.timestamp) {
output += chalk.gray(`[${entry.timestamp}] `);
}
if (this.colorize) {
output += colorFn(`${levelName} `);
} else {
output += `${levelName} `;
}
if (entry.component) {
output += chalk.gray(`[${entry.component}] `);
}
output += entry.message;
if (entry.context && Object.keys(entry.context).length > 0) {
output += chalk.gray(` ${JSON.stringify(entry.context)}`);
}
if (entry.error) {
output += "\n" + (entry.error.stack || `${entry.error.name}: ${entry.error.message}`);
}
return output;
}
/**
* Format log entry for CSV output
*/
formatCSV(entry) {
const timestamp = entry.timestamp;
const level = entry.level;
const component = entry.component || "";
const message = entry.message.replace(/"/g, '""');
const context = entry.context ? JSON.stringify(entry.context).replace(/"/g, '""') : "";
const errorMessage = entry.error ? `${entry.error.name}: ${entry.error.message}`.replace(/"/g, '""') : "";
return `"${timestamp}","${level}","${component}","${message}","${context}","${errorMessage}"`;
}
/**
* Output a log entry to console and/or file
*/
output(entry) {
if (this.outputFormat === "json") {
if (entry.level === "ERROR" || entry.level === "FATAL") {
console.error(JSON.stringify(entry));
} else {
console.log(JSON.stringify(entry));
}
} else {
const formattedMessage = this.formatHuman(entry);
if (entry.level === "ERROR" || entry.level === "FATAL") {
console.error(formattedMessage);
} else {
console.log(formattedMessage);
}
}
if (this.fileStream && !this.fileStream.destroyed) {
try {
let fileContent;
const fileFormat = this.fileOutput?.format || "human";
switch (fileFormat) {
case "json":
fileContent = JSON.stringify(entry) + "\n";
break;
case "csv":
fileContent = this.formatCSV(entry) + "\n";
break;
default:
fileContent = this.formatHuman(entry) + "\n";
}
this.fileStream.write(fileContent);
this.rotateLogsIfNeeded();
} catch (error) {
console.error(
`Failed to write to log file: ${error instanceof Error ? error.message : String(error)}`
);
}
}
}
/**
* Core logging method
*/
log(level, message, context, _error) {
if (!this.shouldLog(level)) return;
const entry = this.createLogEntry(level, message, context, _error);
this.output(entry);
}
/**
* Log a trace message (most verbose)
*/
trace(message, context) {
this.log(LogLevel.TRACE, message, context);
}
/**
* Log a debug message
*/
debug(message, context) {
this.log(LogLevel.DEBUG, message, context);
}
/**
* Log an info message
*/
info(message, context) {
this.log(LogLevel.INFO, message, context);
}
/**
* Log a warning message
*/
warn(message, context) {
this.log(LogLevel.WARN, message, context);
}
/**
* Log an error message
*/
error(messageOrError, context) {
if (messageOrError instanceof Error) {
this.log(LogLevel.ERROR, messageOrError.message, context, messageOrError);
} else {
this.log(LogLevel.ERROR, messageOrError, context);
}
}
/**
* Log a fatal error message
*/
fatal(messageOrError, context) {
if (messageOrError instanceof Error) {
this.log(LogLevel.FATAL, messageOrError.message, context, messageOrError);
} else {
this.log(LogLevel.FATAL, messageOrError, context);
}
}
/**
* Log performance timing
*/
timing(operation, duration, context) {
const extendedContext = {
...context,
operation,
processingTime: duration
};
this.debug(`Operation "${operation}" completed in ${duration}ms`, extendedContext);
}
/**
* Clean up resources (close file streams)
*/
cleanup() {
if (this.fileStream) {
this.fileStream.end();
this.fileStream = void 0;
}
this.progressStates.clear();
}
/**
* Get current logger state for debugging
*/
getState() {
return {
level: this.level,
verbose: this.verbose,
veryVerbose: this.veryVerbose,
quiet: this.quiet,
silent: this.silent,
fileOutputEnabled: !!this.fileOutput,
progressTrackingEnabled: this.enableProgressTracking,
activeProgressCount: this.progressStates.size
};
}
};
logger = new Logger({
level: process.env.ENIGMA_LOG_LEVEL ? parseLogLevel(process.env.ENIGMA_LOG_LEVEL) : process.env.NODE_ENV === "development" ? LogLevel.DEBUG : LogLevel.INFO,
verbose: process.env.ENIGMA_VERBOSE === "true",
veryVerbose: process.env.ENIGMA_VERY_VERBOSE === "true",
quiet: process.env.ENIGMA_QUIET === "true",
colorize: process.stdout.isTTY,
timestamp: true,
fileOutput: createFileOutputFromEnv(),
enableProgressTracking: process.env.ENIGMA_PROGRESS_TRACKING !== "false"
});
}
});
// src/utils/pathUtils.ts
import * as path from "path";
import { z as z10 } from "zod";
function createPathUtils(options = {}) {
return new PathUtils(options);
}
function calculateRelativePath(fromPath, toPath, options = {}) {
const utils = createPathUtils(options);
const result = utils.calculateRelativePath(fromPath, toPath);
return result.relativePath;
}
function validatePath(inputPath, context = "path") {
const utils = createPathUtils();
return utils.validatePath(inputPath, context);
}
function normalizePath(inputPath, forWeb = false) {
const utils = createPathUtils();
return utils.normalizePath(inputPath, forWeb);
}
function isPathSafe(inputPath) {
const validation = validatePath(inputPath);
return validation.isValid && !validation.security.hasTraversal;
}
function calculateRelativePathsBatch(pairs, options = {}) {
const utils = createPathUtils(options);
return pairs.map(({ from, to }) => utils.calculateRelativePath(from, to));
}
var PathCalculationOptionsSchema, PathUtilsError, PathSecurityError, PathValidationError, PathUtils;
var init_pathUtils = __esm({
"src/utils/pathUtils.ts"() {
"use strict";
PathCalculationOptionsSchema = z10.object({
/** Use relative paths instead of absolute paths */
useRelativePaths: z10.boolean().default(true),
/** Base path for resolving relative paths */
basePath: z10.string().optional(),
/** Whether to normalize paths for web use (forward slashes) */
normalizeForWeb: z10.boolean().default(true),
/** Maximum allowed path depth to prevent excessive nesting */
maxDepth: z10.number().min(1).max(100).default(50),
/** Whether to resolve symbolic links */
resolveSymlinks: z10.boolean().default(false),
/** Enable path traversal protection */
enableSecurity: z10.boolean().default(true)
});
PathUtilsError = class extends Error {
constructor(message, code, cause) {
super(message);
this.name = "PathUtilsError";
this.code = code;
this.cause = cause;
}
};
PathSecurityError = class extends PathUtilsError {
constructor(message, path8, cause) {
super(message, "PATH_SECURITY_ERROR", cause);
this.name = "PathSecurityError";
this.path = path8;
}
};
PathValidationError = class extends PathUtilsError {
constructor(message, path8, cause) {
super(message, "PATH_VALIDATION_ERROR", cause);
this.name = "PathValidationError";
this.path = path8;
}
};
PathUtils = class {
constructor(options = {}) {
this.pathCache = /* @__PURE__ */ new Map();
this.validationCache = /* @__PURE__ */ new Map();
this.maxCacheSize = 1e3;
this.options = PathCalculationOptionsSchema.parse(options);
}
/**
* Calculate relative path from one file to another
*/
calculateRelativePath(fromPath, toPath, options) {
const mergedOptions = { ...this.options, ...options };
const cacheKey = `${fromPath}::${toPath}::${JSON.stringify(mergedOptions, Object.keys(mergedOptions).sort())}`;
if (this.pathCache.has(cacheKey)) {
return this.pathCache.get(cacheKey);
}
try {
const fromValidation = this.validatePath(fromPath, "fromPath");
if (!fromValidation.isValid) {
throw new PathValidationError(
`Invalid fromPath: ${fromValidation.errors.join(", ")}`,
fromPath
);
}
const toValidation = this.validatePath(toPath, "toPath");
if (!toValidation.isValid) {
throw new PathValidationError(`Invalid toPath: ${toValidation.errors.join(", ")}`, toPath);
}
if (!mergedOptions.useRelativePaths) {
let normalizedPath = toPath;
if (mergedOptions.normalizeForWeb) {
normalizedPath = this.normalizeForWeb(toPath);
if (process.platform === "win32") {
normalizedPath = normalizedPath.toLowerCase();
}
}
const result2 = {
relativePath: normalizedPath,
isValid: true,
normalizedPath,
metadata: {
fromPath,
toPath,
basePath: mergedOptions.basePath,
platformSeparators: path.sep,
webPath: normalizedPath,
depth: this.calculatePathDepth(normalizedPath)
}
};
this.cacheResult(cacheKey, result2);
return result2;
}
let normalizedFromPath = this.normalizePlatformPath(fromPath);
let normalizedToPath = this.normalizePlatformPath(toPath);
if (mergedOptions.basePath) {
if (!path.isAbsolute(normalizedFromPath)) {
normalizedFromPath = path.resolve(mergedOptions.basePath, normalizedFromPath);
}
if (!path.isAbsolute(normalizedToPath)) {
normalizedToPath = path.resolve(mergedOptions.basePath, normalizedToPath);
}
}
const fromDir = path.dirname(normalizedFromPath);
const relativePath = path.relative(fromDir, normalizedToPath);
const webPath = mergedOptions.normalizeForWeb ? this.normalizeForWeb(relativePath) : relativePath;
if (mergedOptions.enableSecurity) {
this.performSecurityCheck(webPath, fromPath, toPath);
}
const result = {
relativePath: webPath,
isValid: true,
normalizedPath: webPath,
metadata: {
fromPath: normalizedFromPath,
toPath: normalizedToPath,
basePath: mergedOptions.basePath,
platformSeparators: path.sep,
webPath,
depth: this.calculatePathDepth(webPath)
}
};
this.cacheResult(cacheKey, result);
return result;
} catch (error) {
throw new PathUtilsError(
`Failed to calculate relative path: ${error instanceof Error ? error.message : String(error)}`,
"CALCULATION_ERROR",
error instanceof Error ? error : void 0
);
}
}
/**
* Validate a path for security and correctness
*/
validatePath(inputPath, context = "path") {
const cacheKey = `validate::${inputPath}::${context}`;
if (this.validationCache.has(cacheKey)) {
return this.validationCache.get(cacheKey);
}
const result = {
isValid: true,
normalizedPath: inputPath,
errors: [],
warnings: [],
security: {
hasTraversal: false,
isAbsolute: false,
depth: 0
}
};
try {
if (!inputPath || typeof inputPath !== "string") {
result.isValid = false;
result.errors.push(`${context} must be a non-empty string`);
return result;
}
const trimmedPath = inputPath.trim();
if (trimmedPath !== inputPath) {
result.warnings.push(`${context} has leading/trailing whitespace`);
}
if (this.options.enableSecurity) {
if (trimmedPath.includes("..")) {
result.security.hasTraversal = true;
result.warnings.push(`${context} contains path traversal sequences`);
}
if (trimmedPath.includes("\0")) {
result.isValid = false;
result.errors.push(`${context} contains null bytes (security risk)`);
return result;
}
const suspiciousPatterns = [
/\.(\.)+/,
// Multiple dots
/[<>:"|?*]/,
// Invalid Windows characters
/^\s*$/
// Whitespace only
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(trimmedPath)) {
result.warnings.push(`${context} contains potentially problematic characters`);
break;
}
}
}
result.normalizedPath = this.normalizePath(trimmedPath, true);
result.security.isAbsolute = path.isAbsolute(trimmedPath);
result.security.depth = this.calculatePathDepth(result.normalizedPath);
if (result.security.depth > this.options.maxDepth) {
result.isValid = false;
result.errors.push(`${context} exceeds maximum depth of ${this.options.maxDepth}`);
}
this.cacheValidationResult(cacheKey, result);
return result;
} catch (error) {
result.isValid = false;
result.errors.push(
`Validation failed: ${error instanceof Error ? error.message : String(error)}`
);
return result;
}
}
/**
* Normalize path for comparison and consistency
*/
normalizePath(inputPath, forWeb = false) {
if (!inputPath) return "";
let normalized = inputPath;
if (forWeb) {
normalized = normalized.toLowerCase();
} else if (process.platform === "win32") {
normalized = normalized.toLowerCase();
}
if (forWeb) {
normalized = normalized.replace(/\\/g, "/");
} else {
normalized = path.normalize(normalized);
if (process.platform === "win32" && inputPath.includes("/") && !inputPath.includes("\\")) {
normalized = normalized.replace(/\\/g, "/");
}
}
normalized = normalized.replace(/^\.\//, "");
normalized = normalized.replace(/^\.\\/, "");
if (normalized === "/" || normalized === "\\") {
return "/";
}
if (forWeb && normalized.startsWith("/") && normalized.length > 1) {
normalized = normalized.substring(1);
}
if (forWeb && normalized.startsWith("\\") && normalized.length > 1) {
normalized = normalized.substring(1);
}
const separator = forWeb ? "/" : normalized.includes("/") ? "/" : path.sep;
if (separator === "/") {
normalized = normalized.replace(/\/+/g, "/");
} else {
normalized = normalized.replace(/\\+/g, "\\");
}
if (normalized.length > 1) {
normalized = normalized.replace(/\/$/, "");
normalized = normalized.replace(/\\$/, "");
}
return normalized;
}
/**
* Normalize path for web use (forward slashes only)
*/
normalizeForWeb(inputPath) {
return inputPath.replace(/\\/g, "/");
}
/**
* Normalize path using platform-specific separators
*/
normalizePlatformPath(inputPath) {
return inputPath.replace(/[/\\]/g, path.sep);
}
/**
* Calculate the depth of a path (number of directory levels)
*/
calculatePathDepth(inputPath) {
if (!inputPath || inputPath === "." || inputPath === "/") return 0;
const normalizedPath = this.normalizePath(inputPath, true);
const segments = normalizedPath.split("/").filter((segment) => segment && segment !== ".");
return segments.length;
}
/**
* Perform security checks on calculated paths
*/
performSecurityCheck(calculatedPath, _fromPath, _toPath) {
if (calculatedPath.includes("..")) {
const depth = (calculatedPath.match(/\.\./g) || []).length;
if (depth > 10) {
throw new PathSecurityError(
`Excessive path traversal detected (${depth} levels up)`,
calculatedPath
);
}
}
if (this.options.useRelativePaths && path.isAbsolute(calculatedPath)) {
throw new PathSecurityError(
"Unexpected absolute path in relative calculation result",
calculatedPath
);
}
}
/**
* Cache management
*/
cacheResult(key, result) {
if (this.pathCache.size >= this.maxCacheSize) {
const firstKey = this.pathCache.keys().next().value;
if (firstKey) this.pathCache.delete(firstKey);
}
this.pathCache.set(key, result);
}
cacheValidationResult(key, result) {
if (this.validationCache.size >= this.maxCacheSize) {
const firstKey = this.validationCache.keys().next().value;
if (firstKey) this.validationCache.delete(firstKey);
}
this.validationCache.set(key, result);
}
/**
* Clear all caches
*/
clearCache() {
this.pathCache.clear();
this.validationCache.clear();
}
/**
* Get cache statistics
*/
getCacheStats() {
return {
paths: this.pathCache.size,
validations: this.validationCache.size,
maxSize: this.maxCacheSize
};
}
};
}
});
// src/processors/htmlExtractor.ts
import * as cheerio2 from "cheerio";
import * as fs3 from "fs/promises";
import { z as z12 } from "zod";
function createHtmlExtractor(options = {}) {
return new HtmlExtractor(options);
}
async function extractClassesFromHtml(html, options = {}) {
const extractor = new HtmlExtractor(options);
return extractor.extractFromString(html);
}
async function extractClassesFromFile(filePath, options = {}) {
const extractor = new HtmlExtractor(options);
return extractor.extractFromFile(filePath);
}
var HtmlExtractionOptionsSchema, HtmlParsingError, FileReadError, HtmlExtractor;
var init_htmlExtractor = __esm({
"src/processors/htmlExtractor.ts"() {
"use strict";
HtmlExtractionOptionsSchema = z12.object({
preserveWhitespace: z12.boolean().default(false),
caseSensitive: z12.boolean().default(true),
ignoreEmpty: z12.boolean().default(true),
maxFileSize: z12.number().min(1).default(10 * 1024 * 1024),
// 10MB
timeout: z12.number().min(1).default(5e3)
// 5 seconds
});
HtmlParsingError = class extends Error {
constructor(message, source, cause) {
super(message);
this.name = "HtmlParsingError";
this.source = source;
this.cause = cause;
}
};
FileReadError = class extends Error {
constructor(message, filePath, cause) {
super(message);
this.name = "FileReadError";
this.filePath = filePath;
this.cause = cause;
}
};
HtmlExtractor = class {
constructor(options = {}) {
this.options = HtmlExtractionOptionsSchema.parse(options);
}
/**
* Extract classes from HTML string
*/
async extractFromString(html, source = "string") {
const startTime = Date.now();
const metadata = {
source,
processedAt: /* @__PURE__ */ new Date(),
processingTime: 0,
errors: []
};
try {
const $ = cheerio2.load(html, {
xml: {
xmlMode: false,
decodeEntities: true,
withStartIndices: false,
withEndIndices: false
}
});
const classes = /* @__PURE__ */ new Map();
let totalElements = 0;
let totalClasses = 0;
$("[class]").each((index, element) => {
totalElements++;
const $element = $(element);
const classAttr = $element.attr("class");
if (!classAttr) return;
const elementClasses = this.parseClassAttribute(classAttr);
totalClasses += elementClasses.length;
const tagName = element.tagName?.toLowerCase() || "unknown";
const attributes = element.attribs || {};
const depth = this.calculateDepth($element);
elementClasses.forEach((className) => {
if (!this.options.caseSensitive) {
className = className.toLowerCase();
}
if (!classes.has(className)) {
classes.set(className, {
name: className,
frequency: 0,
contexts: []
});
}
const classData = classes.get(className);
classData.frequency++;
if (classData.contexts.length < 10) {
classData.contexts.push({
tagName,
attributes: this.sanitizeAttributes(attributes),
depth
});
}
});
});
metadata.processingTime = Date.now() - startTime;
return {
classes,
totalElements,
totalClasses,
uniqueClasses: classes.size,
metadata
};
} catch (error) {
metadata.errors.push(error instanceof Error ? error.message : String(error));
metadata.processingTime = Date.now() - startTime;
throw new HtmlParsingError(
`Failed to parse HTML: ${error instanceof Error ? error.message : String(error)}`,
source,
error instanceof Error ? error : void 0
);
}
}
/**
* Extract classes from HTML file
*/
async extractFromFile(filePath) {
try {
const stats = await fs3.stat(filePath);
if (stats.size > this.options.maxFileSize) {
throw new FileReadError(
`File size (${stats.size} bytes) exceeds maximum allowed size (${this.options.maxFileSize} bytes)`,
filePath
);
}
const html = await this.readFileWithTimeout(filePath, this.options.timeout);
const result = await this.extractFromString(html, filePath);
result.metadata.fileSize = stats.size;
return result;
} catch (error) {
if (error instanceof HtmlParsingError || error instanceof FileReadError) {
throw error;
}
throw new FileReadError(
`Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
filePath,
error instanceof Error ? error : void 0
);
}
}
/**
* Extract classes from multiple HTML files
*/
async extractFromFiles(filePaths) {
const results = [];
for (const filePath of filePaths) {
try {
const result = await this.extractFromFile(filePath);
results.push(result);
} catch (error) {
results.push({
classes: /* @__PURE__ */ new Map(),
totalElements: 0,
totalClasses: 0,
uniqueClasses: 0,
metadata: {
source: filePath,
processedAt: /* @__PURE__ */ new Date(),
processingTime: 0,
errors: [error instanceof Error ? error.message : String(error)]
}
});
}
}
return results;
}
/**
* Parse class attribute string into individual class names
*/
parseClassAttribute(classAttr) {
if (!classAttr || !this.options.preserveWhitespace && !classAttr.trim()) {
return [];
}
const classes = classAttr.split(/\s+/).map((cls) => this.options.preserveWhitespace ? cls : cls.trim()).filter((cls) => this.options.ignoreEmpty ? cls.length > 0 : true);
return classes;
}
/**
* Calculate the depth of an element in the DOM tree
*/
calculateDepth($element) {
let depth = 0;
let current = $element.parent();
while (current.length > 0 && current.prop("tagName") !== "HTML") {
depth++;
current = current.parent();
}
return depth;
}
/**
* Sanitize element attributes to avoid sensitive data exposure
*/
sanitizeAttributes(attributes) {
const sanitized = {};
const allowedAttributes = ["id", "class", "data-", "aria-"];
Object.entries(attributes).forEach(([key, value]) => {
if (allowedAttributes.some((allowed) => key.startsWith(allowed))) {
sanitized[key] = value.length > 100 ? value.substring(0, 100) + "..." : value;
}
});
return sanitized;
}
/**
* Read file with timeout protection
*/
async readFileWithTimeout(filePath, timeout) {
return new Promise((resolve9, reject) => {
const timer = setTimeout(() => {
reject(new Error(`File read timeout after ${timeout}ms`));
}, timeout);
fs3.readFile(filePath, "utf8").then((content) => {
clearTimeout(timer);
resolve9(content);
}).catch((error) => {
clearTimeout(timer);
reject(error);
});
});
}
};
}
});
// src/processors/jsExtractor.ts
import * as fs5 from "fs/promises";
import { z as z14 } from "zod";
function createJsExtractor(options = {}) {
return new JsExtractor(options);
}
async function extractClassesFromJs(code, options = {}) {
const extractor = createJsExtractor(options);
return extractor.extractFromString(code);
}
async function extractClassesFromJsFile(filePath, options = {}) {
const extractor = createJsExtractor(options);
return extractor.extractFromFile(filePath);
}
var JsExtractionOptionsSchema, JsParsingError, JsFileReadError, RegexPatterns, JsExtractor;
var init_jsExtractor = __esm({
"src/processors/jsExtractor.ts"() {
"use strict";
JsExtractionOptionsSchema = z14.object({
enableFrameworkDetection: z14.boolean().default(true),
includeDynamicClasses: z14.boolean().default(true),
caseSensitive: z14.boolean().default(true),
ignoreEmpty: z14.boolean().default(true),
maxFileSize: z14.number().min(1).default(10 * 1024 * 1024),
// 10MB
timeout: z14.number().min(1).default(1e4),
// 10 seconds
supportedFrameworks: z14.array(z14.string()).default(["react", "preact", "solid", "vue", "angular"])
});
JsParsingError = class extends Error {
constructor(message, source, cause) {
super(message);
this.name = "JsParsingError";
this.source = source;
this.cause = cause;
}
};
JsFileReadError = class extends Error {
constructor(message, filePath, cause) {
super(message);
this.name = "JsFileReadError";
this.filePath = filePath;
this.cause = cause;
}
};
RegexPatterns = class {
};
// Static className/class patterns
RegexPatterns.STATIC_CLASSNAME = /(?:className|class)\s*=\s*["'`]([^"'`]*?)["'`]/g;
// Template literal patterns with simple content
RegexPatterns.TEMPLATE_SIMPLE = /(?:className|class)\s*=\s*\{`([^`]*?)`\}/g;
// Dynamic expression patterns (basic)
RegexPatterns.DYNAMIC_EXPRESSION = /(?:className|class)\s*=\s*\{([^}]*?)\}/g;
// Utility function patterns (clsx, classnames, cn)
RegexPatterns.UTILITY_FUNCTIONS = /(?:clsx|classnames|cn)\s*\(([^)]*)\)/g;
// JavaScript variable assignments with quoted strings (for extracting class strings from variables)
RegexPatterns.JS_STRING_LITERALS = /(?:const|let|var)\s+\w+\s*=\s*["'`]([^"'`]*?)["'`]/g;
// Object property values with quoted strings (for extracting classes from object literals)
RegexPatterns.OBJECT_PROPERTY_STRINGS = /\w+\s*:\s*["'`]([^"'`]*?)["'`]/g;
// Framework detection patterns
RegexPatterns.REACT_IMPORT = /(?:import.*?from\s+['"]react['"]|import\s+React|from\s+['"]react['"])/;
RegexPatterns.PREACT_IMPORT = /(?:import.*?from\s+['"]preact['"]|from\s+['"]preact['"])/;
RegexPatterns.SOLID_IMPORT = /(?:import.*?from\s+['"]solid-js['"]|from\s+['"]solid-js['"])/;
RegexPatterns.VUE_IMPORT = /(?:import.*?from\s+['"]vue['"]|from\s+['"]vue['"])/;
RegexPatterns.ANGULAR_IMPORT = /(?:import.*?from\s+['"]@angular|from\s+['"]@angular)/;
// JSX syntax detection
RegexPatterns.JSX_SYNTAX = /<[A-Z][A-Za-z0-9]*|<[a-z][a-zA-Z0-9-]*(?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:"[^"]*"|'[^']*'|{[^}]*}))?)*\s*\/?>/;
JsExtractor = class {
constructor(options = {}) {
this.options = JsExtractionOptionsSchema.parse(options);
}
/**
* Extract classes from JavaScript/JSX string
*/
async extractFromString(code, source = "string") {
const startTime = Date.now();
const metadata = {
source,
processedAt: /* @__PURE__ */ new Date(),
processingTime: 0,
errors: [],
extractionStats: {
staticMatches: 0,
dynamicMatches: 0,
templateMatches: 0,
utilityMatches: 0
}
};
try {
const framework = this.options.enableFrameworkDetection ? this.detectFramework(code) : "unknown";
const classes = /* @__PURE__ */ new Map();
let totalMatches = 0;
let totalClasses = 0;
const staticMatches = this.extractStaticClasses(code);
this.processMatches(staticMatches, "static", classes, framework);
const jsStringMatches = this.extractJsStringLiterals(code);
this.processMatches(jsStringMatches, "static", classes, framework);
const objectPropertyMatches = this.extractObjectPropertyStrings(code);
this.processMatches(objectPropertyMatches, "static", classes, framework);
const totalStaticMatches = staticMatches.length + jsStringMatches.length + objectPropertyMatches.length;
totalMatches += totalStaticMatches;
metadata.extractionStats.staticMatches = totalStaticMatches;
const templateMatches = this.extractTemplateClasses(code);
this.processMatches(templateMatches, "template", classes, framework);
totalMatches += templateMatches.length;
metadata.extractionStats.templateMatches = templateMatches.length;
const utilityMatches = this.extractUtilityClasses(code);
this.processMatches(utilityMatches, "utility", classes, framework);
totalMatches += utilityMatches.length;
metadata.extractionStats.utilityMatches = utilityMatches.length;
if (this.options.includeDynamicClasses) {
const dynamicMatches = this.extractDynamicClasses(code);
this.processMatches(dynamicMatches, "dynamic", classes, framework);
totalMatches += dynamicMatches.length;
metadata.extractionStats.dynamicMatches = dynamicMatches.length;
}
classes.forEach((classData) => {
totalClasses += classData.frequency;
});
metadata.processingTime = Date.now() - startTime;
return {
classes,
totalMatches,
totalClasses,
uniqueClasses: classes.size,
framework,
metadata
};
} catch (error) {
metadata.errors.push(error instanceof Error ? error.message : String(error));
metadata.processingTime = Date.now() - startTime;
throw new JsParsingError(
`Failed to parse JavaScript/JSX: ${error instanceof Error ? error.message : String(error)}`,
source,
error instanceof Error ? error : void 0
);
}
}
/**
* Extract classes from JavaScript/JSX file
*/
async extractFromFile(filePath) {
try {
const stats = await fs5.stat(filePath);
if (stats.size > this.options.maxFileSize) {
throw new JsFileReadError(
`File size (${stats.size} bytes) exceeds maximum allowed size (${this.options.maxFileSize} bytes)`,
filePath
);
}
const code = await this.readFileWithTimeout(filePath, this.options.timeout);
const result = await this.extractFromString(code, filePath);
result.metadata.fileSize = stats.size;
return result;
} catch (error) {
if (error instanceof JsParsingError || error instanceof JsFileReadError) {
throw error;
}
throw new JsFileReadError(
`Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
filePath,
error instanceof Error ? error : void 0
);
}
}
/**
* Extract classes from multiple JavaScript/JSX files
*/
async extractFromFiles(filePaths) {
const results = [];
for (const filePath of filePaths) {
try {
const result = await this.extractFromFile(filePath);
results.push(result);
} catch (error) {
results.push({
classes: /* @__PURE__ */ new Map(),
totalMatches: 0,
totalClasses: 0,
uniqueClasses: 0,
framework: "unknown",
metadata: {
source: filePath,
processedAt: /* @__PURE__ */ new Date(),