uglify-js-minify-css-allfiles 2.2.3 → 2.3.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/CHANGELOG.md +39 -0
- package/README.md +214 -127
- package/demo.js +3 -0
- package/dist/module.js +199 -95
- 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 +222 -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,114 +2,211 @@
|
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
if (fileExt === '.js') {
|
|
39
|
+
const newHash = crypto.randomBytes(16).toString('hex').substring(0, 8);
|
|
40
|
+
|
|
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
|
+
}
|
|
55
|
+
|
|
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;
|
|
60
|
+
}
|
|
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
|
+
await logger?.warn('Failed to generate hash, keeping original URL', {
|
|
79
|
+
file: filePath,
|
|
80
|
+
image: imagePath + ext,
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (changed) {
|
|
86
|
+
modified = true;
|
|
87
|
+
await logger?.info('Updated CSS image version', {
|
|
88
|
+
file: filePath,
|
|
89
|
+
image: imagePath + ext,
|
|
90
|
+
oldHash: hashManager.getPreviousHash(absoluteImagePath),
|
|
91
|
+
newHash: hash,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
newContent = newContent.replace(`?v=${marker}`, `?v=${hash}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await Promise.all(promises.filter((p) => p instanceof Promise));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { content: newContent, modified };
|
|
103
|
+
}
|
|
35
104
|
|
|
36
105
|
/**
|
|
37
|
-
*
|
|
38
|
-
* @
|
|
39
|
-
* @param {string}
|
|
40
|
-
* @param {
|
|
41
|
-
* @param {
|
|
106
|
+
* Updates image references in a file with version query strings.
|
|
107
|
+
* @async
|
|
108
|
+
* @param {string} filePath - Path to the file to process.
|
|
109
|
+
* @param {Object} versioningOptions - Options for versioning.
|
|
110
|
+
* @param {string[]} [versioningOptions.extensions] - List of file extensions to version.
|
|
111
|
+
* @param {Logger} logger - Logger instance.
|
|
112
|
+
* @param {HashManager} hashManager - Hash manager instance.
|
|
42
113
|
* @returns {Promise<void>}
|
|
43
114
|
*/
|
|
115
|
+
async function updateImageReferences(filePath, versioningOptions, logger, hashManager) {
|
|
116
|
+
const { extensions } = versioningOptions;
|
|
117
|
+
const targetExtensions =
|
|
118
|
+
extensions || DEFAULT_IMAGE_EXTENSIONS.map((ext) => (ext === 'jpe?g' ? ['.jpg', '.jpeg'] : ['.' + ext.replace('?', '')])).flat();
|
|
44
119
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
transformed = transformSync(content, options.babelOptions).code;
|
|
57
|
-
}
|
|
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 });
|
|
120
|
+
const fileExt = getExtension(filePath);
|
|
121
|
+
const patterns = IMAGE_PATTERNS[fileExt.substring(1)] || [];
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
125
|
+
let modified = false;
|
|
126
|
+
|
|
127
|
+
for (const pattern of patterns) {
|
|
128
|
+
const result = await processPattern(pattern, content, fileExt, filePath, logger, hashManager, targetExtensions);
|
|
129
|
+
content = result.content;
|
|
130
|
+
modified = modified || result.modified;
|
|
62
131
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
await writeFile(filePath, output.styles, logger);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
await logger?.error('CSS minification failed', { filePath, error: error.message });
|
|
132
|
+
|
|
133
|
+
if (modified) {
|
|
134
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
135
|
+
await logger?.info('Updated file with versioned image references', {
|
|
136
|
+
file: filePath,
|
|
137
|
+
});
|
|
73
138
|
}
|
|
74
|
-
}
|
|
75
|
-
|
|
139
|
+
} catch (error) {
|
|
140
|
+
await logger?.error('Failed to process file', {
|
|
141
|
+
file: filePath,
|
|
142
|
+
error: error.message,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolves Babel options based on the provided configuration.
|
|
149
|
+
* @param {boolean|BabelOptions} useBabel - The Babel options object or boolean.
|
|
150
|
+
* @returns {Promise<BabelOptions|null>} - A promise that resolves to the Babel options or null if disabled.
|
|
151
|
+
*/
|
|
152
|
+
async function resolveBabelOptions(useBabel) {
|
|
153
|
+
if (!useBabel) return null;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const presetEnvUrl = resolveModulePath('@babel/preset-env');
|
|
157
|
+
const presetEnv = await import(presetEnvUrl);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
presets: [[presetEnv.default, typeof useBabel === 'object' ? useBabel : {}]],
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('Error loading @babel/preset-env:', error);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
76
167
|
|
|
77
168
|
/**
|
|
78
169
|
* Processes a single file based on its extension.
|
|
79
170
|
* @async
|
|
80
171
|
* @param {string} filePath - The path of the file to process.
|
|
81
172
|
* @param {Logger} logger - The logger instance.
|
|
82
|
-
* @param {
|
|
173
|
+
* @param {Object} options - Processing options.
|
|
174
|
+
* @param {BabelOptions} [options.babelOptions] - Babel transformation options.
|
|
175
|
+
* @param {Object} [options.jsMinifyOptions] - JavaScript minification options.
|
|
176
|
+
* @param {Object} [options.cssMinifyOptions] - CSS minification options.
|
|
83
177
|
* @returns {Promise<void>}
|
|
84
178
|
*/
|
|
85
179
|
async function processFile(filePath, logger, options) {
|
|
86
180
|
try {
|
|
87
181
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
88
|
-
const fileExtension =
|
|
89
|
-
const handler = FILE_HANDLERS[fileExtension];
|
|
182
|
+
const fileExtension = getExtension(filePath);
|
|
90
183
|
|
|
91
|
-
|
|
92
|
-
|
|
184
|
+
let result;
|
|
185
|
+
if (fileExtension === '.js') {
|
|
186
|
+
let transformed = fileContent;
|
|
187
|
+
if (options.babelOptions) {
|
|
188
|
+
const babelCoreUrl = resolveModulePath('@babel/core');
|
|
189
|
+
const { transformSync } = await import(babelCoreUrl);
|
|
190
|
+
transformed = transformSync(fileContent, options.babelOptions).code;
|
|
191
|
+
}
|
|
192
|
+
result = minifyJS(transformed, options.jsMinifyOptions);
|
|
193
|
+
} else if (fileExtension === '.css') {
|
|
194
|
+
const output = await minifyCSS(fileContent, options.cssMinifyOptions);
|
|
195
|
+
if (output.warnings.length > 0) {
|
|
196
|
+
await logger?.warn('CSS minification warnings', { filePath, warnings: output.warnings });
|
|
197
|
+
}
|
|
198
|
+
result = output.styles;
|
|
93
199
|
} else {
|
|
94
200
|
await logger?.info(`Unsupported file type, skipping: ${filePath}`);
|
|
201
|
+
return;
|
|
95
202
|
}
|
|
203
|
+
|
|
204
|
+
await writeFile(filePath, result, logger);
|
|
96
205
|
} catch (error) {
|
|
97
206
|
await logger?.error('Error processing file', { filePath, error: error.message });
|
|
98
207
|
}
|
|
99
208
|
}
|
|
100
209
|
|
|
101
|
-
/**
|
|
102
|
-
* Resolves Babel options based on the provided configuration.
|
|
103
|
-
* @param {boolean|BabelOptions} useBabel - The Babel options object or boolean.
|
|
104
|
-
* @returns {BabelOptions|null} The resolved Babel options or null if no valid options are provided.
|
|
105
|
-
*/
|
|
106
|
-
function resolveBabelOptions(useBabel) {
|
|
107
|
-
if (!useBabel) return null;
|
|
108
|
-
return {
|
|
109
|
-
presets: [['@babel/preset-env', typeof useBabel === 'object' ? useBabel : {}]],
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
210
|
/**
|
|
114
211
|
* Options for Babel configuration.
|
|
115
212
|
* @typedef {Object} BabelOptions
|
|
@@ -158,39 +255,44 @@ function resolveBabelOptions(useBabel) {
|
|
|
158
255
|
* Options for minification configuration.
|
|
159
256
|
* @typedef {Object} MinifyOptions
|
|
160
257
|
* @property {string} [excludeFolder=''] - Folder to exclude from minification.
|
|
161
|
-
* @property {boolean|BabelOptions} [useBabel=false] - Whether to use Babel for transformation
|
|
162
|
-
* @property {boolean|LogOptions} [useLog=true] - Whether to use logging
|
|
258
|
+
* @property {boolean|BabelOptions} [useBabel=false] - Whether to use Babel for transformation.
|
|
259
|
+
* @property {boolean|LogOptions} [useLog=true] - Whether to use logging.
|
|
163
260
|
* @property {JSMinifyOptions} [jsMinifyOptions={}] - Options for JavaScript minification.
|
|
164
261
|
* @property {CSSMinifyOptions} [cssMinifyOptions={}] - Options for CSS minification.
|
|
262
|
+
* @property {string[]|null} [useVersioning=null] - Options for file versioning.
|
|
165
263
|
*/
|
|
166
264
|
|
|
167
265
|
/**
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* @param {string} contentPath - The path to
|
|
171
|
-
* @param {MinifyOptions} [options={}] -
|
|
172
|
-
* @returns {Promise<void>} A promise that resolves when all files have been processed.
|
|
266
|
+
* Main function to minify all files and handle versioning.
|
|
267
|
+
* @async
|
|
268
|
+
* @param {string} contentPath - The path to process files from.
|
|
269
|
+
* @param {MinifyOptions} [options={}] - Configuration options.
|
|
270
|
+
* @returns {Promise<void>} - A promise that resolves when all files have been processed.
|
|
173
271
|
* @throws {Error} If there's an issue reading or writing files.
|
|
174
272
|
*/
|
|
175
273
|
export default async function minifyAll(contentPath, options = {}) {
|
|
176
|
-
const {
|
|
177
|
-
excludeFolder = '',
|
|
178
|
-
useBabel = false,
|
|
179
|
-
useLog = true,
|
|
180
|
-
jsMinifyOptions = {},
|
|
181
|
-
cssMinifyOptions = {},
|
|
182
|
-
} = options;
|
|
274
|
+
const { excludeFolder = '', useBabel = false, useLog = true, jsMinifyOptions = {}, cssMinifyOptions = {}, useVersioning = null } = options;
|
|
183
275
|
|
|
184
276
|
let logger = null;
|
|
185
277
|
if (useLog) {
|
|
186
278
|
const logOptions = typeof useLog === 'object' ? useLog : {};
|
|
187
279
|
logger = new Logger(logOptions);
|
|
188
280
|
await logger.initialize();
|
|
189
|
-
await logger.info('Starting minification process', {
|
|
281
|
+
await logger.info('Starting minification process', {
|
|
282
|
+
contentPath,
|
|
283
|
+
excludeFolder,
|
|
284
|
+
useBabel,
|
|
285
|
+
useVersioning: !!useVersioning,
|
|
286
|
+
});
|
|
190
287
|
}
|
|
191
288
|
|
|
192
|
-
const rootDir =
|
|
193
|
-
const babelOptions = resolveBabelOptions(useBabel);
|
|
289
|
+
const rootDir = resolvePath(contentPath || '');
|
|
290
|
+
const babelOptions = await resolveBabelOptions(useBabel);
|
|
291
|
+
const hashManager = useVersioning ? new HashManager(rootDir) : null;
|
|
292
|
+
|
|
293
|
+
if (hashManager) {
|
|
294
|
+
await hashManager.initialize();
|
|
295
|
+
}
|
|
194
296
|
|
|
195
297
|
const processOptions = {
|
|
196
298
|
babelOptions,
|
|
@@ -200,17 +302,19 @@ export default async function minifyAll(contentPath, options = {}) {
|
|
|
200
302
|
|
|
201
303
|
try {
|
|
202
304
|
await getAllFiles(rootDir, async (filePath) => {
|
|
203
|
-
const relativePath =
|
|
204
|
-
if (
|
|
205
|
-
excludeFolder &&
|
|
206
|
-
(relativePath.startsWith(excludeFolder) ||
|
|
207
|
-
relativePath.includes(path.sep + excludeFolder + path.sep))
|
|
208
|
-
) {
|
|
305
|
+
const relativePath = makeRelativePath(filePath, rootDir);
|
|
306
|
+
if (excludeFolder && containsFolder(relativePath, excludeFolder)) {
|
|
209
307
|
await logger?.debug('Skipping excluded file', { filePath });
|
|
210
308
|
return;
|
|
211
309
|
}
|
|
212
310
|
|
|
213
311
|
await processFile(filePath, logger, processOptions);
|
|
312
|
+
|
|
313
|
+
// Apply versioning after successful processing if enabled
|
|
314
|
+
if (useVersioning && hashManager) {
|
|
315
|
+
await updateImageReferences(filePath, useVersioning, logger, hashManager);
|
|
316
|
+
}
|
|
317
|
+
|
|
214
318
|
logger?.incrementProcessedFiles(filePath);
|
|
215
319
|
});
|
|
216
320
|
} 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
|
+
console.warn(`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 = {
|