image-asset-manager
Version:
A comprehensive image asset management tool for frontend projects
314 lines • 11.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeScanner = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const types_1 = require("../types");
class CodeScanner {
/**
* Scan a directory for source code files and extract image references
*/
async scanDirectory(projectPath, options) {
const references = [];
const scannedFiles = [];
const errors = [];
try {
await this.scanDirectoryRecursive(projectPath, projectPath, references, scannedFiles, errors, options, 0);
return {
references,
scannedFiles,
errors,
};
}
catch (error) {
throw new types_1.ImageAssetError(types_1.ErrorCode.FILE_NOT_FOUND, `Failed to scan directory for code references: ${projectPath}`, error, true);
}
}
/**
* Recursively scan directories for source code files
*/
async scanDirectoryRecursive(currentPath, basePath, references, scannedFiles, errors, options, currentDepth) {
// Check depth limit
if (options.maxDepth > 0 && currentDepth >= options.maxDepth) {
return;
}
try {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
const relativePath = path.relative(basePath, fullPath);
// Check if should be excluded
if (this.shouldExclude(relativePath, options.excludePatterns)) {
continue;
}
if (entry.isDirectory() && options.recursive) {
await this.scanDirectoryRecursive(fullPath, basePath, references, scannedFiles, errors, options, currentDepth + 1);
}
else if (entry.isFile() && this.isSourceCodeFile(fullPath)) {
try {
const fileReferences = await this.scanFile(fullPath, basePath);
references.push(...fileReferences);
scannedFiles.push(relativePath);
}
catch (error) {
errors.push({
file: relativePath,
error: error instanceof Error ? error.message : String(error),
});
}
}
}
}
catch (error) {
throw new types_1.ImageAssetError(types_1.ErrorCode.PERMISSION_DENIED, `Permission denied or directory not found: ${currentPath}`, error, true);
}
}
/**
* Scan a single source code file for image references
*/
async scanFile(filePath, basePath) {
try {
const content = await fs.readFile(filePath, "utf-8");
const relativePath = path.relative(basePath, filePath);
return this.extractReferences(content, relativePath);
}
catch (error) {
throw new types_1.ImageAssetError(types_1.ErrorCode.FILE_NOT_FOUND, `Failed to read file: ${filePath}`, error, true);
}
}
/**
* Extract image references from file content
*/
extractReferences(content, filePath) {
const references = [];
const lines = content.split("\n");
for (const patternConfig of CodeScanner.REFERENCE_PATTERNS) {
let match;
patternConfig.pattern.lastIndex = 0; // Reset regex state
while ((match = patternConfig.pattern.exec(content)) !== null) {
const imagePath = match[1];
const matchIndex = match.index;
// Find line number
const lineNumber = this.getLineNumber(content, matchIndex);
// Get context (the line containing the match)
const context = lines[lineNumber - 1]?.trim() || "";
references.push({
filePath,
lineNumber,
context,
importType: patternConfig.type,
});
}
}
return references;
}
/**
* Get line number for a given character index in content
*/
getLineNumber(content, index) {
const beforeMatch = content.substring(0, index);
return beforeMatch.split("\n").length;
}
/**
* Check if a file is a source code file we should scan
*/
isSourceCodeFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
return CodeScanner.SUPPORTED_EXTENSIONS.includes(ext);
}
/**
* Check if a path should be excluded from scanning
*/
shouldExclude(filePath, excludePatterns) {
const normalizedPath = filePath.replace(/\\/g, "/");
return excludePatterns.some((pattern) => {
// Simple pattern matching - can be enhanced with glob patterns later
if (pattern.includes("*")) {
const regex = new RegExp(pattern.replace(/\*/g, ".*"));
return regex.test(normalizedPath);
}
return normalizedPath.includes(pattern);
});
}
/**
* Resolve relative image paths to absolute paths within the project
*/
resolveImagePath(referencePath, sourceFilePath, projectPath) {
// If it's already an absolute path within the project, return as-is
if (path.isAbsolute(referencePath)) {
return referencePath;
}
// Resolve relative to the source file's directory
const sourceDir = path.dirname(path.join(projectPath, sourceFilePath));
const resolvedPath = path.resolve(sourceDir, referencePath);
// Return path relative to project root
return path.relative(projectPath, resolvedPath);
}
/**
* Filter references to only include existing image files
*/
async filterValidReferences(references, imageFiles, projectPath) {
const imagePathSet = new Set(imageFiles.map((file) => file.relativePath.replace(/\\/g, "/")));
const validReferences = [];
for (const ref of references) {
// Extract the image path from the reference context
const imagePath = this.extractImagePathFromReference(ref);
if (!imagePath)
continue;
// Resolve the path relative to the project
const resolvedPath = this.resolveImagePath(imagePath, ref.filePath, projectPath).replace(/\\/g, "/");
// Check if this image file exists in our scanned images
if (imagePathSet.has(resolvedPath)) {
validReferences.push(ref);
}
}
return validReferences;
}
/**
* Extract the actual image path from a reference context
*/
extractImagePathFromReference(reference) {
const context = reference.context;
// Try each pattern to extract the path
for (const patternConfig of CodeScanner.REFERENCE_PATTERNS) {
patternConfig.pattern.lastIndex = 0;
const match = patternConfig.pattern.exec(context);
if (match) {
return match[1];
}
}
return null;
}
/**
* Get default scan options
*/
static getDefaultOptions() {
return {
extensions: CodeScanner.SUPPORTED_EXTENSIONS,
excludePatterns: [
"node_modules",
".git",
"dist",
"build",
".next",
".nuxt",
"coverage",
".nyc_output",
"*.min.js",
"*.bundle.js",
],
recursive: true,
maxDepth: 0, // No limit
};
}
}
exports.CodeScanner = CodeScanner;
// Supported source code file extensions
CodeScanner.SUPPORTED_EXTENSIONS = [
".js",
".jsx",
".ts",
".tsx",
".vue",
".html",
".htm",
".css",
".scss",
".sass",
".less",
];
// Image file extensions to look for in references
CodeScanner.IMAGE_EXTENSIONS = [
".svg",
".png",
".jpg",
".jpeg",
".gif",
".webp",
".ico",
".bmp",
];
// Regular expressions for different import/reference patterns
CodeScanner.REFERENCE_PATTERNS = [
// ES6 imports: import icon from './icon.svg'
{
pattern: /import\s+(?:\w+\s+from\s+)?['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]/gi,
type: "import",
},
// CommonJS require: require('./icon.svg')
{
pattern: /require\s*\(\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]\s*\)/gi,
type: "require",
},
// Dynamic imports: import('./icon.svg')
{
pattern: /import\s*\(\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]\s*\)/gi,
type: "import",
},
// CSS url(): url('./icon.svg')
{
pattern: /url\s*\(\s*['"`]?([^'"`\)]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]?\s*\)/gi,
type: "url",
},
// HTML src attributes: <img src="./icon.svg">
{
pattern: /(?:src|href)\s*=\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]/gi,
type: "inline",
},
// JSX/TSX image imports in src: <img src={require('./icon.svg')} />
{
pattern: /src\s*=\s*\{\s*require\s*\(\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]\s*\)\s*\}/gi,
type: "require",
},
// JSX/TSX image imports: <img src={icon} /> where icon is imported
// This is handled by tracking import statements separately
// Vue template references: <img :src="require('./icon.svg')" />
{
pattern: /:src\s*=\s*['"`][^'"`]*require\s*\(\s*['"`]([^'"`]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]\s*\)[^'"`]*['"`]/gi,
type: "require",
},
// CSS @import: @import url('./icon.svg')
{
pattern: /@import\s+url\s*\(\s*['"`]?([^'"`\)]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]?\s*\)/gi,
type: "url",
},
// Background-image in CSS: background-image: url('./icon.svg')
{
pattern: /background(?:-image)?\s*:\s*url\s*\(\s*['"`]?([^'"`\)]+\.(?:svg|png|jpe?g|gif|webp|ico|bmp))['"`]?\s*\)/gi,
type: "url",
},
];
//# sourceMappingURL=CodeScanner.js.map