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/CHANGELOG.md +26 -0
- package/README.md +214 -127
- package/demo.js +3 -0
- package/dist/module.js +185 -93
- package/dist/modules/fileHandler.js +21 -10
- package/dist/modules/hashManager.js +114 -0
- package/dist/modules/imageUtils.js +63 -0
- package/dist/modules/logger.js +4 -6
- package/dist/modules/minifier.js +6 -8
- package/dist/modules/pathResolver.js +100 -0
- package/logs/log-02-10-2025.log +12 -0
- package/logs/log-02-11-2025.log +1094 -0
- package/logs/log-02-13-2025.log +317 -0
- package/package.json +1 -1
- package/test/img/sample.jpg +0 -0
- package/test/img/samples.svg +10 -0
- package/test/test.css +72 -6
- package/test/test.js +9 -2
- package/logs/log-08-20-2024.log +0 -65
- package/logs/log-08-21-2024.log +0 -80
- package/logs/log-08-22-2024.log +0 -8
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
|
|
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 {
|
|
13
|
-
import {
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
*
|
|
103
|
+
* Updates image references in a file with version query strings.
|
|
79
104
|
* @async
|
|
80
|
-
* @param {string} filePath -
|
|
81
|
-
* @param {
|
|
82
|
-
* @param {
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
const handler = FILE_HANDLERS[fileExtension];
|
|
121
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
122
|
+
let modified = false;
|
|
90
123
|
|
|
91
|
-
|
|
92
|
-
await
|
|
93
|
-
|
|
94
|
-
|
|
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('
|
|
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
|
|
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
|
|
171
|
-
* @property {boolean|LogOptions} [useLog=true] - Whether to use logging
|
|
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
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* @param {string} contentPath - The path to
|
|
180
|
-
* @param {MinifyOptions} [options={}] -
|
|
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', {
|
|
278
|
+
await logger.info('Starting minification process', {
|
|
279
|
+
contentPath,
|
|
280
|
+
excludeFolder,
|
|
281
|
+
useBabel,
|
|
282
|
+
useVersioning: !!useVersioning,
|
|
283
|
+
});
|
|
199
284
|
}
|
|
200
285
|
|
|
201
|
-
const rootDir =
|
|
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 =
|
|
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
|
|
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 -
|
|
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 =
|
|
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 -
|
|
35
|
-
* @param {string} content -
|
|
36
|
-
* @param {
|
|
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
|
+
}
|
package/dist/modules/logger.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
-
import
|
|
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
|
|
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 =
|
|
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) {
|
package/dist/modules/minifier.js
CHANGED
|
@@ -8,10 +8,9 @@ import CleanCSS from 'clean-css';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Minifies JavaScript content.
|
|
11
|
-
*
|
|
12
|
-
* @param {
|
|
13
|
-
* @
|
|
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 -
|
|
36
|
-
* @param {Object} options -
|
|
37
|
-
* @returns {Promise<
|
|
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 = {
|