uglify-js-minify-css-allfiles 2.2.4 → 2.3.1

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/dist/module.js CHANGED
@@ -2,106 +2,149 @@
2
2
  * uglify-js and minify-css for all files
3
3
  * Released under the terms of MIT license
4
4
  * Copyright (C) 2024 yeongmin
5
+ * @module module
5
6
  */
6
7
 
7
8
  import { promises as fs } from 'fs';
8
- import path from 'path';
9
+ import crypto from 'crypto';
9
10
  import Logger from './modules/logger.js';
10
- import { getAllFiles, writeFile } from './modules/fileHandler.js';
11
+ import { getAllFiles, writeFile, shouldVersionFile } from './modules/fileHandler.js';
12
+ import HashManager from './modules/hashManager.js';
11
13
  import { minifyJS, minifyCSS } from './modules/minifier.js';
12
- import { createRequire } from 'module';
13
- import { fileURLToPath, pathToFileURL } from 'url';
14
-
15
- const require = createRequire(import.meta.url);
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = path.dirname(__filename);
14
+ import { DEFAULT_IMAGE_EXTENSIONS, IMAGE_PATTERNS } from './modules/imageUtils.js';
15
+ import { resolveImagePath, resolveModulePath, makeRelativePath, containsFolder, getExtension, resolvePath } from './modules/pathResolver.js';
18
16
 
19
17
  /**
20
- * Resolves the path of a module.
21
- * @param {string} moduleName - The name of the module to resolve.
22
- * @returns {string} The resolved module path as a URL.
18
+ * Processes image references in files and adds/updates version hashes for cache busting.
19
+ * Handles both JavaScript and CSS files differently:
20
+ * - For JS files: Generates a random hash
21
+ * - For CSS files: Generates a content-based hash
22
+ *
23
+ * @async
24
+ * @param {RegExp} pattern - Regular expression pattern to match image references
25
+ * @param {string} content - Content of the file being processed
26
+ * @param {string} fileExt - File extension (e.g., '.js', '.css')
27
+ * @param {string} filePath - Absolute path to the file being processed
28
+ * @param {Logger} logger - Logger instance for tracking changes
29
+ * @param {HashManager} hashManager - Manages content-based hashing for images
30
+ * @param {string[]} targetExtensions - List of image extensions to process (e.g., ['.png', '.jpg'])
31
+ * @returns {Promise<{content: string, modified: boolean}>} Modified content and whether changes were made
23
32
  */
24
- function resolveModulePath(moduleName) {
25
- const modulePath = require.resolve(moduleName, { paths: [__dirname] });
26
- return pathToFileURL(modulePath).href;
27
- }
33
+ async function processPattern(pattern, content, fileExt, filePath, logger, hashManager, targetExtensions) {
34
+ const promises = [];
35
+ let newContent = content;
36
+ let modified = false;
28
37
 
29
- /**
30
- * @typedef {Object} FileHandlerOptions
31
- * @property {BabelOptions} [babelOptions] - Babel transformation options.
32
- * @property {JSMinifyOptions} [jsMinifyOptions] - JavaScript minification options.
33
- * @property {CSSMinifyOptions} [cssMinifyOptions] - CSS minification options.
34
- */
38
+ if (fileExt === '.js') {
39
+ const newHash = crypto.randomBytes(16).toString('hex').substring(0, 8);
35
40
 
36
- /**
37
- * @callback FileHandler
38
- * @param {string} filePath - The path of the file to process.
39
- * @param {string} content - The content of the file.
40
- * @param {Logger} logger - The logger instance.
41
- * @param {FileHandlerOptions} options - Options for processing.
42
- * @returns {Promise<void>}
43
- */
41
+ newContent = content.replace(pattern, (match, imagePath, ext, queryString) => {
42
+ if (imagePath.startsWith('data:') || imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
43
+ return match;
44
+ }
45
+ modified = true;
46
+ logger?.info('Updated JS image version', {
47
+ file: filePath,
48
+ image: imagePath + ext,
49
+ newHash,
50
+ });
51
+ return `${imagePath}${ext}?v=${newHash}${queryString}`;
52
+ });
53
+ return { content: newContent, modified };
54
+ }
44
55
 
45
- /**
46
- * Object containing handlers for different file types.
47
- * @type {Object.<string, FileHandler>}
48
- */
49
- const FILE_HANDLERS = {
50
- '.js': async (filePath, content, logger, options) => {
51
- try {
52
- let transformed = content;
53
- if (options.babelOptions) {
54
- const babelCoreUrl = resolveModulePath('@babel/core');
55
- const { transformSync } = await import(babelCoreUrl);
56
- transformed = transformSync(content, options.babelOptions).code;
56
+ if (fileExt === '.css') {
57
+ newContent = content.replace(pattern, (match, imagePath, _ext, queryString) => {
58
+ if (imagePath.startsWith('data:') || imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
59
+ return match;
57
60
  }
58
- const result = minifyJS(transformed, options.jsMinifyOptions);
59
- await writeFile(filePath, result, logger);
60
- } catch (error) {
61
- await logger?.error('JavaScript minification failed', { filePath, error: error.message });
62
- }
63
- },
64
- '.css': async (filePath, content, logger, options) => {
65
- try {
66
- const output = await minifyCSS(content, options.cssMinifyOptions);
67
- if (0 < output.warnings.length) {
68
- await logger?.warn('CSS minification warnings', { filePath, warnings: output.warnings });
61
+
62
+ const absoluteImagePath = resolveImagePath(imagePath, filePath);
63
+ if (!absoluteImagePath || !shouldVersionFile(absoluteImagePath, targetExtensions)) {
64
+ return match;
65
+ }
66
+
67
+ const marker = `__HASH_MARKER_${promises.length}__`;
68
+ promises.push({ marker, imagePath, absoluteImagePath });
69
+ return match.replace(imagePath + (queryString || ''), `${imagePath}?v=${marker}`);
70
+ });
71
+
72
+ if (promises.length > 0) {
73
+ for (const { marker, imagePath, absoluteImagePath } of promises) {
74
+ const { hash, changed } = await hashManager.generateHash(absoluteImagePath);
75
+
76
+ if (!hash) {
77
+ newContent = newContent.replace(`${imagePath}?v=${marker}`, imagePath);
78
+ // 'Failed to generate hash, keeping original URL : ' + filePath;
79
+ continue;
80
+ }
81
+
82
+ if (changed) {
83
+ modified = true;
84
+ await logger?.info('Updated CSS image version', {
85
+ file: filePath,
86
+ image: imagePath,
87
+ oldHash: hashManager.getPreviousHash(absoluteImagePath),
88
+ newHash: hash,
89
+ });
90
+ }
91
+
92
+ newContent = newContent.replace(`?v=${marker}`, `?v=${hash}`);
69
93
  }
70
- await writeFile(filePath, output.styles, logger);
71
- } catch (error) {
72
- await logger?.error('CSS minification failed', { filePath, error: error.message });
73
94
  }
74
- },
75
- };
95
+
96
+ await Promise.all(promises.filter((p) => p instanceof Promise));
97
+ }
98
+
99
+ return { content: newContent, modified };
100
+ }
76
101
 
77
102
  /**
78
- * Processes a single file based on its extension.
103
+ * Updates image references in a file with version query strings.
79
104
  * @async
80
- * @param {string} filePath - The path of the file to process.
81
- * @param {Logger} logger - The logger instance.
82
- * @param {FileHandlerOptions} options - Options for processing.
105
+ * @param {string} filePath - Path to the file to process.
106
+ * @param {Object} versioningOptions - Options for versioning.
107
+ * @param {string[]} [versioningOptions.extensions] - List of file extensions to version.
108
+ * @param {Logger} logger - Logger instance.
109
+ * @param {HashManager} hashManager - Hash manager instance.
83
110
  * @returns {Promise<void>}
84
111
  */
85
- async function processFile(filePath, logger, options) {
112
+ async function updateImageReferences(filePath, versioningOptions, logger, hashManager) {
113
+ const { extensions } = versioningOptions;
114
+ const targetExtensions =
115
+ extensions || DEFAULT_IMAGE_EXTENSIONS.map((ext) => (ext === 'jpe?g' ? ['.jpg', '.jpeg'] : ['.' + ext.replace('?', '')])).flat();
116
+
117
+ const fileExt = getExtension(filePath);
118
+ const patterns = IMAGE_PATTERNS[fileExt.substring(1)] || [];
119
+
86
120
  try {
87
- const fileContent = await fs.readFile(filePath, 'utf-8');
88
- const fileExtension = path.extname(filePath).toLowerCase();
89
- const handler = FILE_HANDLERS[fileExtension];
121
+ let content = await fs.readFile(filePath, 'utf-8');
122
+ let modified = false;
90
123
 
91
- if (handler) {
92
- await handler(filePath, fileContent, logger, options);
93
- } else {
94
- await logger?.info(`Unsupported file type, skipping: ${filePath}`);
124
+ for (const pattern of patterns) {
125
+ const result = await processPattern(pattern, content, fileExt, filePath, logger, hashManager, targetExtensions);
126
+ content = result.content;
127
+ modified = modified || result.modified;
128
+ }
129
+
130
+ if (modified) {
131
+ await fs.writeFile(filePath, content, 'utf-8');
132
+ await logger?.info('Updated file with versioned image references', {
133
+ file: filePath,
134
+ });
95
135
  }
96
136
  } catch (error) {
97
- await logger?.error('Error processing file', { filePath, error: error.message });
137
+ await logger?.error('Failed to process file', {
138
+ file: filePath,
139
+ error: error.message,
140
+ });
98
141
  }
99
142
  }
100
143
 
101
144
  /**
102
145
  * Resolves Babel options based on the provided configuration.
103
146
  * @param {boolean|BabelOptions} useBabel - The Babel options object or boolean.
104
- * @returns {Promise<BabelOptions|null>} A promise that resolves to the Babel options or null if no valid options are provided.
147
+ * @returns {Promise<BabelOptions|null>} - A promise that resolves to the Babel options or null if disabled.
105
148
  */
106
149
  async function resolveBabelOptions(useBabel) {
107
150
  if (!useBabel) return null;
@@ -119,6 +162,48 @@ async function resolveBabelOptions(useBabel) {
119
162
  }
120
163
  }
121
164
 
165
+ /**
166
+ * Processes a single file based on its extension.
167
+ * @async
168
+ * @param {string} filePath - The path of the file to process.
169
+ * @param {Logger} logger - The logger instance.
170
+ * @param {Object} options - Processing options.
171
+ * @param {BabelOptions} [options.babelOptions] - Babel transformation options.
172
+ * @param {Object} [options.jsMinifyOptions] - JavaScript minification options.
173
+ * @param {Object} [options.cssMinifyOptions] - CSS minification options.
174
+ * @returns {Promise<void>}
175
+ */
176
+ async function processFile(filePath, logger, options) {
177
+ try {
178
+ const fileContent = await fs.readFile(filePath, 'utf-8');
179
+ const fileExtension = getExtension(filePath);
180
+
181
+ let result;
182
+ if (fileExtension === '.js') {
183
+ let transformed = fileContent;
184
+ if (options.babelOptions) {
185
+ const babelCoreUrl = resolveModulePath('@babel/core');
186
+ const { transformSync } = await import(babelCoreUrl);
187
+ transformed = transformSync(fileContent, options.babelOptions).code;
188
+ }
189
+ result = minifyJS(transformed, options.jsMinifyOptions);
190
+ } else if (fileExtension === '.css') {
191
+ const output = await minifyCSS(fileContent, options.cssMinifyOptions);
192
+ if (output.warnings.length > 0) {
193
+ await logger?.warn('CSS minification warnings', { filePath, warnings: output.warnings });
194
+ }
195
+ result = output.styles;
196
+ } else {
197
+ // `Unsupported file type, skipping: ${filePath}`;
198
+ return;
199
+ }
200
+
201
+ await writeFile(filePath, result, logger);
202
+ } catch (error) {
203
+ await logger?.error('Error processing file', { filePath, error: error.message });
204
+ }
205
+ }
206
+
122
207
  /**
123
208
  * Options for Babel configuration.
124
209
  * @typedef {Object} BabelOptions
@@ -167,39 +252,44 @@ async function resolveBabelOptions(useBabel) {
167
252
  * Options for minification configuration.
168
253
  * @typedef {Object} MinifyOptions
169
254
  * @property {string} [excludeFolder=''] - Folder to exclude from minification.
170
- * @property {boolean|BabelOptions} [useBabel=false] - Whether to use Babel for transformation, and the options for Babel if used.
171
- * @property {boolean|LogOptions} [useLog=true] - Whether to use logging, and the options for logging if used.
255
+ * @property {boolean|BabelOptions} [useBabel=false] - Whether to use Babel for transformation.
256
+ * @property {boolean|LogOptions} [useLog=true] - Whether to use logging.
172
257
  * @property {JSMinifyOptions} [jsMinifyOptions={}] - Options for JavaScript minification.
173
258
  * @property {CSSMinifyOptions} [cssMinifyOptions={}] - Options for CSS minification.
259
+ * @property {string[]|null} [useVersioning=null] - Options for file versioning.
174
260
  */
175
261
 
176
262
  /**
177
- * Minifies all JavaScript and CSS files in the specified directory and its subdirectories.
178
- *
179
- * @param {string} contentPath - The path to the directory containing the files to be minified.
180
- * @param {MinifyOptions} [options={}] - Options for minification, Babel, and logging.
181
- * @returns {Promise<void>} A promise that resolves when all files have been processed.
263
+ * Main function to minify all files and handle versioning.
264
+ * @async
265
+ * @param {string} contentPath - The path to process files from.
266
+ * @param {MinifyOptions} [options={}] - Configuration options.
267
+ * @returns {Promise<void>} - A promise that resolves when all files have been processed.
182
268
  * @throws {Error} If there's an issue reading or writing files.
183
269
  */
184
270
  export default async function minifyAll(contentPath, options = {}) {
185
- const {
186
- excludeFolder = '',
187
- useBabel = false,
188
- useLog = true,
189
- jsMinifyOptions = {},
190
- cssMinifyOptions = {},
191
- } = options;
271
+ const { excludeFolder = '', useBabel = false, useLog = true, jsMinifyOptions = {}, cssMinifyOptions = {}, useVersioning = null } = options;
192
272
 
193
273
  let logger = null;
194
274
  if (useLog) {
195
275
  const logOptions = typeof useLog === 'object' ? useLog : {};
196
276
  logger = new Logger(logOptions);
197
277
  await logger.initialize();
198
- await logger.info('Starting minification process', { contentPath, excludeFolder, useBabel });
278
+ await logger.info('Starting minification process', {
279
+ contentPath,
280
+ excludeFolder,
281
+ useBabel,
282
+ useVersioning: !!useVersioning,
283
+ });
199
284
  }
200
285
 
201
- const rootDir = path.resolve(contentPath || '');
286
+ const rootDir = resolvePath(contentPath || '');
202
287
  const babelOptions = await resolveBabelOptions(useBabel);
288
+ const hashManager = useVersioning ? new HashManager(rootDir) : null;
289
+
290
+ if (hashManager) {
291
+ await hashManager.initialize();
292
+ }
203
293
 
204
294
  const processOptions = {
205
295
  babelOptions,
@@ -209,17 +299,19 @@ export default async function minifyAll(contentPath, options = {}) {
209
299
 
210
300
  try {
211
301
  await getAllFiles(rootDir, async (filePath) => {
212
- const relativePath = path.relative(rootDir, filePath);
213
- if (
214
- excludeFolder &&
215
- (relativePath.startsWith(excludeFolder) ||
216
- relativePath.includes(path.sep + excludeFolder + path.sep))
217
- ) {
302
+ const relativePath = makeRelativePath(filePath, rootDir);
303
+ if (excludeFolder && containsFolder(relativePath, excludeFolder)) {
218
304
  await logger?.debug('Skipping excluded file', { filePath });
219
305
  return;
220
306
  }
221
307
 
222
308
  await processFile(filePath, logger, processOptions);
309
+
310
+ // Apply versioning after successful processing if enabled
311
+ if (useVersioning && hashManager) {
312
+ await updateImageReferences(filePath, useVersioning, logger, hashManager);
313
+ }
314
+
223
315
  logger?.incrementProcessedFiles(filePath);
224
316
  });
225
317
  } catch (error) {
@@ -4,20 +4,21 @@
4
4
  */
5
5
 
6
6
  import { promises as fs } from 'fs';
7
- import path from 'path';
7
+ import { joinPaths, getExtension } from './pathResolver.js';
8
8
 
9
9
  /**
10
10
  * Recursively gets all files in a directory and its subdirectories.
11
- *
12
11
  * @async
13
- * @param {string} dirPath - The path to the directory to search.
14
- * @param {Function} callback - The callback function to execute for each file.
12
+ * @param {string} dirPath - The path to the directory to search
13
+ * @param {Function} callback - Callback function for each file
14
+ * @param {string} callback.filePath - Full path to the file
15
+ * @param {fs.Stats} callback.fileStat - File statistics
15
16
  * @returns {Promise<void>}
16
17
  */
17
18
  export async function getAllFiles(dirPath, callback) {
18
19
  const files = await fs.readdir(dirPath);
19
20
  for (const file of files) {
20
- const filePath = path.join(dirPath, file);
21
+ const filePath = joinPaths(dirPath, file);
21
22
  const fileStat = await fs.stat(filePath);
22
23
  if (fileStat.isFile()) {
23
24
  await callback(filePath, fileStat);
@@ -28,12 +29,11 @@ export async function getAllFiles(dirPath, callback) {
28
29
  }
29
30
 
30
31
  /**
31
- * Writes content to a file.
32
- *
32
+ * Writes content to a file with logging
33
33
  * @async
34
- * @param {string} filePath - The path of the file to write.
35
- * @param {string} content - The content to write to the file.
36
- * @param {Object} logger - The logger object for logging operations.
34
+ * @param {string} filePath - Path to write the file
35
+ * @param {string} content - Content to write
36
+ * @param {Logger} [logger] - Logger instance for operation logging
37
37
  * @returns {Promise<void>}
38
38
  */
39
39
  export async function writeFile(filePath, content, logger) {
@@ -49,3 +49,14 @@ export async function writeFile(filePath, content, logger) {
49
49
  await logger?.error(`Write failed: ${error.message}`, { filePath });
50
50
  }
51
51
  }
52
+
53
+ /**
54
+ * Checks if a file should be versioned based on its extension
55
+ * @param {string} filePath - Path to the file
56
+ * @param {string[]} extensions - List of extensions to match against
57
+ * @returns {boolean} Whether the file should be versioned
58
+ */
59
+ export function shouldVersionFile(filePath, extensions) {
60
+ const ext = getExtension(filePath);
61
+ return extensions.includes(ext);
62
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Hash manager module for tracking file changes
3
+ * @module hashManager
4
+ */
5
+
6
+ import { promises as fs } from 'fs';
7
+ import crypto from 'crypto';
8
+ import { joinPaths } from './pathResolver.js';
9
+
10
+ const HASH_FILE = '.image-hashes.json';
11
+
12
+ /**
13
+ * Hash manager class for tracking file changes and maintaining version history
14
+ * @class
15
+ */
16
+ export default class HashManager {
17
+ /**
18
+ * Creates a new HashManager instance
19
+ * @param {string} rootDir - Root directory for hash file storage
20
+ */
21
+ constructor(rootDir) {
22
+ this.hashFile = joinPaths(rootDir, HASH_FILE);
23
+ this.hashes = new Map();
24
+ this.initialized = false;
25
+ }
26
+
27
+ /**
28
+ * Initializes the hash manager by loading existing hashes from file
29
+ * @async
30
+ * @returns {Promise<void>}
31
+ */
32
+ async initialize() {
33
+ if (this.initialized) return;
34
+
35
+ try {
36
+ const content = await fs.readFile(this.hashFile, 'utf-8');
37
+ const savedHashes = JSON.parse(content);
38
+ Object.entries(savedHashes).forEach(([key, value]) => {
39
+ this.hashes.set(key, value);
40
+ });
41
+ } catch (error) {
42
+ await this.save();
43
+ }
44
+
45
+ this.initialized = true;
46
+ }
47
+
48
+ /**
49
+ * Saves current hashes to the hash file
50
+ * @async
51
+ * @returns {Promise<void>}
52
+ */
53
+ async save() {
54
+ const hashObject = Object.fromEntries(this.hashes);
55
+ await fs.writeFile(this.hashFile, JSON.stringify(hashObject, null, 2));
56
+ }
57
+
58
+ /**
59
+ * Gets the previous hash for a file
60
+ * @param {string} filePath - Path to the file
61
+ * @returns {string|null} Previous hash value or null if not found
62
+ */
63
+ getPreviousHash(filePath) {
64
+ return this.hashes.get(filePath);
65
+ }
66
+
67
+ /**
68
+ * Updates the hash for a file and saves to disk
69
+ * @async
70
+ * @param {string} filePath - Path to the file
71
+ * @param {string} hash - New hash value
72
+ * @returns {Promise<void>}
73
+ */
74
+ async updateHash(filePath, hash) {
75
+ this.hashes.set(filePath, hash);
76
+ await this.save();
77
+ }
78
+
79
+ /**
80
+ * Generates a hash for a file and checks if it changed
81
+ * @async
82
+ * @param {string} filePath - Path to the file
83
+ * @returns {Promise<{hash: string|null, changed: boolean}>} Hash information
84
+ */
85
+ async generateHash(filePath) {
86
+ try {
87
+ // Check file existence
88
+ try {
89
+ await fs.access(filePath);
90
+ } catch (error) {
91
+ // `File not found: ${filePath}`;
92
+ return { hash: null, changed: false };
93
+ }
94
+
95
+ const content = await fs.readFile(filePath);
96
+ const newHash = crypto.createHash('md5').update(content).digest('hex').substring(0, 8);
97
+
98
+ const previousHash = this.getPreviousHash(filePath);
99
+ const changed = previousHash !== newHash;
100
+
101
+ if (changed) {
102
+ await this.updateHash(filePath, newHash);
103
+ }
104
+
105
+ return {
106
+ hash: newHash,
107
+ changed: changed || !previousHash,
108
+ };
109
+ } catch (error) {
110
+ console.error(`Error generating hash for ${filePath}:`, error);
111
+ return { hash: null, changed: false };
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Image utilities module for handling image paths and patterns
3
+ * @module imageUtils
4
+ */
5
+
6
+ // All supported image formats
7
+ export const DEFAULT_IMAGE_EXTENSIONS = ['png', 'jpe?g', 'gif', 'svg', 'webp', 'avif', 'jxl', 'heic', 'heif', 'bmp', 'tiff?'];
8
+
9
+ const IMAGE_EXT_PATTERN = DEFAULT_IMAGE_EXTENSIONS.join('|');
10
+
11
+ export const IMAGE_PATTERNS = {
12
+ css: [
13
+ // url() pattern that handles query parameters separately
14
+ new RegExp(`url\\(['"]?([^'"()\\s?#]+\\.(${IMAGE_EXT_PATTERN}))([?][^'"()\\s]*)?['"]?\\)`, 'gi'),
15
+
16
+ // image-set() pattern with separate query parameter capture
17
+ new RegExp(
18
+ `image-set\\(\\s*(?:['"]([^'"?#]+\\.(${IMAGE_EXT_PATTERN}))([?][^'"()\\s]*)?['"]\\s*(?:type\\(['"]image/[^'"]+['"]\\))?\\s*,?\\s*)+\\)`,
19
+ 'gi',
20
+ ),
21
+
22
+ // Additional pattern for complex background(CSS) declarations
23
+ new RegExp(`(?:^|\\s|,)url\\(['"]?([^'"()\\s?#]+\\.(${IMAGE_EXT_PATTERN}))([?][^'"()\\s]*)?['"]?\\)`, 'gi'),
24
+ ],
25
+ js: [
26
+ // Basic extension-based pattern for all quotes
27
+ new RegExp(`(['"\`])([^'"\`]*?\\.(?:${IMAGE_EXT_PATTERN}))(['"\`])`, 'gi'),
28
+ ],
29
+ };
30
+
31
+ /**
32
+ * Parse URL and query parameters
33
+ * @param {string} url - URL to parse
34
+ * @returns {{path: string, params: URLSearchParams}} Parsed URL parts
35
+ */
36
+ export function parseURL(url) {
37
+ const [path, query] = url.split('?');
38
+ return {
39
+ path,
40
+ params: new URLSearchParams(query || ''),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Updates image reference with version query string
46
+ * @param {string} match - Full matched string
47
+ * @param {string} imagePath - Image path without query parameters
48
+ * @param {string} existingQuery - Existing query string (if any)
49
+ * @param {string} version - Version string to append
50
+ * @returns {string} Updated reference with version
51
+ */
52
+ export function updateImageReference(match, imagePath, existingQuery, version) {
53
+ // Skip data URIs
54
+ if (imagePath.startsWith('data:')) {
55
+ return match;
56
+ }
57
+
58
+ // Add version parameter, replacing any existing version
59
+ const versionedPath = `${imagePath}?v=${version}`;
60
+
61
+ // Replace while preserving the original structure
62
+ return match.replace(imagePath + (existingQuery || ''), versionedPath);
63
+ }
@@ -1,5 +1,5 @@
1
1
  import fs from 'fs/promises';
2
- import path from 'path';
2
+ import { joinPaths } from './pathResolver.js';
3
3
 
4
4
  class Logger {
5
5
  static LOG_LEVELS = {
@@ -59,9 +59,7 @@ class Logger {
59
59
  this.logLevel = Logger.LOG_LEVELS[normalizedLevel];
60
60
  console.log(`Log level set to "${normalizedLevel}"`);
61
61
  } else {
62
- console.warn(
63
- `Invalid log level "${level}". Using default level "${Logger.DEFAULT_LOG_LEVEL}".`,
64
- );
62
+ console.warn(`Invalid log level "${level}". Using default level "${Logger.DEFAULT_LOG_LEVEL}".`);
65
63
  this.logLevel = Logger.LOG_LEVELS[Logger.DEFAULT_LOG_LEVEL];
66
64
  }
67
65
  }
@@ -131,7 +129,7 @@ class Logger {
131
129
  }
132
130
 
133
131
  getLogFilePath() {
134
- return path.join(this.logDir, `log-${this.currentDate}.log`);
132
+ return joinPaths(this.logDir, `log-${this.currentDate}.log`);
135
133
  }
136
134
 
137
135
  getCurrentDate() {
@@ -152,7 +150,7 @@ class Logger {
152
150
  const files = await fs.readdir(this.logDir);
153
151
  const now = new Date();
154
152
  for (const file of files) {
155
- const filePath = path.join(this.logDir, file);
153
+ const filePath = joinPaths(this.logDir, file);
156
154
  const stats = await fs.stat(filePath);
157
155
  const diffDays = (now - stats.mtime) / (1000 * 60 * 60 * 24);
158
156
  if (diffDays > this.retentionDays) {
@@ -8,10 +8,9 @@ import CleanCSS from 'clean-css';
8
8
 
9
9
  /**
10
10
  * Minifies JavaScript content.
11
- *
12
- * @param {string} content - The JavaScript content to minify.
13
- * @param {Object} options - Options for JavaScript minification.
14
- * @returns {string} The minified JavaScript content.
11
+ * @param {string} content - JavaScript content to minify.
12
+ * @param {Object} [options={}] - UglifyJS options.
13
+ * @returns {string} Minified JavaScript code.
15
14
  */
16
15
  export function minifyJS(content, options = {}) {
17
16
  const defaultOptions = {
@@ -30,11 +29,10 @@ export function minifyJS(content, options = {}) {
30
29
 
31
30
  /**
32
31
  * Minifies CSS content.
33
- *
34
32
  * @async
35
- * @param {string} content - The CSS content to minify.
36
- * @param {Object} options - Options for CSS minification.
37
- * @returns {Promise<Object>} A promise that resolves to the minification result.
33
+ * @param {string} content - CSS content to minify.
34
+ * @param {Object} [options={}] - Clean-CSS options.
35
+ * @returns {Promise<{styles: string, warnings: string[]}>} Minified CSS and warnings.
38
36
  */
39
37
  export function minifyCSS(content, options = {}) {
40
38
  const defaultOptions = {