@pz4l/tinyimg-core 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +688 -0
- package/README.zh-CN.md +688 -0
- package/dist/index.d.mts +516 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +851 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/utils/logger.ts","../src/cache/buffer-storage.ts","../src/cache/hash.ts","../src/cache/paths.ts","../src/cache/stats.ts","../src/cache/storage.ts","../src/compress/retry.ts","../src/compress/web-compressor.ts","../src/compress/api-compressor.ts","../src/errors/types.ts","../src/compress/compose.ts","../src/compress/concurrency.ts","../src/config/storage.ts","../src/config/loader.ts","../src/keys/masker.ts","../src/keys/quota.ts","../src/keys/validator.ts","../src/keys/selector.ts","../src/keys/pool.ts","../src/compress/service.ts"],"sourcesContent":["export function logWarning(message: string): void {\n console.warn(`⚠ ${message}`)\n}\n\nexport function logInfo(message: string): void {\n console.log(`ℹ ${message}`)\n}\n","import type { Buffer } from 'node:buffer'\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { logInfo } from '../utils/logger'\n\n/**\n * Cache storage for reading and writing compressed image data by hash.\n *\n * Uses atomic writes (temp file + rename) for concurrent safety.\n * Handles corruption gracefully by returning null on read failures.\n */\nexport class BufferCacheStorage {\n constructor(private readonly cacheDir: string) {}\n\n /**\n * Ensure cache directory exists.\n */\n private async ensureDir(): Promise<void> {\n await mkdir(this.cacheDir, { recursive: true, mode: 0o755 })\n }\n\n /**\n * Get the cache file path for an image hash.\n *\n * @param hash - MD5 hash of the image buffer\n * @returns Path to cache file (MD5 hash as filename, no extension)\n */\n getCachePath(hash: string): string {\n return join(this.cacheDir, hash)\n }\n\n /**\n * Read cached compressed image data by hash.\n *\n * @param hash - MD5 hash of the image buffer\n * @returns Cached Buffer or null if not found/corrupted\n */\n async read(hash: string): Promise<Buffer | null> {\n try {\n const cachePath = this.getCachePath(hash)\n const data = await readFile(cachePath)\n return data\n }\n catch {\n // Silent failure on cache miss or corruption\n return null\n }\n }\n\n /**\n * Write compressed image data to cache by hash.\n *\n * Uses atomic write pattern: temp file + rename.\n *\n * @param hash - MD5 hash of the image buffer\n * @param data - Compressed image data to cache\n */\n async write(hash: string, data: Buffer): Promise<void> {\n await this.ensureDir()\n\n const cachePath = this.getCachePath(hash)\n const tmpPath = `${cachePath}.tmp`\n\n // Atomic write: temp file + rename\n await writeFile(tmpPath, data)\n await rename(tmpPath, cachePath)\n }\n}\n\n/**\n * Read cached image data from multiple cache directories in priority order.\n *\n * @param hash - MD5 hash of the image buffer\n * @param cacheDirs - Array of cache directories (priority order)\n * @returns First successful Buffer read or null if all miss\n */\nexport async function readCacheByHash(\n hash: string,\n cacheDirs: string[],\n): Promise<Buffer | null> {\n for (const cacheDir of cacheDirs) {\n const storage = new BufferCacheStorage(cacheDir)\n const data = await storage.read(hash)\n if (data !== null) {\n const prefix = hash.substring(0, 8)\n logInfo(`ℹ Cache hit: ${prefix}`)\n return data\n }\n }\n\n return null\n}\n\n/**\n * Write compressed image data to cache by hash.\n *\n * @param hash - MD5 hash of the image buffer\n * @param data - Compressed image data to cache\n * @param cacheDir - Cache directory to write to\n */\nexport async function writeCacheByHash(\n hash: string,\n data: Buffer,\n cacheDir: string,\n): Promise<void> {\n const storage = new BufferCacheStorage(cacheDir)\n await storage.write(hash, data)\n\n const prefix = hash.substring(0, 8)\n logInfo(`ℹ Cached: ${prefix}`)\n}\n","import type { Buffer } from 'node:buffer'\nimport { createHash } from 'node:crypto'\nimport { readFile } from 'node:fs/promises'\n\n/**\n * Calculate MD5 hash of a file's content.\n *\n * @param filePath - Absolute path to the file\n * @returns MD5 hash as a 32-character hexadecimal string\n *\n * @example\n * ```ts\n * const hash = await calculateMD5('/path/to/image.png')\n * console.log(hash) // 'a1b2c3d4e5f6...'\n * ```\n */\nexport async function calculateMD5(filePath: string): Promise<string> {\n const content = await readFile(filePath)\n const hash = createHash('md5')\n hash.update(content)\n return hash.digest('hex')\n}\n\n/**\n * Calculate MD5 hash of a buffer's content.\n *\n * @param buffer - Buffer to hash\n * @returns MD5 hash as a 32-character hexadecimal string\n *\n * @example\n * ```ts\n * const hash = await calculateMD5FromBuffer(buffer)\n * console.log(hash) // 'a1b2c3d4e5f6...'\n * ```\n */\nexport async function calculateMD5FromBuffer(buffer: Buffer): Promise<string> {\n const hash = createHash('md5')\n hash.update(buffer)\n return hash.digest('hex')\n}\n","import { homedir } from 'node:os'\nimport { join } from 'node:path'\n\n/**\n * Get the project-level cache directory path.\n *\n * @param projectRoot - Absolute path to the project root directory\n * @returns Path to project cache directory: `node_modules/.tinyimg_cache/`\n *\n * @example\n * ```ts\n * const cachePath = getProjectCachePath('/Users/test/project')\n * // Returns: '/Users/test/project/node_modules/.tinyimg_cache'\n * ```\n */\nexport function getProjectCachePath(projectRoot: string): string {\n return join(projectRoot, 'node_modules', '.tinyimg_cache')\n}\n\n/**\n * Get the global cache directory path.\n *\n * @returns Path to global cache directory: `~/.tinyimg/cache/`\n *\n * @example\n * ```ts\n * const cachePath = getGlobalCachePath()\n * // Returns: '/Users/username/.tinyimg/cache'\n * ```\n */\nexport function getGlobalCachePath(): string {\n return join(homedir(), '.tinyimg', 'cache')\n}\n","import { readdir, stat } from 'node:fs/promises'\nimport { getGlobalCachePath, getProjectCachePath } from './paths'\n\n/**\n * Cache statistics interface.\n */\nexport interface CacheStats {\n count: number\n size: number\n}\n\n/**\n * Format bytes to human-readable format.\n *\n * @param bytes - Number of bytes\n * @returns Formatted string (e.g., \"1.23 MB\", \"456 KB\")\n *\n * @example\n * ```ts\n * formatBytes(0) // \"0 B\"\n * formatBytes(512) // \"512 B\"\n * formatBytes(1024) // \"1.00 KB\"\n * formatBytes(1024 * 1024) // \"1.00 MB\"\n * formatBytes(1024 * 1024 * 1024) // \"1.00 GB\"\n * ```\n */\nexport function formatBytes(bytes: number): string {\n if (bytes === 0) {\n return '0 B'\n }\n\n if (bytes < 1024) {\n return `${bytes} B`\n }\n\n if (bytes < 1024 * 1024) {\n return `${(bytes / 1024).toFixed(2)} KB`\n }\n\n if (bytes < 1024 * 1024 * 1024) {\n return `${(bytes / (1024 * 1024)).toFixed(2)} MB`\n }\n\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`\n}\n\n/**\n * Get cache statistics for a directory.\n *\n * @param cacheDir - Cache directory path\n * @returns Cache statistics (count and size)\n *\n * @example\n * ```ts\n * const stats = await getCacheStats('/path/to/cache')\n * console.log(`Files: ${stats.count}, Size: ${formatBytes(stats.size)}`)\n * ```\n */\nexport async function getCacheStats(cacheDir: string): Promise<CacheStats> {\n try {\n const files = await readdir(cacheDir)\n\n let count = 0\n let size = 0\n\n for (const file of files) {\n const filePath = `${cacheDir}/${file}`\n const stats = await stat(filePath)\n\n if (stats.isFile()) {\n count++\n size += stats.size\n }\n }\n\n return { count, size }\n }\n catch {\n // Directory doesn't exist or is not accessible\n return { count: 0, size: 0 }\n }\n}\n\n/**\n * Get cache statistics for both project and global cache.\n *\n * @param projectRoot - Optional project root directory\n * @returns Object with project and global cache statistics\n *\n * @example\n * ```ts\n * // Get both project and global stats\n * const stats = await getAllCacheStats('/project/path')\n * console.log(`Project: ${stats.project?.count}, Global: ${stats.global.count}`)\n *\n * // Get only global stats\n * const globalOnly = await getAllCacheStats()\n * console.log(`Global: ${globalOnly.global.count}`)\n * ```\n */\nexport async function getAllCacheStats(projectRoot?: string): Promise<{\n project: CacheStats | null\n global: CacheStats\n}> {\n const global = await getCacheStats(getGlobalCachePath())\n\n let project: CacheStats | null = null\n if (projectRoot) {\n project = await getCacheStats(getProjectCachePath(projectRoot))\n }\n\n return { project, global }\n}\n","import type { Buffer } from 'node:buffer'\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { logInfo } from '../utils/logger'\nimport { calculateMD5 } from './hash'\n\n/**\n * Cache storage for reading and writing compressed image data.\n *\n * Uses atomic writes (temp file + rename) for concurrent safety.\n * Handles corruption gracefully by returning null on read failures.\n */\nexport class CacheStorage {\n constructor(private readonly cacheDir: string) {}\n\n /**\n * Ensure cache directory exists.\n */\n private async ensureDir(): Promise<void> {\n await mkdir(this.cacheDir, { recursive: true, mode: 0o755 })\n }\n\n /**\n * Get the cache file path for an image.\n *\n * @param imagePath - Absolute path to the source image\n * @returns Path to cache file (MD5 hash as filename, no extension)\n */\n async getCachePath(imagePath: string): Promise<string> {\n const md5Hash = await calculateMD5(imagePath)\n return join(this.cacheDir, md5Hash)\n }\n\n /**\n * Read cached compressed image data.\n *\n * @param imagePath - Absolute path to the source image\n * @returns Cached Buffer or null if not found/corrupted\n */\n async read(imagePath: string): Promise<Buffer | null> {\n try {\n const cachePath = await this.getCachePath(imagePath)\n const data = await readFile(cachePath)\n return data\n }\n catch {\n // Silent failure on cache miss or corruption\n return null\n }\n }\n\n /**\n * Write compressed image data to cache.\n *\n * Uses atomic write pattern: temp file + rename.\n *\n * @param imagePath - Absolute path to the source image\n * @param data - Compressed image data to cache\n */\n async write(imagePath: string, data: Buffer): Promise<void> {\n await this.ensureDir()\n\n const cachePath = await this.getCachePath(imagePath)\n const tmpPath = `${cachePath}.tmp`\n\n // Atomic write: temp file + rename\n await writeFile(tmpPath, data)\n await rename(tmpPath, cachePath)\n }\n}\n\n/**\n * Read cached image data from multiple cache directories in priority order.\n *\n * @param imagePath - Absolute path to the source image\n * @param cacheDirs - Array of cache directories (priority order)\n * @returns First successful Buffer read or null if all miss\n */\nexport async function readCache(\n imagePath: string,\n cacheDirs: string[],\n): Promise<Buffer | null> {\n for (const cacheDir of cacheDirs) {\n const storage = new CacheStorage(cacheDir)\n const data = await storage.read(imagePath)\n if (data !== null) {\n const md5Hash = await calculateMD5(imagePath)\n const prefix = md5Hash.substring(0, 8)\n logInfo(`ℹ️ cache hit: ${prefix}`)\n return data\n }\n }\n\n return null\n}\n\n/**\n * Write compressed image data to cache.\n *\n * @param imagePath - Absolute path to the source image\n * @param data - Compressed image data to cache\n * @param cacheDir - Cache directory to write to\n */\nexport async function writeCache(\n imagePath: string,\n data: Buffer,\n cacheDir: string,\n): Promise<void> {\n const storage = new CacheStorage(cacheDir)\n await storage.write(imagePath, data)\n\n const md5Hash = await calculateMD5(imagePath)\n const prefix = md5Hash.substring(0, 8)\n logInfo(`ℹ️ cache miss: ${prefix}, compressed`)\n}\n","import { logWarning } from '../utils/logger'\n\nexport class RetryManager {\n private failureCount = 0\n\n constructor(\n private maxRetries: number = 8,\n private baseDelay: number = 1000, // 1 second\n ) {}\n\n async execute<T>(operation: () => Promise<T>): Promise<T> {\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n try {\n const result = await operation()\n this.failureCount = 0 // Reset on success\n return result\n }\n catch (error) {\n this.failureCount++\n\n if (attempt === this.maxRetries || !this.shouldRetry(error)) {\n throw error\n }\n\n const delay = this.baseDelay * 2 ** attempt\n logWarning(`Retry ${attempt + 1}/${this.maxRetries} after ${delay}ms`)\n await this.sleep(delay)\n }\n }\n\n throw new Error('Max retries exceeded')\n }\n\n private shouldRetry(error: any): boolean {\n // Network errors\n if (error.code && ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'].includes(error.code)) {\n return true\n }\n\n // HTTP 5xx server errors\n if (error.statusCode && error.statusCode >= 500 && error.statusCode < 600) {\n return true\n }\n\n // Don't retry on 4xx client errors\n return false\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n }\n\n getFailureCount(): number {\n return this.failureCount\n }\n\n reset(): void {\n this.failureCount = 0\n }\n}\n","import type { ICompressor } from './types'\nimport { Buffer } from 'node:buffer'\nimport https from 'node:https'\nimport FormData from 'form-data'\nimport { logInfo } from '../utils/logger'\nimport { RetryManager } from './retry'\n\nconst TINYPNG_WEB_URL = 'https://tinypng.com/backend/opt/shrink'\n\nexport class TinyPngWebCompressor implements ICompressor {\n private retryManager: RetryManager\n\n constructor(maxRetries: number = 8) {\n this.retryManager = new RetryManager(maxRetries)\n }\n\n async compress(buffer: Buffer): Promise<Buffer> {\n return this.retryManager.execute(async () => {\n // Step 1: Upload image to get compressed URL\n const uploadUrl = await this.uploadToTinyPngWeb(buffer)\n\n // Step 2: Download compressed image\n const compressedBuffer = await this.downloadCompressedImage(uploadUrl)\n\n const originalSize = buffer.byteLength\n const compressedSize = compressedBuffer.byteLength\n const saved = ((1 - compressedSize / originalSize) * 100).toFixed(1)\n\n logInfo(`Compressed with [TinyPngWebCompressor]: ${originalSize} → ${compressedSize} (saved ${saved}%)`)\n\n return compressedBuffer\n })\n }\n\n private async uploadToTinyPngWeb(buffer: Buffer): Promise<string> {\n return new Promise((resolve, reject) => {\n const form = new FormData()\n form.append('file', buffer, { filename: 'image.png' })\n\n const req = https.request(\n TINYPNG_WEB_URL,\n {\n method: 'POST',\n headers: form.getHeaders(),\n },\n (res) => {\n let data = ''\n\n res.on('data', (chunk) => {\n data += chunk\n })\n\n res.on('end', () => {\n if (res.statusCode !== 200) {\n const error = new Error(`HTTP ${res.statusCode}: ${data}`)\n ;(error as any).statusCode = res.statusCode\n return reject(error)\n }\n\n try {\n // Response contains JSON with output URL\n const response = JSON.parse(data)\n if (!response.output?.url) {\n return reject(new Error('No output URL in response'))\n }\n resolve(response.output.url)\n }\n catch (error: any) {\n reject(new Error(`Failed to parse response: ${error.message}`))\n }\n })\n },\n )\n\n req.on('error', (error) => {\n reject(error)\n })\n\n form.pipe(req)\n })\n }\n\n private async downloadCompressedImage(url: string): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n https.get(url, (res) => {\n if (res.statusCode !== 200) {\n const error = new Error(`HTTP ${res.statusCode} downloading compressed image`)\n ;(error as any).statusCode = res.statusCode\n return reject(error)\n }\n\n const chunks: Buffer[] = []\n res.on('data', chunk => chunks.push(chunk))\n res.on('end', () => resolve(Buffer.concat(chunks)))\n res.on('error', reject)\n }).on('error', reject)\n })\n }\n\n getFailureCount(): number {\n return this.retryManager.getFailureCount()\n }\n}\n","import type { KeyPool } from '../keys/pool'\nimport type { ICompressor } from './types'\nimport { Buffer } from 'node:buffer'\nimport tinify from 'tinify'\nimport { logInfo, logWarning } from '../utils/logger'\nimport { RetryManager } from './retry'\n\n// Re-export TinyPngWebCompressor for convenience\nexport { TinyPngWebCompressor } from './web-compressor'\n\n// 5MB limit per CONTEXT.md D-09 - design decision, not tinify API limit\n// tinify API supports up to 500MB, but we limit to 5MB for quota management\nconst MAX_FILE_SIZE = 5 * 1024 * 1024\n\nexport class TinyPngApiCompressor implements ICompressor {\n private retryManager: RetryManager\n private currentKey: string | null = null\n\n constructor(\n private keyPool: KeyPool,\n maxRetries: number = 8,\n ) {\n this.retryManager = new RetryManager(maxRetries)\n }\n\n async compress(buffer: Buffer): Promise<Buffer> {\n // Check 5MB limit per CONTEXT.md D-09\n if (buffer.byteLength > MAX_FILE_SIZE) {\n logWarning(`File exceeds 5MB limit for API compressor (${(buffer.byteLength / 1024 / 1024).toFixed(2)}MB)`)\n throw new Error('File size exceeds 5MB limit')\n }\n\n return this.retryManager.execute(async () => {\n // Only set tinify.key if it's different from current\n const key = await this.keyPool.selectKey()\n if (this.currentKey !== key) {\n try {\n tinify.key = key\n this.currentKey = key\n }\n catch {\n // In test environments, tinify.key might not be writable\n // This is OK as long as fromBuffer is mocked\n }\n }\n\n const originalSize = buffer.byteLength\n const result = await tinify.fromBuffer(buffer).toBuffer()\n const compressedSize = result.byteLength\n const saved = ((1 - compressedSize / originalSize) * 100).toFixed(1)\n\n this.keyPool.decrementQuota()\n logInfo(`Compressed with [TinyPngApiCompressor]: ${originalSize} → ${compressedSize} (saved ${saved}%)`)\n\n return Buffer.from(result)\n })\n }\n\n getFailureCount(): number {\n return this.retryManager.getFailureCount()\n }\n}\n","export class AllKeysExhaustedError extends Error {\n constructor(message = 'All API keys have exhausted quota') {\n super(message)\n this.name = 'AllKeysExhaustedError'\n }\n}\n\nexport class NoValidKeysError extends Error {\n constructor(message = 'No valid API keys available') {\n super(message)\n this.name = 'NoValidKeysError'\n }\n}\n\nexport class AllCompressionFailedError extends Error {\n constructor(message = 'All compression methods failed') {\n super(message)\n this.name = 'AllCompressionFailedError'\n }\n}\n","import type { Buffer } from 'node:buffer'\nimport type { CompressionMode, CompressOptions } from './types'\nimport { AllCompressionFailedError } from '../errors/types'\nimport { logInfo, logWarning } from '../utils/logger'\n/**\n * Compress buffer with automatic fallback through multiple compressors\n *\n * @param buffer - Original image data\n * @param options - Compression options (mode, compressors, maxRetries)\n * @returns Compressed image data\n * @throws AllCompressionFailedError when all compressors fail\n *\n * @example\n * ```ts\n * try {\n * const compressed = await compressWithFallback(buffer, { mode: 'auto' })\n * } catch (error) {\n * if (error instanceof AllCompressionFailedError) {\n * // All compression methods failed\n * }\n * }\n * ```\n */\nexport async function compressWithFallback(\n buffer: Buffer,\n options: CompressOptions = {},\n): Promise<Buffer> {\n const compressors = options.compressors ?? []\n\n for (const compressor of compressors) {\n try {\n logInfo(`Attempting compression with [${compressor.constructor.name}]`)\n const result = await compressor.compress(buffer)\n return result\n }\n catch (error: any) {\n logWarning(`[${compressor.constructor.name}] failed: ${error.message}`)\n\n // If this is AllCompressionFailedError, propagate immediately\n if (error.name === 'AllCompressionFailedError') {\n throw error\n }\n\n // Otherwise, try next compressor\n continue\n }\n }\n\n throw new AllCompressionFailedError('All compression methods failed')\n}\n\n/**\n * Get default compressor types for a given mode\n * This is a helper - actual compressor instances created in service layer\n *\n * @param mode - Compression mode\n * @returns Compressor type names (not instances)\n *\n * @example\n * ```ts\n * const types = getCompressorTypesForMode('auto')\n * // Returns: ['TinyPngApiCompressor', 'TinyPngWebCompressor']\n * ```\n */\nexport function getCompressorTypesForMode(mode: CompressionMode = 'auto'): string[] {\n switch (mode) {\n case 'api':\n return ['TinyPngApiCompressor']\n case 'web':\n return ['TinyPngWebCompressor']\n case 'auto':\n default:\n return ['TinyPngApiCompressor', 'TinyPngWebCompressor']\n }\n}\n","import pLimit from 'p-limit'\n\n/**\n * Create a concurrency limiter for async operations\n *\n * @param concurrency - Max concurrent operations (default: 8)\n * @returns Limit function that wraps async operations\n *\n * @example\n * ```ts\n * const limit = createConcurrencyLimiter(2)\n * const task1 = limit(() => asyncOperation1())\n * const task2 = limit(() => asyncOperation2())\n * await Promise.all([task1, task2])\n * ```\n */\nexport function createConcurrencyLimiter(concurrency: number = 8) {\n return pLimit(concurrency)\n}\n\n/**\n * Execute tasks with concurrency control\n *\n * @param tasks - Array of async functions to execute\n * @param concurrency - Max concurrent tasks (default: 8)\n * @returns Promise resolving to array of results\n *\n * @example\n * ```ts\n * const tasks = [\n * () => compressImage(buffer1),\n * () => compressImage(buffer2),\n * () => compressImage(buffer3)\n * ]\n * const results = await executeWithConcurrency(tasks, 2)\n * // Only 2 compressions run at a time\n * ```\n */\nexport async function executeWithConcurrency<T>(\n tasks: (() => Promise<T>)[],\n concurrency: number = 8,\n): Promise<T[]> {\n const limit = createConcurrencyLimiter(concurrency)\n\n // Map each task to a limited execution\n const limitedTasks = tasks.map(task => limit(task))\n\n // Wait for all to complete\n return Promise.all(limitedTasks)\n}\n","import type { ConfigFile } from './types'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nconst CONFIG_DIR = '.tinyimg'\nconst CONFIG_FILE = 'keys.json'\n\nexport function getConfigPath(): string {\n const homeDir = os.homedir()\n return path.join(homeDir, CONFIG_DIR, CONFIG_FILE)\n}\n\nexport function ensureConfigFile(): void {\n const configPath = getConfigPath()\n const configDir = path.dirname(configPath)\n\n if (!fs.existsSync(configDir)) {\n fs.mkdirSync(configDir, { recursive: true, mode: 0o700 })\n }\n\n if (!fs.existsSync(configPath)) {\n const initialContent: ConfigFile = { keys: [] }\n fs.writeFileSync(\n configPath,\n JSON.stringify(initialContent, null, 2),\n { mode: 0o600 },\n )\n }\n}\n\nexport function readConfig(): ConfigFile {\n ensureConfigFile()\n const configPath = getConfigPath()\n const content = fs.readFileSync(configPath, 'utf-8')\n return JSON.parse(content) as ConfigFile\n}\n\nexport function writeConfig(config: ConfigFile): void {\n ensureConfigFile()\n const configPath = getConfigPath()\n fs.writeFileSync(\n configPath,\n JSON.stringify(config, null, 2),\n { mode: 0o600 },\n )\n}\n","import process from 'node:process'\nimport { readConfig } from './storage'\n\nexport interface LoadedKey {\n key: string\n valid?: boolean\n lastCheck?: string\n}\n\nexport function loadKeys(): LoadedKey[] {\n // Priority 1: Environment variable (highest priority)\n const envKeys = process.env.TINYPNG_KEYS\n if (envKeys && envKeys.trim()) {\n const keys = envKeys.split(',')\n .map((k: string) => k.trim())\n .filter((k: string) => k.length > 0)\n return keys.map((key: string) => ({ key }))\n }\n\n // Priority 2: Global config file\n try {\n const config = readConfig()\n return config.keys.map(metadata => ({\n key: metadata.key,\n valid: metadata.valid,\n lastCheck: metadata.lastCheck,\n }))\n }\n catch {\n // Config file doesn't exist or is invalid\n return []\n }\n}\n","export function maskKey(key: string): string {\n if (key.length < 8) {\n return '****'\n }\n const first = key.substring(0, 4)\n const last = key.substring(key.length - 4)\n return `${first}****${last}`\n}\n","import tinify from 'tinify'\nimport { maskKey } from './masker'\n\nconst MONTHLY_LIMIT = 500 // Free tier limit\n\nexport async function queryQuota(key: string): Promise<number> {\n try {\n tinify.key = key\n await tinify.validate() // Required to set compressionCount\n const usedThisMonth = tinify.compressionCount ?? 0\n const remaining = Math.max(0, MONTHLY_LIMIT - usedThisMonth)\n return remaining\n }\n catch (error: any) {\n // Invalid key or quota exhausted - return 0\n if (error?.message?.includes('credentials') || error?.message?.includes('Unauthorized') || error?.constructor?.name === 'AccountError') {\n return 0\n }\n throw error\n }\n}\n\nexport interface QuotaTracker {\n key: string\n remaining: number\n localCounter: number\n decrement: () => void\n isZero: () => boolean\n}\n\nexport function createQuotaTracker(key: string, remaining: number): QuotaTracker {\n const localCounter = remaining\n\n return {\n key,\n remaining,\n localCounter,\n decrement() {\n if (this.localCounter > 0) {\n this.localCounter--\n\n if (this.localCounter === 0) {\n console.warn(`⚠ Key ${maskKey(this.key)} quota exhausted, switching to next key`)\n }\n }\n },\n isZero() {\n return this.localCounter === 0\n },\n }\n}\n","import tinify from 'tinify'\nimport { maskKey } from './masker'\n\nexport async function validateKey(key: string): Promise<boolean> {\n try {\n tinify.key = key\n await tinify.validate()\n\n console.log(`✓ API key ${maskKey(key)} validated successfully`)\n return true\n }\n catch (error: any) {\n // Check if it's an AccountError (invalid credentials)\n if (error?.message?.includes('credentials') || error?.message?.includes('Unauthorized') || error?.constructor?.name === 'AccountError') {\n console.warn(`⚠ Invalid API key ${maskKey(key)} marked and skipped`)\n return false\n }\n // Re-throw network and server errors\n throw error\n }\n}\n","import { logWarning } from '../utils/logger'\nimport { maskKey } from './masker'\nimport { createQuotaTracker, queryQuota } from './quota'\nimport { validateKey } from './validator'\n\nexport interface KeySelection {\n key: string\n tracker: ReturnType<typeof createQuotaTracker>\n}\n\n// Strategy 1: Random (default)\nexport class RandomSelector {\n async select(keys: string[]): Promise<KeySelection | null> {\n const available = await this.getAvailableKeys(keys)\n if (available.length === 0)\n return null\n\n const randomIndex = Math.floor(Math.random() * available.length)\n const selected = available[randomIndex]\n return selected\n }\n\n protected async getAvailableKeys(keys: string[]): Promise<KeySelection[]> {\n const available: KeySelection[] = []\n\n for (const key of keys) {\n const isValid = await validateKey(key)\n if (!isValid)\n continue\n\n const remaining = await queryQuota(key)\n if (remaining === 0) {\n logWarning(`Key ${maskKey(key)} has no quota remaining`)\n continue\n }\n\n available.push({\n key,\n tracker: createQuotaTracker(key, remaining),\n })\n }\n\n return available\n }\n}\n\n// Strategy 2: Round-Robin\nexport class RoundRobinSelector extends RandomSelector {\n private currentIndex = 0\n\n async select(keys: string[]): Promise<KeySelection | null> {\n const available = await this.getAvailableKeys(keys)\n if (available.length === 0)\n return null\n\n const selected = available[this.currentIndex % available.length]\n this.currentIndex++\n return selected\n }\n\n reset(): void {\n this.currentIndex = 0\n }\n}\n\n// Strategy 3: Priority (use first available)\nexport class PrioritySelector extends RandomSelector {\n async select(keys: string[]): Promise<KeySelection | null> {\n const available = await this.getAvailableKeys(keys)\n if (available.length === 0)\n return null\n\n // Return first available key\n return available[0]\n }\n}\n","import type { KeySelection } from './selector'\nimport { loadKeys } from '../config/loader'\nimport { AllKeysExhaustedError, NoValidKeysError } from '../errors/types'\nimport { PrioritySelector, RandomSelector, RoundRobinSelector } from './selector'\n\nexport type KeyStrategy = 'random' | 'round-robin' | 'priority'\n\nexport class KeyPool {\n private keys: string[]\n private selector: RandomSelector | RoundRobinSelector | PrioritySelector\n private currentSelection: KeySelection | null = null\n\n constructor(strategy: KeyStrategy = 'random') {\n this.keys = loadKeys().map(k => k.key)\n\n if (this.keys.length === 0) {\n throw new NoValidKeysError('No API keys configured')\n }\n\n this.selector = this.createSelector(strategy)\n }\n\n private createSelector(strategy: KeyStrategy) {\n switch (strategy) {\n case 'random':\n return new RandomSelector()\n case 'round-robin':\n return new RoundRobinSelector()\n case 'priority':\n return new PrioritySelector()\n default:\n return new RandomSelector()\n }\n }\n\n async selectKey(): Promise<string> {\n // If current key has quota, use it\n if (this.currentSelection && !this.currentSelection.tracker.isZero()) {\n return this.currentSelection.key\n }\n\n // Need to select new key\n const selection = await this.selector.select(this.keys)\n\n if (!selection) {\n throw new AllKeysExhaustedError()\n }\n\n this.currentSelection = selection\n return selection.key\n }\n\n decrementQuota(): void {\n if (this.currentSelection) {\n this.currentSelection.tracker.decrement()\n }\n }\n\n getCurrentKey(): string | null {\n return this.currentSelection?.key ?? null\n }\n}\n","import type { Buffer } from 'node:buffer'\nimport type { CompressOptions, ICompressor } from './types'\nimport process from 'node:process'\nimport { readCacheByHash, writeCacheByHash } from '../cache/buffer-storage'\nimport { calculateMD5FromBuffer } from '../cache/hash'\nimport { getGlobalCachePath, getProjectCachePath } from '../cache/paths'\nimport { KeyPool } from '../keys/pool'\nimport { logInfo, logWarning } from '../utils/logger'\nimport { TinyPngApiCompressor, TinyPngWebCompressor } from './api-compressor'\nimport { compressWithFallback } from './compose'\nimport { createConcurrencyLimiter } from './concurrency'\n\nexport interface CompressServiceOptions extends CompressOptions {\n /**\n * Enable cache (default: true)\n */\n cache?: boolean\n\n /**\n * Use project cache only, ignore global cache (default: false)\n */\n projectCacheOnly?: boolean\n\n /**\n * Concurrency limit for batch operations (default: 8)\n */\n concurrency?: number\n\n /**\n * Optional KeyPool instance for testing or advanced usage\n * If not provided, a new KeyPool will be created with 'random' strategy\n */\n keyPool?: KeyPool\n}\n\n/**\n * Compress a single image with cache integration and fallback\n *\n * @param buffer - Original image data\n * @param options - Compression options\n * @returns Compressed image data\n */\nexport async function compressImage(\n buffer: Buffer,\n options: CompressServiceOptions = {},\n): Promise<Buffer> {\n const {\n cache = true,\n projectCacheOnly = false,\n mode = 'auto',\n maxRetries = 8,\n } = options\n\n // Step 1: Calculate MD5 for cache key\n const hash = await calculateMD5FromBuffer(buffer)\n const hashPrefix = hash.substring(0, 8)\n\n // Step 2: Check cache if enabled\n if (cache) {\n try {\n // Try project cache first\n const projectCachePath = getProjectCachePath(process.cwd())\n const cached = await readCacheByHash(hash, [projectCachePath])\n if (cached) {\n logInfo(`ℹ Cache hit: ${hashPrefix}`)\n return cached\n }\n\n // Try global cache if not project-only\n if (!projectCacheOnly) {\n const globalCachePath = getGlobalCachePath()\n const globalCached = await readCacheByHash(hash, [globalCachePath])\n if (globalCached) {\n logInfo(`ℹ Cache hit (global): ${hashPrefix}`)\n return globalCached\n }\n }\n }\n catch (error: any) {\n logWarning(`Cache read failed: ${error.message}`)\n // Continue to compression on cache errors\n }\n }\n\n // Step 3: Compress with fallback\n logInfo(`ℹ Cache miss: ${hashPrefix}, compressing...`)\n\n const compressed = await compressWithFallback(buffer, {\n mode,\n maxRetries,\n compressors: createCompressors(options),\n })\n\n // Step 4: Write to project cache if enabled\n if (cache) {\n try {\n const projectCachePath = getProjectCachePath(process.cwd())\n await writeCacheByHash(hash, compressed, projectCachePath)\n logInfo(`ℹ Cached: ${hashPrefix}`)\n }\n catch (error: any) {\n logWarning(`Cache write failed: ${error.message}`)\n // Don't fail compression on cache write errors\n }\n }\n\n return compressed\n}\n\n/**\n * Compress multiple images with concurrency control\n *\n * @param buffers - Array of image buffers\n * @param options - Compression options\n * @returns Array of compressed buffers\n */\nexport async function compressImages(\n buffers: Buffer[],\n options: CompressServiceOptions = {},\n): Promise<Buffer[]> {\n const { concurrency = 8 } = options\n const limit = createConcurrencyLimiter(concurrency)\n\n const tasks = buffers.map(buffer =>\n limit(() => compressImage(buffer, options)),\n )\n\n return Promise.all(tasks)\n}\n\n/**\n * Create compressor instances based on options\n * Factory function to inject KeyPool for API compressor\n */\nfunction createCompressors(options: CompressServiceOptions): ICompressor[] {\n const { mode = 'auto', maxRetries = 8, keyPool } = options\n const compressors: ICompressor[] = []\n\n // Create or use provided KeyPool for API compressor\n // Note: This will use keys from env var or config file (from Phase 2)\n const pool = keyPool || new KeyPool('random') // Could be made configurable\n\n if (mode === 'auto' || mode === 'api') {\n compressors.push(new TinyPngApiCompressor(pool, maxRetries))\n }\n\n if (mode === 'auto' || mode === 'web') {\n compressors.push(new TinyPngWebCompressor(maxRetries))\n }\n\n return compressors\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAgB,WAAW,SAAuB;AAChD,SAAQ,KAAK,KAAK,UAAU;;AAG9B,SAAgB,QAAQ,SAAuB;AAC7C,SAAQ,IAAI,KAAK,UAAU;;;;;;;;;;ACM7B,IAAa,qBAAb,MAAgC;CAC9B,YAAY,UAAmC;AAAlB,OAAA,WAAA;;;;;CAK7B,MAAc,YAA2B;AACvC,QAAM,MAAM,KAAK,UAAU;GAAE,WAAW;GAAM,MAAM;GAAO,CAAC;;;;;;;;CAS9D,aAAa,MAAsB;AACjC,SAAO,KAAK,KAAK,UAAU,KAAK;;;;;;;;CASlC,MAAM,KAAK,MAAsC;AAC/C,MAAI;AAGF,UADa,MAAM,SADD,KAAK,aAAa,KAAK,CACH;UAGlC;AAEJ,UAAO;;;;;;;;;;;CAYX,MAAM,MAAM,MAAc,MAA6B;AACrD,QAAM,KAAK,WAAW;EAEtB,MAAM,YAAY,KAAK,aAAa,KAAK;EACzC,MAAM,UAAU,GAAG,UAAU;AAG7B,QAAM,UAAU,SAAS,KAAK;AAC9B,QAAM,OAAO,SAAS,UAAU;;;;;;;;;;AAWpC,eAAsB,gBACpB,MACA,WACwB;AACxB,MAAK,MAAM,YAAY,WAAW;EAEhC,MAAM,OAAO,MADG,IAAI,mBAAmB,SAAS,CACrB,KAAK,KAAK;AACrC,MAAI,SAAS,MAAM;AAEjB,WAAQ,gBADO,KAAK,UAAU,GAAG,EAAE,GACF;AACjC,UAAO;;;AAIX,QAAO;;;;;;;;;AAUT,eAAsB,iBACpB,MACA,MACA,UACe;AAEf,OADgB,IAAI,mBAAmB,SAAS,CAClC,MAAM,MAAM,KAAK;AAG/B,SAAQ,aADO,KAAK,UAAU,GAAG,EAAE,GACL;;;;;;;;;;;;;;;;AC7FhC,eAAsB,aAAa,UAAmC;CACpE,MAAM,UAAU,MAAM,SAAS,SAAS;CACxC,MAAM,OAAO,WAAW,MAAM;AAC9B,MAAK,OAAO,QAAQ;AACpB,QAAO,KAAK,OAAO,MAAM;;;;;;;;;;;;;;AAe3B,eAAsB,uBAAuB,QAAiC;CAC5E,MAAM,OAAO,WAAW,MAAM;AAC9B,MAAK,OAAO,OAAO;AACnB,QAAO,KAAK,OAAO,MAAM;;;;;;;;;;;;;;;;ACvB3B,SAAgB,oBAAoB,aAA6B;AAC/D,QAAO,KAAK,aAAa,gBAAgB,iBAAiB;;;;;;;;;;;;;AAc5D,SAAgB,qBAA6B;AAC3C,QAAO,KAAK,SAAS,EAAE,YAAY,QAAQ;;;;;;;;;;;;;;;;;;;ACL7C,SAAgB,YAAY,OAAuB;AACjD,KAAI,UAAU,EACZ,QAAO;AAGT,KAAI,QAAQ,KACV,QAAO,GAAG,MAAM;AAGlB,KAAI,QAAQ,OAAO,KACjB,QAAO,IAAI,QAAQ,MAAM,QAAQ,EAAE,CAAC;AAGtC,KAAI,QAAQ,OAAO,OAAO,KACxB,QAAO,IAAI,SAAS,OAAO,OAAO,QAAQ,EAAE,CAAC;AAG/C,QAAO,IAAI,SAAS,OAAO,OAAO,OAAO,QAAQ,EAAE,CAAC;;;;;;;;;;;;;;AAetD,eAAsB,cAAc,UAAuC;AACzE,KAAI;EACF,MAAM,QAAQ,MAAM,QAAQ,SAAS;EAErC,IAAI,QAAQ;EACZ,IAAI,OAAO;AAEX,OAAK,MAAM,QAAQ,OAAO;GAExB,MAAM,QAAQ,MAAM,KADH,GAAG,SAAS,GAAG,OACE;AAElC,OAAI,MAAM,QAAQ,EAAE;AAClB;AACA,YAAQ,MAAM;;;AAIlB,SAAO;GAAE;GAAO;GAAM;SAElB;AAEJ,SAAO;GAAE,OAAO;GAAG,MAAM;GAAG;;;;;;;;;;;;;;;;;;;;AAqBhC,eAAsB,iBAAiB,aAGpC;CACD,MAAM,SAAS,MAAM,cAAc,oBAAoB,CAAC;CAExD,IAAI,UAA6B;AACjC,KAAI,YACF,WAAU,MAAM,cAAc,oBAAoB,YAAY,CAAC;AAGjE,QAAO;EAAE;EAAS;EAAQ;;;;;;;;;;ACnG5B,IAAa,eAAb,MAA0B;CACxB,YAAY,UAAmC;AAAlB,OAAA,WAAA;;;;;CAK7B,MAAc,YAA2B;AACvC,QAAM,MAAM,KAAK,UAAU;GAAE,WAAW;GAAM,MAAM;GAAO,CAAC;;;;;;;;CAS9D,MAAM,aAAa,WAAoC;EACrD,MAAM,UAAU,MAAM,aAAa,UAAU;AAC7C,SAAO,KAAK,KAAK,UAAU,QAAQ;;;;;;;;CASrC,MAAM,KAAK,WAA2C;AACpD,MAAI;AAGF,UADa,MAAM,SADD,MAAM,KAAK,aAAa,UAAU,CACd;UAGlC;AAEJ,UAAO;;;;;;;;;;;CAYX,MAAM,MAAM,WAAmB,MAA6B;AAC1D,QAAM,KAAK,WAAW;EAEtB,MAAM,YAAY,MAAM,KAAK,aAAa,UAAU;EACpD,MAAM,UAAU,GAAG,UAAU;AAG7B,QAAM,UAAU,SAAS,KAAK;AAC9B,QAAM,OAAO,SAAS,UAAU;;;;;;;;;;AAWpC,eAAsB,UACpB,WACA,WACwB;AACxB,MAAK,MAAM,YAAY,WAAW;EAEhC,MAAM,OAAO,MADG,IAAI,aAAa,SAAS,CACf,KAAK,UAAU;AAC1C,MAAI,SAAS,MAAM;AAGjB,WAAQ,kBAFQ,MAAM,aAAa,UAAU,EACtB,UAAU,GAAG,EAAE,GACJ;AAClC,UAAO;;;AAIX,QAAO;;;;;;;;;AAUT,eAAsB,WACpB,WACA,MACA,UACe;AAEf,OADgB,IAAI,aAAa,SAAS,CAC5B,MAAM,WAAW,KAAK;AAIpC,SAAQ,mBAFQ,MAAM,aAAa,UAAU,EACtB,UAAU,GAAG,EAAE,CACL,cAAc;;;;AC/GjD,IAAa,eAAb,MAA0B;CACxB,eAAuB;CAEvB,YACE,aAA6B,GAC7B,YAA4B,KAC5B;AAFQ,OAAA,aAAA;AACA,OAAA,YAAA;;CAGV,MAAM,QAAW,WAAyC;AACxD,OAAK,IAAI,UAAU,GAAG,WAAW,KAAK,YAAY,UAChD,KAAI;GACF,MAAM,SAAS,MAAM,WAAW;AAChC,QAAK,eAAe;AACpB,UAAO;WAEF,OAAO;AACZ,QAAK;AAEL,OAAI,YAAY,KAAK,cAAc,CAAC,KAAK,YAAY,MAAM,CACzD,OAAM;GAGR,MAAM,QAAQ,KAAK,YAAY,KAAK;AACpC,cAAW,SAAS,UAAU,EAAE,GAAG,KAAK,WAAW,SAAS,MAAM,IAAI;AACtE,SAAM,KAAK,MAAM,MAAM;;AAI3B,QAAM,IAAI,MAAM,uBAAuB;;CAGzC,YAAoB,OAAqB;AAEvC,MAAI,MAAM,QAAQ;GAAC;GAAc;GAAa;GAAY,CAAC,SAAS,MAAM,KAAK,CAC7E,QAAO;AAIT,MAAI,MAAM,cAAc,MAAM,cAAc,OAAO,MAAM,aAAa,IACpE,QAAO;AAIT,SAAO;;CAGT,MAAc,IAA2B;AACvC,SAAO,IAAI,SAAQ,YAAW,WAAW,SAAS,GAAG,CAAC;;CAGxD,kBAA0B;AACxB,SAAO,KAAK;;CAGd,QAAc;AACZ,OAAK,eAAe;;;;;AClDxB,MAAM,kBAAkB;AAExB,IAAa,uBAAb,MAAyD;CACvD;CAEA,YAAY,aAAqB,GAAG;AAClC,OAAK,eAAe,IAAI,aAAa,WAAW;;CAGlD,MAAM,SAAS,QAAiC;AAC9C,SAAO,KAAK,aAAa,QAAQ,YAAY;GAE3C,MAAM,YAAY,MAAM,KAAK,mBAAmB,OAAO;GAGvD,MAAM,mBAAmB,MAAM,KAAK,wBAAwB,UAAU;GAEtE,MAAM,eAAe,OAAO;GAC5B,MAAM,iBAAiB,iBAAiB;AAGxC,WAAQ,2CAA2C,aAAa,KAAK,eAAe,YAFpE,IAAI,iBAAiB,gBAAgB,KAAK,QAAQ,EAAE,CAEgC,IAAI;AAExG,UAAO;IACP;;CAGJ,MAAc,mBAAmB,QAAiC;AAChE,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,OAAO,IAAI,UAAU;AAC3B,QAAK,OAAO,QAAQ,QAAQ,EAAE,UAAU,aAAa,CAAC;GAEtD,MAAM,MAAM,MAAM,QAChB,iBACA;IACE,QAAQ;IACR,SAAS,KAAK,YAAY;IAC3B,GACA,QAAQ;IACP,IAAI,OAAO;AAEX,QAAI,GAAG,SAAS,UAAU;AACxB,aAAQ;MACR;AAEF,QAAI,GAAG,aAAa;AAClB,SAAI,IAAI,eAAe,KAAK;MAC1B,MAAM,wBAAQ,IAAI,MAAM,QAAQ,IAAI,WAAW,IAAI,OAAO;AACxD,YAAc,aAAa,IAAI;AACjC,aAAO,OAAO,MAAM;;AAGtB,SAAI;MAEF,MAAM,WAAW,KAAK,MAAM,KAAK;AACjC,UAAI,CAAC,SAAS,QAAQ,IACpB,QAAO,uBAAO,IAAI,MAAM,4BAA4B,CAAC;AAEvD,cAAQ,SAAS,OAAO,IAAI;cAEvB,OAAY;AACjB,6BAAO,IAAI,MAAM,6BAA6B,MAAM,UAAU,CAAC;;MAEjE;KAEL;AAED,OAAI,GAAG,UAAU,UAAU;AACzB,WAAO,MAAM;KACb;AAEF,QAAK,KAAK,IAAI;IACd;;CAGJ,MAAc,wBAAwB,KAA8B;AAClE,SAAO,IAAI,SAAS,SAAS,WAAW;AACtC,SAAM,IAAI,MAAM,QAAQ;AACtB,QAAI,IAAI,eAAe,KAAK;KAC1B,MAAM,wBAAQ,IAAI,MAAM,QAAQ,IAAI,WAAW,+BAA+B;AAC5E,WAAc,aAAa,IAAI;AACjC,YAAO,OAAO,MAAM;;IAGtB,MAAM,SAAmB,EAAE;AAC3B,QAAI,GAAG,SAAQ,UAAS,OAAO,KAAK,MAAM,CAAC;AAC3C,QAAI,GAAG,aAAa,QAAQ,OAAO,OAAO,OAAO,CAAC,CAAC;AACnD,QAAI,GAAG,SAAS,OAAO;KACvB,CAAC,GAAG,SAAS,OAAO;IACtB;;CAGJ,kBAA0B;AACxB,SAAO,KAAK,aAAa,iBAAiB;;;;;ACxF9C,MAAM,gBAAgB,IAAI,OAAO;AAEjC,IAAa,uBAAb,MAAyD;CACvD;CACA,aAAoC;CAEpC,YACE,SACA,aAAqB,GACrB;AAFQ,OAAA,UAAA;AAGR,OAAK,eAAe,IAAI,aAAa,WAAW;;CAGlD,MAAM,SAAS,QAAiC;AAE9C,MAAI,OAAO,aAAa,eAAe;AACrC,cAAW,+CAA+C,OAAO,aAAa,OAAO,MAAM,QAAQ,EAAE,CAAC,KAAK;AAC3G,SAAM,IAAI,MAAM,8BAA8B;;AAGhD,SAAO,KAAK,aAAa,QAAQ,YAAY;GAE3C,MAAM,MAAM,MAAM,KAAK,QAAQ,WAAW;AAC1C,OAAI,KAAK,eAAe,IACtB,KAAI;AACF,WAAO,MAAM;AACb,SAAK,aAAa;WAEd;GAMR,MAAM,eAAe,OAAO;GAC5B,MAAM,SAAS,MAAM,OAAO,WAAW,OAAO,CAAC,UAAU;GACzD,MAAM,iBAAiB,OAAO;GAC9B,MAAM,UAAU,IAAI,iBAAiB,gBAAgB,KAAK,QAAQ,EAAE;AAEpE,QAAK,QAAQ,gBAAgB;AAC7B,WAAQ,2CAA2C,aAAa,KAAK,eAAe,UAAU,MAAM,IAAI;AAExG,UAAO,OAAO,KAAK,OAAO;IAC1B;;CAGJ,kBAA0B;AACxB,SAAO,KAAK,aAAa,iBAAiB;;;;;AC3D9C,IAAa,wBAAb,cAA2C,MAAM;CAC/C,YAAY,UAAU,qCAAqC;AACzD,QAAM,QAAQ;AACd,OAAK,OAAO;;;AAIhB,IAAa,mBAAb,cAAsC,MAAM;CAC1C,YAAY,UAAU,+BAA+B;AACnD,QAAM,QAAQ;AACd,OAAK,OAAO;;;AAIhB,IAAa,4BAAb,cAA+C,MAAM;CACnD,YAAY,UAAU,kCAAkC;AACtD,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;ACMhB,eAAsB,qBACpB,QACA,UAA2B,EAAE,EACZ;CACjB,MAAM,cAAc,QAAQ,eAAe,EAAE;AAE7C,MAAK,MAAM,cAAc,YACvB,KAAI;AACF,UAAQ,gCAAgC,WAAW,YAAY,KAAK,GAAG;AAEvE,SADe,MAAM,WAAW,SAAS,OAAO;UAG3C,OAAY;AACjB,aAAW,IAAI,WAAW,YAAY,KAAK,YAAY,MAAM,UAAU;AAGvE,MAAI,MAAM,SAAS,4BACjB,OAAM;AAIR;;AAIJ,OAAM,IAAI,0BAA0B,iCAAiC;;;;;;;;;;;;;;;AAgBvE,SAAgB,0BAA0B,OAAwB,QAAkB;AAClF,SAAQ,MAAR;EACE,KAAK,MACH,QAAO,CAAC,uBAAuB;EACjC,KAAK,MACH,QAAO,CAAC,uBAAuB;EAEjC,QACE,QAAO,CAAC,wBAAwB,uBAAuB;;;;;;;;;;;;;;;;;;;ACxD7D,SAAgB,yBAAyB,cAAsB,GAAG;AAChE,QAAO,OAAO,YAAY;;;;;;;;;;;;;;;;;;;;AAqB5B,eAAsB,uBACpB,OACA,cAAsB,GACR;CACd,MAAM,QAAQ,yBAAyB,YAAY;CAGnD,MAAM,eAAe,MAAM,KAAI,SAAQ,MAAM,KAAK,CAAC;AAGnD,QAAO,QAAQ,IAAI,aAAa;;;;AC3ClC,MAAM,aAAa;AACnB,MAAM,cAAc;AAEpB,SAAgB,gBAAwB;CACtC,MAAM,UAAU,GAAG,SAAS;AAC5B,QAAO,KAAK,KAAK,SAAS,YAAY,YAAY;;AAGpD,SAAgB,mBAAyB;CACvC,MAAM,aAAa,eAAe;CAClC,MAAM,YAAY,KAAK,QAAQ,WAAW;AAE1C,KAAI,CAAC,GAAG,WAAW,UAAU,CAC3B,IAAG,UAAU,WAAW;EAAE,WAAW;EAAM,MAAM;EAAO,CAAC;AAG3D,KAAI,CAAC,GAAG,WAAW,WAAW,CAE5B,IAAG,cACD,YACA,KAAK,UAH4B,EAAE,MAAM,EAAE,EAAE,EAGd,MAAM,EAAE,EACvC,EAAE,MAAM,KAAO,CAChB;;AAIL,SAAgB,aAAyB;AACvC,mBAAkB;CAClB,MAAM,aAAa,eAAe;CAClC,MAAM,UAAU,GAAG,aAAa,YAAY,QAAQ;AACpD,QAAO,KAAK,MAAM,QAAQ;;AAG5B,SAAgB,YAAY,QAA0B;AACpD,mBAAkB;CAClB,MAAM,aAAa,eAAe;AAClC,IAAG,cACD,YACA,KAAK,UAAU,QAAQ,MAAM,EAAE,EAC/B,EAAE,MAAM,KAAO,CAChB;;;;ACpCH,SAAgB,WAAwB;CAEtC,MAAM,UAAU,QAAQ,IAAI;AAC5B,KAAI,WAAW,QAAQ,MAAM,CAI3B,QAHa,QAAQ,MAAM,IAAI,CAC5B,KAAK,MAAc,EAAE,MAAM,CAAC,CAC5B,QAAQ,MAAc,EAAE,SAAS,EAAE,CAC1B,KAAK,SAAiB,EAAE,KAAK,EAAE;AAI7C,KAAI;AAEF,SADe,YAAY,CACb,KAAK,KAAI,cAAa;GAClC,KAAK,SAAS;GACd,OAAO,SAAS;GAChB,WAAW,SAAS;GACrB,EAAE;SAEC;AAEJ,SAAO,EAAE;;;;;AC9Bb,SAAgB,QAAQ,KAAqB;AAC3C,KAAI,IAAI,SAAS,EACf,QAAO;AAIT,QAAO,GAFO,IAAI,UAAU,GAAG,EAAE,CAEjB,MADH,IAAI,UAAU,IAAI,SAAS,EAAE;;;;ACF5C,MAAM,gBAAgB;AAEtB,eAAsB,WAAW,KAA8B;AAC7D,KAAI;AACF,SAAO,MAAM;AACb,QAAM,OAAO,UAAU;EACvB,MAAM,gBAAgB,OAAO,oBAAoB;AAEjD,SADkB,KAAK,IAAI,GAAG,gBAAgB,cAAc;UAGvD,OAAY;AAEjB,MAAI,OAAO,SAAS,SAAS,cAAc,IAAI,OAAO,SAAS,SAAS,eAAe,IAAI,OAAO,aAAa,SAAS,eACtH,QAAO;AAET,QAAM;;;AAYV,SAAgB,mBAAmB,KAAa,WAAiC;AAG/E,QAAO;EACL;EACA;EACA,cALmB;EAMnB,YAAY;AACV,OAAI,KAAK,eAAe,GAAG;AACzB,SAAK;AAEL,QAAI,KAAK,iBAAiB,EACxB,SAAQ,KAAK,SAAS,QAAQ,KAAK,IAAI,CAAC,yCAAyC;;;EAIvF,SAAS;AACP,UAAO,KAAK,iBAAiB;;EAEhC;;;;AC9CH,eAAsB,YAAY,KAA+B;AAC/D,KAAI;AACF,SAAO,MAAM;AACb,QAAM,OAAO,UAAU;AAEvB,UAAQ,IAAI,aAAa,QAAQ,IAAI,CAAC,yBAAyB;AAC/D,SAAO;UAEF,OAAY;AAEjB,MAAI,OAAO,SAAS,SAAS,cAAc,IAAI,OAAO,SAAS,SAAS,eAAe,IAAI,OAAO,aAAa,SAAS,gBAAgB;AACtI,WAAQ,KAAK,qBAAqB,QAAQ,IAAI,CAAC,qBAAqB;AACpE,UAAO;;AAGT,QAAM;;;;;ACPV,IAAa,iBAAb,MAA4B;CAC1B,MAAM,OAAO,MAA8C;EACzD,MAAM,YAAY,MAAM,KAAK,iBAAiB,KAAK;AACnD,MAAI,UAAU,WAAW,EACvB,QAAO;AAIT,SADiB,UADG,KAAK,MAAM,KAAK,QAAQ,GAAG,UAAU,OAAO;;CAKlE,MAAgB,iBAAiB,MAAyC;EACxE,MAAM,YAA4B,EAAE;AAEpC,OAAK,MAAM,OAAO,MAAM;AAEtB,OAAI,CADY,MAAM,YAAY,IAAI,CAEpC;GAEF,MAAM,YAAY,MAAM,WAAW,IAAI;AACvC,OAAI,cAAc,GAAG;AACnB,eAAW,OAAO,QAAQ,IAAI,CAAC,yBAAyB;AACxD;;AAGF,aAAU,KAAK;IACb;IACA,SAAS,mBAAmB,KAAK,UAAU;IAC5C,CAAC;;AAGJ,SAAO;;;AAKX,IAAa,qBAAb,cAAwC,eAAe;CACrD,eAAuB;CAEvB,MAAM,OAAO,MAA8C;EACzD,MAAM,YAAY,MAAM,KAAK,iBAAiB,KAAK;AACnD,MAAI,UAAU,WAAW,EACvB,QAAO;EAET,MAAM,WAAW,UAAU,KAAK,eAAe,UAAU;AACzD,OAAK;AACL,SAAO;;CAGT,QAAc;AACZ,OAAK,eAAe;;;AAKxB,IAAa,mBAAb,cAAsC,eAAe;CACnD,MAAM,OAAO,MAA8C;EACzD,MAAM,YAAY,MAAM,KAAK,iBAAiB,KAAK;AACnD,MAAI,UAAU,WAAW,EACvB,QAAO;AAGT,SAAO,UAAU;;;;;AClErB,IAAa,UAAb,MAAqB;CACnB;CACA;CACA,mBAAgD;CAEhD,YAAY,WAAwB,UAAU;AAC5C,OAAK,OAAO,UAAU,CAAC,KAAI,MAAK,EAAE,IAAI;AAEtC,MAAI,KAAK,KAAK,WAAW,EACvB,OAAM,IAAI,iBAAiB,yBAAyB;AAGtD,OAAK,WAAW,KAAK,eAAe,SAAS;;CAG/C,eAAuB,UAAuB;AAC5C,UAAQ,UAAR;GACE,KAAK,SACH,QAAO,IAAI,gBAAgB;GAC7B,KAAK,cACH,QAAO,IAAI,oBAAoB;GACjC,KAAK,WACH,QAAO,IAAI,kBAAkB;GAC/B,QACE,QAAO,IAAI,gBAAgB;;;CAIjC,MAAM,YAA6B;AAEjC,MAAI,KAAK,oBAAoB,CAAC,KAAK,iBAAiB,QAAQ,QAAQ,CAClE,QAAO,KAAK,iBAAiB;EAI/B,MAAM,YAAY,MAAM,KAAK,SAAS,OAAO,KAAK,KAAK;AAEvD,MAAI,CAAC,UACH,OAAM,IAAI,uBAAuB;AAGnC,OAAK,mBAAmB;AACxB,SAAO,UAAU;;CAGnB,iBAAuB;AACrB,MAAI,KAAK,iBACP,MAAK,iBAAiB,QAAQ,WAAW;;CAI7C,gBAA+B;AAC7B,SAAO,KAAK,kBAAkB,OAAO;;;;;;;;;;;;ACjBzC,eAAsB,cACpB,QACA,UAAkC,EAAE,EACnB;CACjB,MAAM,EACJ,QAAQ,MACR,mBAAmB,OACnB,OAAO,QACP,aAAa,MACX;CAGJ,MAAM,OAAO,MAAM,uBAAuB,OAAO;CACjD,MAAM,aAAa,KAAK,UAAU,GAAG,EAAE;AAGvC,KAAI,MACF,KAAI;EAGF,MAAM,SAAS,MAAM,gBAAgB,MAAM,CADlB,oBAAoB,QAAQ,KAAK,CAAC,CACE,CAAC;AAC9D,MAAI,QAAQ;AACV,WAAQ,gBAAgB,aAAa;AACrC,UAAO;;AAIT,MAAI,CAAC,kBAAkB;GAErB,MAAM,eAAe,MAAM,gBAAgB,MAAM,CADzB,oBAAoB,CACsB,CAAC;AACnE,OAAI,cAAc;AAChB,YAAQ,yBAAyB,aAAa;AAC9C,WAAO;;;UAIN,OAAY;AACjB,aAAW,sBAAsB,MAAM,UAAU;;AAMrD,SAAQ,iBAAiB,WAAW,kBAAkB;CAEtD,MAAM,aAAa,MAAM,qBAAqB,QAAQ;EACpD;EACA;EACA,aAAa,kBAAkB,QAAQ;EACxC,CAAC;AAGF,KAAI,MACF,KAAI;AAEF,QAAM,iBAAiB,MAAM,YADJ,oBAAoB,QAAQ,KAAK,CAAC,CACD;AAC1D,UAAQ,aAAa,aAAa;UAE7B,OAAY;AACjB,aAAW,uBAAuB,MAAM,UAAU;;AAKtD,QAAO;;;;;;;;;AAUT,eAAsB,eACpB,SACA,UAAkC,EAAE,EACjB;CACnB,MAAM,EAAE,cAAc,MAAM;CAC5B,MAAM,QAAQ,yBAAyB,YAAY;CAEnD,MAAM,QAAQ,QAAQ,KAAI,WACxB,YAAY,cAAc,QAAQ,QAAQ,CAAC,CAC5C;AAED,QAAO,QAAQ,IAAI,MAAM;;;;;;AAO3B,SAAS,kBAAkB,SAAgD;CACzE,MAAM,EAAE,OAAO,QAAQ,aAAa,GAAG,YAAY;CACnD,MAAM,cAA6B,EAAE;CAIrC,MAAM,OAAO,WAAW,IAAI,QAAQ,SAAS;AAE7C,KAAI,SAAS,UAAU,SAAS,MAC9B,aAAY,KAAK,IAAI,qBAAqB,MAAM,WAAW,CAAC;AAG9D,KAAI,SAAS,UAAU,SAAS,MAC9B,aAAY,KAAK,IAAI,qBAAqB,WAAW,CAAC;AAGxD,QAAO"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pz4l/tinyimg-core",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"description": "Core library for TinyPNG image compression with multi-key management, intelligent caching, and fallback strategies",
|
|
6
|
+
"author": "pzehrel",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/pzehrel/tinyimg/tree/main/packages/tinyimg-core#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/pzehrel/tinyimg.git",
|
|
12
|
+
"directory": "packages/tinyimg-core"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/pzehrel/tinyimg/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"tinypng",
|
|
19
|
+
"image",
|
|
20
|
+
"compression",
|
|
21
|
+
"compress",
|
|
22
|
+
"optimize",
|
|
23
|
+
"optimization",
|
|
24
|
+
"img",
|
|
25
|
+
"minify",
|
|
26
|
+
"png",
|
|
27
|
+
"jpg",
|
|
28
|
+
"jpeg",
|
|
29
|
+
"webp",
|
|
30
|
+
"imagemin"
|
|
31
|
+
],
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"import": "./dist/index.mjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsdown",
|
|
46
|
+
"typecheck": "tsc --noEmit"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"form-data": "4.0.5",
|
|
50
|
+
"p-limit": "7.3.0",
|
|
51
|
+
"tinify": "1.8.2"
|
|
52
|
+
}
|
|
53
|
+
}
|