ultimate-jekyll-manager 0.0.92 → 0.0.94
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/commands/minify-html.js +19 -0
- package/dist/gulp/tasks/minifyHtml.js +254 -70
- package/dist/gulp/tasks/utils/BU/minifyHtml.js +183 -0
- package/dist/gulp/tasks/utils/BU/minifyHtml.worker.js +72 -0
- package/dist/gulp/tasks/utils/github-cache.js +88 -64
- package/firebase-debug.log +8 -0
- package/package.json +2 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Libraries
|
|
2
|
+
const Manager = new (require('../build.js'));
|
|
3
|
+
const logger = Manager.logger('minify');
|
|
4
|
+
const { execute } = require('node-powertools');
|
|
5
|
+
|
|
6
|
+
// Load package
|
|
7
|
+
const package = Manager.getPackage('main');
|
|
8
|
+
const project = Manager.getPackage('project');
|
|
9
|
+
|
|
10
|
+
module.exports = async function (options) {
|
|
11
|
+
// Log
|
|
12
|
+
logger.log(`Starting minify...`);
|
|
13
|
+
|
|
14
|
+
// Build environment variables with all options
|
|
15
|
+
const envVars = `UJ_MINIFY_HTML_FORCE=true`;
|
|
16
|
+
|
|
17
|
+
// Run the full build process with minify force enabled
|
|
18
|
+
await execute(`${envVars} bundle exec npm run gulp -- minifyHtml`, { log: true })
|
|
19
|
+
};
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
const Manager = new (require('../../build.js'));
|
|
3
3
|
const logger = Manager.logger('minifyHtml');
|
|
4
4
|
const { src, dest, series } = require('gulp');
|
|
5
|
-
const { minify } = require('html
|
|
5
|
+
const { minify: minifyRust } = require('@minify-html/node');
|
|
6
|
+
const { minify: minifyJs } = require('terser');
|
|
6
7
|
const through2 = require('through2');
|
|
7
8
|
|
|
8
9
|
// Load package
|
|
@@ -19,6 +20,33 @@ const input = [
|
|
|
19
20
|
];
|
|
20
21
|
const output = '_site';
|
|
21
22
|
|
|
23
|
+
// Helper function to minify a single file's content using Rust-based minifier
|
|
24
|
+
async function minifyFileContent(htmlContent, options, filePath) {
|
|
25
|
+
// Extract and temporarily replace JSON-LD scripts
|
|
26
|
+
const { content: contentAfterJsonLd, extracted: jsonLdScripts } = extractJsonLdScripts(htmlContent);
|
|
27
|
+
|
|
28
|
+
// Extract and temporarily replace inline scripts (minified with Terser)
|
|
29
|
+
const { content: contentAfterScripts, extracted: inlineScripts } = await extractInlineScripts(contentAfterJsonLd, filePath);
|
|
30
|
+
|
|
31
|
+
// Extract and temporarily replace IE conditional comments
|
|
32
|
+
const { content: contentAfterComments, extracted: conditionalComments } = extractConditionalComments(contentAfterScripts);
|
|
33
|
+
|
|
34
|
+
// Minify the HTML content using Rust-based minifier (synchronous, much faster)
|
|
35
|
+
const minifiedBuffer = minifyRust(Buffer.from(contentAfterComments), options);
|
|
36
|
+
const minified = minifiedBuffer.toString();
|
|
37
|
+
|
|
38
|
+
// Restore the conditional comments
|
|
39
|
+
let finalHtml = restoreConditionalComments(minified, conditionalComments);
|
|
40
|
+
|
|
41
|
+
// Restore the inline scripts
|
|
42
|
+
finalHtml = restoreInlineScripts(finalHtml, inlineScripts);
|
|
43
|
+
|
|
44
|
+
// Restore the JSON-LD scripts
|
|
45
|
+
finalHtml = restoreJsonLdScripts(finalHtml, jsonLdScripts);
|
|
46
|
+
|
|
47
|
+
return finalHtml;
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
// Main task
|
|
23
51
|
function minifyHtmlTask(complete) {
|
|
24
52
|
// Check if we should minify
|
|
@@ -33,92 +61,248 @@ function minifyHtmlTask(complete) {
|
|
|
33
61
|
logger.log('Starting...');
|
|
34
62
|
Manager.logMemory(logger, 'Start');
|
|
35
63
|
|
|
36
|
-
// Configure minify options
|
|
64
|
+
// Configure minify options for @minify-html/node (Rust-based)
|
|
65
|
+
// NOTE: Inline scripts are extracted before minification to avoid bugs in minify-js
|
|
37
66
|
const options = {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
minifyCSS: true,
|
|
48
|
-
minifyJS: true
|
|
67
|
+
keep_closing_tags: false,
|
|
68
|
+
keep_comments: false,
|
|
69
|
+
keep_html_and_head_opening_tags: false,
|
|
70
|
+
keep_spaces_between_attributes: false,
|
|
71
|
+
keep_ssi_comments: false,
|
|
72
|
+
minify_css: true,
|
|
73
|
+
minify_js: false, // Disabled - inline scripts are extracted, so nothing to minify
|
|
74
|
+
remove_bangs: false,
|
|
75
|
+
remove_processing_instructions: false
|
|
49
76
|
};
|
|
50
77
|
|
|
78
|
+
// Get concurrency limit from environment or use default
|
|
79
|
+
const CONCURRENCY_LIMIT = parseInt(process.env.UJ_MINIFY_CONCURRENCY || '1', 10);
|
|
80
|
+
logger.log(`Concurrency: ${CONCURRENCY_LIMIT} files at a time`);
|
|
81
|
+
|
|
82
|
+
// Collect files for batch processing
|
|
83
|
+
const fileQueue = [];
|
|
84
|
+
const processed = { count: 0 };
|
|
85
|
+
|
|
51
86
|
// Process HTML files
|
|
52
87
|
return src(input)
|
|
53
|
-
.pipe(through2.obj(
|
|
88
|
+
.pipe(through2.obj(function(file, _enc, callback) {
|
|
54
89
|
if (file.isBuffer()) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
conditionalComments.forEach((commentContent, index) => {
|
|
103
|
-
finalHtml = finalHtml.replace(`__CONDITIONAL_COMMENT_PLACEHOLDER_${index}__`, commentContent);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
file.contents = Buffer.from(finalHtml);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
logger.error(`Error minifying ${file.path}: ${err.message}`);
|
|
90
|
+
fileQueue.push({ file });
|
|
91
|
+
callback();
|
|
92
|
+
} else {
|
|
93
|
+
callback(null, file);
|
|
94
|
+
}
|
|
95
|
+
}, async function(callback) {
|
|
96
|
+
// This function is called when all files have been queued
|
|
97
|
+
if (fileQueue.length === 0) {
|
|
98
|
+
logger.log('No HTML files to minify');
|
|
99
|
+
return callback();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const totalFiles = fileQueue.length;
|
|
103
|
+
logger.log(`Minifying ${totalFiles} HTML files...`);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Process files in batches
|
|
107
|
+
for (let i = 0; i < fileQueue.length; i += CONCURRENCY_LIMIT) {
|
|
108
|
+
const batch = fileQueue.slice(i, i + CONCURRENCY_LIMIT);
|
|
109
|
+
|
|
110
|
+
// Process batch in parallel
|
|
111
|
+
const processedFiles = await Promise.all(
|
|
112
|
+
batch.map(async ({ file }) => {
|
|
113
|
+
try {
|
|
114
|
+
const htmlContent = file.contents.toString();
|
|
115
|
+
const finalHtml = await minifyFileContent(htmlContent, options, file.path);
|
|
116
|
+
file.contents = Buffer.from(finalHtml);
|
|
117
|
+
processed.count++;
|
|
118
|
+
|
|
119
|
+
// Log progress every 50 files or on last file
|
|
120
|
+
if (processed.count % 50 === 0 || processed.count === totalFiles) {
|
|
121
|
+
const percentage = ((processed.count / totalFiles) * 100).toFixed(1);
|
|
122
|
+
logger.log(`Progress: ${processed.count}/${totalFiles} files (${percentage}%)`);
|
|
123
|
+
Manager.logMemory(logger, `After ${processed.count} files`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return file;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
logger.error(`Error minifying ${file.path}: ${err.message}`);
|
|
129
|
+
return file;
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Push processed files to the stream
|
|
135
|
+
processedFiles.forEach(file => this.push(file));
|
|
109
136
|
}
|
|
137
|
+
|
|
138
|
+
callback();
|
|
139
|
+
} catch (err) {
|
|
140
|
+
logger.error(`Batch processing error: ${err.message}`);
|
|
141
|
+
callback(err);
|
|
110
142
|
}
|
|
111
|
-
callback(null, file);
|
|
112
143
|
}))
|
|
113
144
|
.pipe(dest(output))
|
|
114
145
|
.on('finish', () => {
|
|
115
146
|
// Log
|
|
116
147
|
logger.log('Finished!');
|
|
148
|
+
Manager.logMemory(logger, 'End');
|
|
117
149
|
|
|
118
150
|
// Complete
|
|
119
|
-
|
|
151
|
+
complete();
|
|
120
152
|
});
|
|
121
153
|
}
|
|
122
154
|
|
|
155
|
+
// Helper: Extract JSON-LD scripts and replace with placeholders
|
|
156
|
+
function extractJsonLdScripts(htmlContent) {
|
|
157
|
+
const extracted = [];
|
|
158
|
+
// Match both quoted and unquoted type attributes (minifier removes quotes)
|
|
159
|
+
const jsonLdRegex = /<script[^>]*type=(?:["']?application\/ld\+json["']?)[^>]*>([\s\S]*?)<\/script>/gi;
|
|
160
|
+
|
|
161
|
+
const content = htmlContent.replace(jsonLdRegex, (match, jsonContent) => {
|
|
162
|
+
// Minify the JSON content
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(jsonContent);
|
|
165
|
+
const minifiedJson = JSON.stringify(parsed);
|
|
166
|
+
extracted.push(minifiedJson);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
extracted.push(jsonContent);
|
|
169
|
+
}
|
|
170
|
+
return `__JSON_LD_PLACEHOLDER_${extracted.length - 1}__`;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return { content, extracted };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Helper: Restore JSON-LD scripts from placeholders
|
|
177
|
+
function restoreJsonLdScripts(htmlContent, jsonLdScripts) {
|
|
178
|
+
let content = htmlContent;
|
|
179
|
+
|
|
180
|
+
jsonLdScripts.forEach((jsonContent, index) => {
|
|
181
|
+
const scriptTag = `<script type=application/ld+json>${jsonContent}</script>`;
|
|
182
|
+
content = content.replace(`__JSON_LD_PLACEHOLDER_${index}__`, scriptTag);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return content;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Helper: Extract inline scripts, minify with Terser, and replace with placeholders
|
|
189
|
+
async function extractInlineScripts(htmlContent, filePath) {
|
|
190
|
+
const extracted = [];
|
|
191
|
+
const scripts = [];
|
|
192
|
+
|
|
193
|
+
// Match <script> tags that are NOT application/ld+json (those are already extracted)
|
|
194
|
+
// This regex excludes external scripts (those with src attribute)
|
|
195
|
+
// Handles both quoted and unquoted type attributes (minifier removes quotes)
|
|
196
|
+
const scriptRegex = /<script(?![^>]*type=(?:["']?application\/ld\+json["']?))(?![^>]*src=)([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
197
|
+
|
|
198
|
+
// First pass: collect all scripts and create placeholders
|
|
199
|
+
const content = htmlContent.replace(scriptRegex, (fullMatch, attributes, jsCode) => {
|
|
200
|
+
const index = scripts.length;
|
|
201
|
+
scripts.push({ fullMatch, attributes, jsCode });
|
|
202
|
+
return `__INLINE_SCRIPT_PLACEHOLDER_${index}__`;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Second pass: minify all scripts in parallel
|
|
206
|
+
const minifyPromises = scripts.map(async ({ fullMatch, attributes, jsCode }, scriptIndex) => {
|
|
207
|
+
// Skip empty scripts
|
|
208
|
+
if (!jsCode.trim()) {
|
|
209
|
+
return fullMatch;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Try to minify the JavaScript with Terser
|
|
213
|
+
try {
|
|
214
|
+
const minified = await minifyJs(jsCode, {
|
|
215
|
+
compress: {
|
|
216
|
+
dead_code: true,
|
|
217
|
+
drop_console: false,
|
|
218
|
+
drop_debugger: true,
|
|
219
|
+
keep_classnames: false,
|
|
220
|
+
keep_fargs: true,
|
|
221
|
+
keep_fnames: false,
|
|
222
|
+
keep_infinity: false,
|
|
223
|
+
},
|
|
224
|
+
mangle: false, // Don't mangle variable names to avoid breaking code
|
|
225
|
+
format: {
|
|
226
|
+
comments: false,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (minified && minified.code) {
|
|
231
|
+
return `<script${attributes}>${minified.code}</script>`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return fullMatch;
|
|
235
|
+
} catch (err) {
|
|
236
|
+
// Minification failed - use original and log detailed error
|
|
237
|
+
const preview = jsCode.length > 100 ? jsCode.substring(0, 100) + '...' : jsCode;
|
|
238
|
+
const lines = jsCode.split('\n');
|
|
239
|
+
|
|
240
|
+
logger.error(`Failed to minify inline script in ${filePath}`);
|
|
241
|
+
logger.error(` Script #${scriptIndex + 1} (${lines.length} lines)`);
|
|
242
|
+
logger.error(` Error: ${err.message}`);
|
|
243
|
+
|
|
244
|
+
if (err.line !== undefined) {
|
|
245
|
+
logger.error(` Line ${err.line}, Column ${err.col || '?'}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
logger.error(` Preview: ${preview.replace(/\n/g, ' ')}`);
|
|
249
|
+
|
|
250
|
+
return fullMatch;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Wait for all minification to complete
|
|
255
|
+
const minifiedScripts = await Promise.all(minifyPromises);
|
|
256
|
+
|
|
257
|
+
// Add all minified scripts to extracted array
|
|
258
|
+
minifiedScripts.forEach(script => extracted.push(script));
|
|
259
|
+
|
|
260
|
+
return { content, extracted };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Helper: Restore inline scripts from placeholders
|
|
264
|
+
function restoreInlineScripts(htmlContent, inlineScripts) {
|
|
265
|
+
let content = htmlContent;
|
|
266
|
+
|
|
267
|
+
inlineScripts.forEach((scriptContent, index) => {
|
|
268
|
+
content = content.replace(`__INLINE_SCRIPT_PLACEHOLDER_${index}__`, scriptContent);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return content;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Helper: Extract IE conditional comments and replace with placeholders
|
|
275
|
+
function extractConditionalComments(htmlContent) {
|
|
276
|
+
const extracted = [];
|
|
277
|
+
const conditionalRegex = /<!--\[if[^>]*\]>([\s\S]*?)<!\[endif\]-->/gi;
|
|
278
|
+
|
|
279
|
+
const content = htmlContent.replace(conditionalRegex, (match, commentContent) => {
|
|
280
|
+
// Minify the content inside the conditional comment
|
|
281
|
+
try {
|
|
282
|
+
const minifiedContent = commentContent
|
|
283
|
+
.replace(/\s+/g, ' ')
|
|
284
|
+
.replace(/>\s+</g, '><')
|
|
285
|
+
.trim();
|
|
286
|
+
extracted.push(match.replace(commentContent, minifiedContent));
|
|
287
|
+
} catch (e) {
|
|
288
|
+
extracted.push(match);
|
|
289
|
+
}
|
|
290
|
+
return `__CONDITIONAL_COMMENT_PLACEHOLDER_${extracted.length - 1}__`;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return { content, extracted };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Helper: Restore IE conditional comments from placeholders
|
|
297
|
+
function restoreConditionalComments(htmlContent, conditionalComments) {
|
|
298
|
+
let content = htmlContent;
|
|
299
|
+
|
|
300
|
+
conditionalComments.forEach((commentContent, index) => {
|
|
301
|
+
content = content.replace(`__CONDITIONAL_COMMENT_PLACEHOLDER_${index}__`, commentContent);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return content;
|
|
305
|
+
}
|
|
306
|
+
|
|
123
307
|
// Default Task (no watcher for minifyHtml as it runs after Jekyll build)
|
|
124
308
|
module.exports = minifyHtmlTask;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Libraries
|
|
2
|
+
const Manager = new (require('../../build.js'));
|
|
3
|
+
const logger = Manager.logger('minifyHtml');
|
|
4
|
+
const { src, dest, series } = require('gulp');
|
|
5
|
+
const { minify } = require('html-minifier-terser');
|
|
6
|
+
const through2 = require('through2');
|
|
7
|
+
|
|
8
|
+
// Load package
|
|
9
|
+
const package = Manager.getPackage('main');
|
|
10
|
+
const project = Manager.getPackage('project');
|
|
11
|
+
const config = Manager.getConfig('project');
|
|
12
|
+
const rootPathPackage = Manager.getRootPath('main');
|
|
13
|
+
const rootPathProject = Manager.getRootPath('project');
|
|
14
|
+
|
|
15
|
+
// Glob
|
|
16
|
+
const input = [
|
|
17
|
+
// Files to include
|
|
18
|
+
'_site/**/*.html',
|
|
19
|
+
];
|
|
20
|
+
const output = '_site';
|
|
21
|
+
|
|
22
|
+
// Helper function to minify a single file's content
|
|
23
|
+
async function minifyFileContent(htmlContent, options) {
|
|
24
|
+
// Extract and temporarily replace JSON-LD scripts
|
|
25
|
+
const jsonLdScripts = [];
|
|
26
|
+
const jsonLdRegex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
27
|
+
|
|
28
|
+
htmlContent = htmlContent.replace(jsonLdRegex, (match, jsonContent) => {
|
|
29
|
+
// Minify the JSON content
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(jsonContent);
|
|
32
|
+
const minifiedJson = JSON.stringify(parsed);
|
|
33
|
+
jsonLdScripts.push(minifiedJson);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
jsonLdScripts.push(jsonContent);
|
|
36
|
+
}
|
|
37
|
+
return `__JSON_LD_PLACEHOLDER_${jsonLdScripts.length - 1}__`;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Extract and temporarily replace IE conditional comments
|
|
41
|
+
const conditionalComments = [];
|
|
42
|
+
const conditionalRegex = /<!--\[if[^>]*\]>([\s\S]*?)<!\[endif\]-->/gi;
|
|
43
|
+
|
|
44
|
+
htmlContent = htmlContent.replace(conditionalRegex, (match, content) => {
|
|
45
|
+
// Minify the content inside the conditional comment
|
|
46
|
+
try {
|
|
47
|
+
const minifiedContent = content
|
|
48
|
+
.replace(/\s+/g, ' ')
|
|
49
|
+
.replace(/>\s+</g, '><')
|
|
50
|
+
.trim();
|
|
51
|
+
conditionalComments.push(match.replace(content, minifiedContent));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
conditionalComments.push(match);
|
|
54
|
+
}
|
|
55
|
+
return `__CONDITIONAL_COMMENT_PLACEHOLDER_${conditionalComments.length - 1}__`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Minify the HTML content
|
|
59
|
+
const minified = await minify(htmlContent, options);
|
|
60
|
+
|
|
61
|
+
// Restore the JSON-LD scripts and conditional comments
|
|
62
|
+
let finalHtml = minified;
|
|
63
|
+
jsonLdScripts.forEach((jsonContent, index) => {
|
|
64
|
+
const scriptTag = `<script type=application/ld+json>${jsonContent}</script>`;
|
|
65
|
+
finalHtml = finalHtml.replace(`__JSON_LD_PLACEHOLDER_${index}__`, scriptTag);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
conditionalComments.forEach((commentContent, index) => {
|
|
69
|
+
finalHtml = finalHtml.replace(`__CONDITIONAL_COMMENT_PLACEHOLDER_${index}__`, commentContent);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return finalHtml;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Helper function to process files in batches
|
|
76
|
+
async function processBatch(batch, options, processed, total) {
|
|
77
|
+
return Promise.all(batch.map(async ({ file }) => {
|
|
78
|
+
try {
|
|
79
|
+
const htmlContent = file.contents.toString();
|
|
80
|
+
const finalHtml = await minifyFileContent(htmlContent, options);
|
|
81
|
+
file.contents = Buffer.from(finalHtml);
|
|
82
|
+
processed.count++;
|
|
83
|
+
|
|
84
|
+
// Log progress every 10 files or on last file
|
|
85
|
+
if (processed.count % 10 === 0 || processed.count === total) {
|
|
86
|
+
const percentage = ((processed.count / total) * 100).toFixed(1);
|
|
87
|
+
logger.log(`Progress: ${processed.count}/${total} files (${percentage}%)`);
|
|
88
|
+
Manager.logMemory(logger, `After ${processed.count} files`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return file;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
logger.error(`Error minifying ${file.path}: ${err.message}`);
|
|
94
|
+
return file;
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Main task
|
|
100
|
+
function minifyHtmlTask(complete) {
|
|
101
|
+
// Check if we should minify
|
|
102
|
+
const shouldMinify = Manager.isBuildMode() || process.env.UJ_MINIFY_HTML_FORCE === 'true';
|
|
103
|
+
|
|
104
|
+
if (!shouldMinify) {
|
|
105
|
+
logger.log('Skipping HTML minification (not in production mode and UJ_MINIFY_HTML_FORCE not set)');
|
|
106
|
+
return complete();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get concurrency limit from environment or use default
|
|
110
|
+
const CONCURRENCY_LIMIT = parseInt(process.env.UJ_MINIFY_CONCURRENCY || '10', 10);
|
|
111
|
+
|
|
112
|
+
// Log
|
|
113
|
+
logger.log('Starting...');
|
|
114
|
+
logger.log(`Concurrency limit: ${CONCURRENCY_LIMIT} files at a time`);
|
|
115
|
+
Manager.logMemory(logger, 'Start');
|
|
116
|
+
|
|
117
|
+
// Configure minify options
|
|
118
|
+
const options = {
|
|
119
|
+
collapseWhitespace: true,
|
|
120
|
+
removeComments: true,
|
|
121
|
+
removeAttributeQuotes: true,
|
|
122
|
+
removeRedundantAttributes: true,
|
|
123
|
+
removeScriptTypeAttributes: true,
|
|
124
|
+
removeStyleLinkTypeAttributes: true,
|
|
125
|
+
useShortDoctype: true,
|
|
126
|
+
removeEmptyAttributes: true,
|
|
127
|
+
removeOptionalTags: false,
|
|
128
|
+
minifyCSS: true,
|
|
129
|
+
minifyJS: true
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Collect files for batch processing
|
|
133
|
+
const fileQueue = [];
|
|
134
|
+
const processed = { count: 0 };
|
|
135
|
+
|
|
136
|
+
// Process HTML files
|
|
137
|
+
return src(input)
|
|
138
|
+
.pipe(through2.obj(function(file, _enc, callback) {
|
|
139
|
+
if (file.isBuffer()) {
|
|
140
|
+
fileQueue.push({ file });
|
|
141
|
+
callback();
|
|
142
|
+
} else {
|
|
143
|
+
callback(null, file);
|
|
144
|
+
}
|
|
145
|
+
}, async function(callback) {
|
|
146
|
+
// This function is called when all files have been queued
|
|
147
|
+
if (fileQueue.length === 0) {
|
|
148
|
+
logger.log('No HTML files to minify');
|
|
149
|
+
return callback();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const totalFiles = fileQueue.length;
|
|
153
|
+
logger.log(`Minifying ${totalFiles} HTML files...`);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Process files in batches
|
|
157
|
+
for (let i = 0; i < fileQueue.length; i += CONCURRENCY_LIMIT) {
|
|
158
|
+
const batch = fileQueue.slice(i, i + CONCURRENCY_LIMIT);
|
|
159
|
+
const processedFiles = await processBatch(batch, options, processed, totalFiles);
|
|
160
|
+
|
|
161
|
+
// Push processed files to the stream
|
|
162
|
+
processedFiles.forEach(file => this.push(file));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
callback();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
logger.error(`Batch processing error: ${err.message}`);
|
|
168
|
+
callback(err);
|
|
169
|
+
}
|
|
170
|
+
}))
|
|
171
|
+
.pipe(dest(output))
|
|
172
|
+
.on('finish', () => {
|
|
173
|
+
// Log
|
|
174
|
+
logger.log('Finished!');
|
|
175
|
+
Manager.logMemory(logger, 'End');
|
|
176
|
+
|
|
177
|
+
// Complete
|
|
178
|
+
complete();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Default Task (no watcher for minifyHtml as it runs after Jekyll build)
|
|
183
|
+
module.exports = minifyHtmlTask;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Worker thread for HTML minification
|
|
2
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
3
|
+
const { minify } = require('html-minifier-terser');
|
|
4
|
+
|
|
5
|
+
// Listen for messages from parent
|
|
6
|
+
parentPort.on('message', async (data) => {
|
|
7
|
+
const { htmlContent, options, index } = data;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Extract and temporarily replace JSON-LD scripts
|
|
11
|
+
const jsonLdScripts = [];
|
|
12
|
+
const jsonLdRegex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
13
|
+
|
|
14
|
+
let processedContent = htmlContent.replace(jsonLdRegex, (match, jsonContent) => {
|
|
15
|
+
// Minify the JSON content
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(jsonContent);
|
|
18
|
+
const minifiedJson = JSON.stringify(parsed);
|
|
19
|
+
jsonLdScripts.push(minifiedJson);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
jsonLdScripts.push(jsonContent);
|
|
22
|
+
}
|
|
23
|
+
return `__JSON_LD_PLACEHOLDER_${jsonLdScripts.length - 1}__`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Extract and temporarily replace IE conditional comments
|
|
27
|
+
const conditionalComments = [];
|
|
28
|
+
const conditionalRegex = /<!--\[if[^>]*\]>([\s\S]*?)<!\[endif\]-->/gi;
|
|
29
|
+
|
|
30
|
+
processedContent = processedContent.replace(conditionalRegex, (match, content) => {
|
|
31
|
+
// Minify the content inside the conditional comment
|
|
32
|
+
try {
|
|
33
|
+
const minifiedContent = content
|
|
34
|
+
.replace(/\s+/g, ' ')
|
|
35
|
+
.replace(/>\s+</g, '><')
|
|
36
|
+
.trim();
|
|
37
|
+
conditionalComments.push(match.replace(content, minifiedContent));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
conditionalComments.push(match);
|
|
40
|
+
}
|
|
41
|
+
return `__CONDITIONAL_COMMENT_PLACEHOLDER_${conditionalComments.length - 1}__`;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Minify the HTML content
|
|
45
|
+
const minified = await minify(processedContent, options);
|
|
46
|
+
|
|
47
|
+
// Restore the JSON-LD scripts and conditional comments
|
|
48
|
+
let finalHtml = minified;
|
|
49
|
+
jsonLdScripts.forEach((jsonContent, idx) => {
|
|
50
|
+
const scriptTag = `<script type=application/ld+json>${jsonContent}</script>`;
|
|
51
|
+
finalHtml = finalHtml.replace(`__JSON_LD_PLACEHOLDER_${idx}__`, scriptTag);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
conditionalComments.forEach((commentContent, idx) => {
|
|
55
|
+
finalHtml = finalHtml.replace(`__CONDITIONAL_COMMENT_PLACEHOLDER_${idx}__`, commentContent);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Send result back to parent
|
|
59
|
+
parentPort.postMessage({
|
|
60
|
+
success: true,
|
|
61
|
+
index,
|
|
62
|
+
result: finalHtml
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// Send error back to parent
|
|
66
|
+
parentPort.postMessage({
|
|
67
|
+
success: false,
|
|
68
|
+
index,
|
|
69
|
+
error: err.message
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
@@ -6,6 +6,7 @@ const jetpack = require('fs-jetpack');
|
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const { Octokit } = require('@octokit/rest');
|
|
8
8
|
const AdmZip = require('adm-zip');
|
|
9
|
+
const { execute } = require('node-powertools');
|
|
9
10
|
|
|
10
11
|
class GitHubCache {
|
|
11
12
|
constructor(options = {}) {
|
|
@@ -21,16 +22,39 @@ class GitHubCache {
|
|
|
21
22
|
|
|
22
23
|
// Initialize GitHub API client
|
|
23
24
|
async init() {
|
|
25
|
+
// Ensure GH_TOKEN is set
|
|
24
26
|
if (!process.env.GH_TOKEN) {
|
|
25
27
|
throw new Error('GH_TOKEN environment variable not set');
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
|
|
30
|
+
// Auto-detect repository if not set
|
|
31
|
+
if (!process.env.GITHUB_REPOSITORY) {
|
|
32
|
+
try {
|
|
33
|
+
const result = await execute('git remote get-url origin', { log: false });
|
|
34
|
+
|
|
35
|
+
// Parse GitHub repository from remote URL
|
|
36
|
+
// Supports: https://github.com/owner/repo.git, git@github.com:owner/repo.git
|
|
37
|
+
const match = result.match(/github\.com[:/]([^/]+\/[^.\s]+)/);
|
|
38
|
+
|
|
39
|
+
if (match) {
|
|
40
|
+
process.env.GITHUB_REPOSITORY = match[1];
|
|
41
|
+
this.logger.log(`📦 Auto-detected repository from git remote: ${process.env.GITHUB_REPOSITORY}`);
|
|
42
|
+
}
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Ignore errors from git command
|
|
45
|
+
this.logger.warn(`⚠️ Could not auto-detect repository from git remote: ${e.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Final check
|
|
28
50
|
if (!process.env.GITHUB_REPOSITORY) {
|
|
29
|
-
throw new Error('GITHUB_REPOSITORY environment variable not set');
|
|
51
|
+
throw new Error('GITHUB_REPOSITORY environment variable not set and could not auto-detect from git remote');
|
|
30
52
|
}
|
|
31
53
|
|
|
54
|
+
// Set owner and repo
|
|
32
55
|
[this.owner, this.repo] = process.env.GITHUB_REPOSITORY.split('/');
|
|
33
|
-
|
|
56
|
+
|
|
57
|
+
// Initialize Octokit
|
|
34
58
|
if (!this.octokit) {
|
|
35
59
|
this.octokit = new Octokit({
|
|
36
60
|
auth: process.env.GH_TOKEN,
|
|
@@ -42,22 +66,22 @@ class GitHubCache {
|
|
|
42
66
|
|
|
43
67
|
// Check if credentials are available
|
|
44
68
|
hasCredentials() {
|
|
45
|
-
return !!
|
|
69
|
+
return !!process.env.GH_TOKEN;
|
|
46
70
|
}
|
|
47
71
|
|
|
48
72
|
// Fetch cache branch from GitHub
|
|
49
73
|
async fetchBranch() {
|
|
50
74
|
await this.init();
|
|
51
|
-
|
|
75
|
+
|
|
52
76
|
this.logger.log(`📥 Fetching cache from branch '${this.branchName}'`);
|
|
53
77
|
|
|
54
78
|
// Check if the branch exists
|
|
55
79
|
let branchExists = false;
|
|
56
80
|
try {
|
|
57
|
-
await this.octokit.repos.getBranch({
|
|
58
|
-
owner: this.owner,
|
|
59
|
-
repo: this.repo,
|
|
60
|
-
branch: this.branchName
|
|
81
|
+
await this.octokit.repos.getBranch({
|
|
82
|
+
owner: this.owner,
|
|
83
|
+
repo: this.repo,
|
|
84
|
+
branch: this.branchName
|
|
61
85
|
});
|
|
62
86
|
branchExists = true;
|
|
63
87
|
} catch (e) {
|
|
@@ -81,15 +105,15 @@ class GitHubCache {
|
|
|
81
105
|
const extractDir = path.dirname(this.cacheDir);
|
|
82
106
|
|
|
83
107
|
jetpack.write(zipPath, Buffer.from(zipBallArchive.data));
|
|
84
|
-
|
|
108
|
+
|
|
85
109
|
const zip = new AdmZip(zipPath);
|
|
86
110
|
zip.extractAllTo(extractDir, true);
|
|
87
111
|
|
|
88
112
|
// Find extracted root folder
|
|
89
|
-
const extractedRoot = jetpack.list(extractDir).find(name =>
|
|
113
|
+
const extractedRoot = jetpack.list(extractDir).find(name =>
|
|
90
114
|
name.startsWith(`${this.owner}-${this.repo}-`)
|
|
91
115
|
);
|
|
92
|
-
|
|
116
|
+
|
|
93
117
|
if (!extractedRoot) {
|
|
94
118
|
throw new Error('Could not find extracted archive root folder');
|
|
95
119
|
}
|
|
@@ -105,30 +129,30 @@ class GitHubCache {
|
|
|
105
129
|
|
|
106
130
|
// Clean up
|
|
107
131
|
jetpack.remove(zipPath);
|
|
108
|
-
|
|
132
|
+
|
|
109
133
|
// Log what was fetched
|
|
110
134
|
const fetchedFiles = jetpack.find(targetPath, { matching: '**/*', files: true, directories: false });
|
|
111
135
|
this.logger.log(`✅ Fetched cache from branch '${this.branchName}' (${fetchedFiles.length} files total)`);
|
|
112
|
-
|
|
136
|
+
|
|
113
137
|
return true;
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
// Push files to cache branch with automatic orphan detection
|
|
117
141
|
async pushBranch(updatedFiles, options = {}) {
|
|
118
142
|
await this.init();
|
|
119
|
-
|
|
143
|
+
|
|
120
144
|
// Git is required
|
|
121
145
|
this.requireGitCommands();
|
|
122
146
|
|
|
123
147
|
// Convert Set to array if needed
|
|
124
148
|
let files = Array.isArray(updatedFiles) ? updatedFiles : [...updatedFiles];
|
|
125
|
-
|
|
149
|
+
|
|
126
150
|
// Auto-add metadata file if it exists and not already included
|
|
127
151
|
const metaPath = path.join(this.cacheDir, 'meta.json');
|
|
128
152
|
if (jetpack.exists(metaPath) && !files.includes(metaPath)) {
|
|
129
153
|
files.push(metaPath);
|
|
130
154
|
}
|
|
131
|
-
|
|
155
|
+
|
|
132
156
|
// Handle orphan detection if validFiles provided
|
|
133
157
|
let forceRecreate = options.forceRecreate || false;
|
|
134
158
|
if (options.validFiles) {
|
|
@@ -143,11 +167,11 @@ class GitHubCache {
|
|
|
143
167
|
}
|
|
144
168
|
}
|
|
145
169
|
}
|
|
146
|
-
|
|
170
|
+
|
|
147
171
|
this.logger.log(`📤 Pushing ${files.length} file(s) to cache branch '${this.branchName}'`);
|
|
148
172
|
|
|
149
173
|
// Generate README if stats provided
|
|
150
|
-
const readme = options.stats ? this.generateReadme(options.stats) :
|
|
174
|
+
const readme = options.stats ? this.generateReadme(options.stats) :
|
|
151
175
|
options.branchReadme || this.generateDefaultReadme();
|
|
152
176
|
|
|
153
177
|
// If forceRecreate is true, we'll handle it in uploadFilesViaGit
|
|
@@ -160,7 +184,7 @@ class GitHubCache {
|
|
|
160
184
|
// Normal update
|
|
161
185
|
await this.ensureBranchExists(readme);
|
|
162
186
|
const uploadedCount = await this.uploadFilesViaGit(files, false, readme);
|
|
163
|
-
|
|
187
|
+
|
|
164
188
|
if (uploadedCount > 0) {
|
|
165
189
|
this.logger.log(`🎉 Pushed ${uploadedCount} file(s) to cache branch`);
|
|
166
190
|
}
|
|
@@ -196,7 +220,7 @@ class GitHubCache {
|
|
|
196
220
|
let source;
|
|
197
221
|
let retries = 5;
|
|
198
222
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
199
|
-
|
|
223
|
+
|
|
200
224
|
while (retries > 0) {
|
|
201
225
|
try {
|
|
202
226
|
const result = await this.octokit.git.getRef({
|
|
@@ -255,12 +279,12 @@ class GitHubCache {
|
|
|
255
279
|
// Ensure branch exists, create if needed
|
|
256
280
|
async ensureBranchExists(readmeContent) {
|
|
257
281
|
let branchExists = false;
|
|
258
|
-
|
|
282
|
+
|
|
259
283
|
try {
|
|
260
|
-
await this.octokit.repos.getBranch({
|
|
261
|
-
owner: this.owner,
|
|
262
|
-
repo: this.repo,
|
|
263
|
-
branch: this.branchName
|
|
284
|
+
await this.octokit.repos.getBranch({
|
|
285
|
+
owner: this.owner,
|
|
286
|
+
repo: this.repo,
|
|
287
|
+
branch: this.branchName
|
|
264
288
|
});
|
|
265
289
|
branchExists = true;
|
|
266
290
|
} catch (e) {
|
|
@@ -321,7 +345,7 @@ class GitHubCache {
|
|
|
321
345
|
// Load metadata file
|
|
322
346
|
loadMetadata(metaPath) {
|
|
323
347
|
let meta = {};
|
|
324
|
-
|
|
348
|
+
|
|
325
349
|
if (jetpack.exists(metaPath)) {
|
|
326
350
|
try {
|
|
327
351
|
meta = jetpack.read(metaPath, 'json');
|
|
@@ -329,7 +353,7 @@ class GitHubCache {
|
|
|
329
353
|
this.logger.warn('⚠️ Metadata file corrupted - starting fresh');
|
|
330
354
|
}
|
|
331
355
|
}
|
|
332
|
-
|
|
356
|
+
|
|
333
357
|
return meta;
|
|
334
358
|
}
|
|
335
359
|
|
|
@@ -337,14 +361,14 @@ class GitHubCache {
|
|
|
337
361
|
saveMetadata(metaPath, meta) {
|
|
338
362
|
jetpack.write(metaPath, meta);
|
|
339
363
|
}
|
|
340
|
-
|
|
364
|
+
|
|
341
365
|
// Clean deleted files from metadata
|
|
342
366
|
cleanDeletedFromMetadata(meta, currentFiles, rootPath) {
|
|
343
|
-
const currentFilesSet = new Set(currentFiles.map(f =>
|
|
367
|
+
const currentFilesSet = new Set(currentFiles.map(f =>
|
|
344
368
|
path.relative(rootPath, f)
|
|
345
369
|
));
|
|
346
370
|
let removedCount = 0;
|
|
347
|
-
|
|
371
|
+
|
|
348
372
|
Object.keys(meta).forEach(key => {
|
|
349
373
|
if (!currentFilesSet.has(key)) {
|
|
350
374
|
delete meta[key];
|
|
@@ -352,7 +376,7 @@ class GitHubCache {
|
|
|
352
376
|
removedCount++;
|
|
353
377
|
}
|
|
354
378
|
});
|
|
355
|
-
|
|
379
|
+
|
|
356
380
|
return removedCount;
|
|
357
381
|
}
|
|
358
382
|
|
|
@@ -370,13 +394,13 @@ class GitHubCache {
|
|
|
370
394
|
// Upload files using git commands (much faster for multiple files)
|
|
371
395
|
async uploadFilesViaGit(files, forceRecreate = false, readme = null) {
|
|
372
396
|
const { execSync } = require('child_process');
|
|
373
|
-
|
|
397
|
+
|
|
374
398
|
this.logger.log(`🚀 Using fast git upload for ${files.length} files`);
|
|
375
|
-
|
|
399
|
+
|
|
376
400
|
try {
|
|
377
401
|
// Work directly in the cache directory
|
|
378
402
|
const gitDir = path.join(this.cacheDir, '.git');
|
|
379
|
-
|
|
403
|
+
|
|
380
404
|
if (forceRecreate) {
|
|
381
405
|
// For force recreate, remove git dir and init fresh
|
|
382
406
|
this.logger.log(`🆕 Initializing fresh repository in ${this.cacheDir}...`);
|
|
@@ -387,19 +411,19 @@ class GitHubCache {
|
|
|
387
411
|
} else if (!jetpack.exists(gitDir)) {
|
|
388
412
|
// If no git dir exists, clone the branch
|
|
389
413
|
this.logger.log(`📥 Initializing git in cache directory...`);
|
|
390
|
-
|
|
414
|
+
|
|
391
415
|
// Save current files temporarily
|
|
392
416
|
const tempBackup = path.join(path.dirname(this.cacheDir), `${path.basename(this.cacheDir)}-backup-${Date.now()}`);
|
|
393
417
|
if (jetpack.exists(this.cacheDir)) {
|
|
394
418
|
jetpack.move(this.cacheDir, tempBackup);
|
|
395
419
|
}
|
|
396
|
-
|
|
420
|
+
|
|
397
421
|
// Clone the branch
|
|
398
422
|
execSync(
|
|
399
423
|
`git clone --depth 1 --branch ${this.branchName} https://${process.env.GH_TOKEN}@github.com/${this.owner}/${this.repo}.git "${this.cacheDir}"`,
|
|
400
424
|
{ stdio: 'ignore' }
|
|
401
425
|
);
|
|
402
|
-
|
|
426
|
+
|
|
403
427
|
// Restore backed up files (overwriting cloned files)
|
|
404
428
|
if (jetpack.exists(tempBackup)) {
|
|
405
429
|
jetpack.copy(tempBackup, this.cacheDir, { overwrite: true });
|
|
@@ -416,28 +440,28 @@ class GitHubCache {
|
|
|
416
440
|
this.logger.warn('⚠️ Pull failed, will force push if needed');
|
|
417
441
|
}
|
|
418
442
|
}
|
|
419
|
-
|
|
443
|
+
|
|
420
444
|
// Add README if provided
|
|
421
445
|
let readmeChanged = false;
|
|
422
446
|
if (readme) {
|
|
423
447
|
const readmePath = path.join(this.cacheDir, 'README.md');
|
|
424
448
|
const existingReadme = jetpack.exists(readmePath) ? jetpack.read(readmePath) : '';
|
|
425
|
-
|
|
449
|
+
|
|
426
450
|
if (existingReadme !== readme) {
|
|
427
451
|
this.logger.log('📝 README content has changed, updating...');
|
|
428
452
|
readmeChanged = true;
|
|
429
453
|
}
|
|
430
|
-
|
|
454
|
+
|
|
431
455
|
jetpack.write(readmePath, readme);
|
|
432
456
|
}
|
|
433
|
-
|
|
457
|
+
|
|
434
458
|
// Check if there are changes
|
|
435
459
|
const status = execSync('git status --porcelain', { cwd: this.cacheDir }).toString();
|
|
436
460
|
if (!status.trim()) {
|
|
437
461
|
this.logger.log('⏭️ No changes to commit (including README)');
|
|
438
462
|
return 0;
|
|
439
463
|
}
|
|
440
|
-
|
|
464
|
+
|
|
441
465
|
// Log what changed
|
|
442
466
|
const changedFiles = status.trim().split('\n').length;
|
|
443
467
|
if (readmeChanged && changedFiles === 1) {
|
|
@@ -447,11 +471,11 @@ class GitHubCache {
|
|
|
447
471
|
} else {
|
|
448
472
|
this.logger.log(`📝 ${changedFiles} files have changed`);
|
|
449
473
|
}
|
|
450
|
-
|
|
474
|
+
|
|
451
475
|
// Add all changes
|
|
452
476
|
this.logger.log(`📝 Staging changes...`);
|
|
453
477
|
execSync('git add -A', { cwd: this.cacheDir, stdio: 'ignore' });
|
|
454
|
-
|
|
478
|
+
|
|
455
479
|
// Create commit message based on what changed
|
|
456
480
|
let commitMessage;
|
|
457
481
|
if (readmeChanged && changedFiles === 1) {
|
|
@@ -461,13 +485,13 @@ class GitHubCache {
|
|
|
461
485
|
} else {
|
|
462
486
|
commitMessage = `📦 Update cache: ${changedFiles} files`;
|
|
463
487
|
}
|
|
464
|
-
|
|
488
|
+
|
|
465
489
|
// Commit
|
|
466
490
|
execSync(
|
|
467
491
|
`git -c user.name="GitHub Actions" -c user.email="actions@github.com" commit -m "${commitMessage}"`,
|
|
468
492
|
{ cwd: this.cacheDir, stdio: 'ignore' }
|
|
469
493
|
);
|
|
470
|
-
|
|
494
|
+
|
|
471
495
|
// Push
|
|
472
496
|
this.logger.log(`📤 Pushing to GitHub...`);
|
|
473
497
|
try {
|
|
@@ -477,7 +501,7 @@ class GitHubCache {
|
|
|
477
501
|
this.logger.warn('⚠️ Normal push failed, attempting force push...');
|
|
478
502
|
execSync('git push --force origin ' + this.branchName, { cwd: this.cacheDir, stdio: 'ignore' });
|
|
479
503
|
}
|
|
480
|
-
|
|
504
|
+
|
|
481
505
|
return changedFiles;
|
|
482
506
|
} catch (error) {
|
|
483
507
|
this.logger.error(`❌ Git command failed: ${error.message}`);
|
|
@@ -488,15 +512,15 @@ class GitHubCache {
|
|
|
488
512
|
// Check for orphaned files in cache
|
|
489
513
|
async checkForOrphans(validFiles) {
|
|
490
514
|
const validSet = new Set(validFiles);
|
|
491
|
-
const cacheFiles = jetpack.find(this.cacheDir, {
|
|
492
|
-
matching: '**/*',
|
|
493
|
-
files: true,
|
|
494
|
-
directories: false
|
|
515
|
+
const cacheFiles = jetpack.find(this.cacheDir, {
|
|
516
|
+
matching: '**/*',
|
|
517
|
+
files: true,
|
|
518
|
+
directories: false
|
|
495
519
|
});
|
|
496
|
-
|
|
520
|
+
|
|
497
521
|
const orphanedFiles = [];
|
|
498
522
|
const validCacheFiles = [];
|
|
499
|
-
|
|
523
|
+
|
|
500
524
|
cacheFiles.forEach(file => {
|
|
501
525
|
const relativePath = path.relative(this.cacheDir, file);
|
|
502
526
|
if (validSet.has(relativePath) || relativePath === 'meta.json') {
|
|
@@ -508,7 +532,7 @@ class GitHubCache {
|
|
|
508
532
|
}
|
|
509
533
|
}
|
|
510
534
|
});
|
|
511
|
-
|
|
535
|
+
|
|
512
536
|
return {
|
|
513
537
|
hasOrphans: orphanedFiles.length > 0,
|
|
514
538
|
orphanedCount: orphanedFiles.length,
|
|
@@ -531,16 +555,16 @@ This branch stores ${this.description}.
|
|
|
531
555
|
// Generate README with stats
|
|
532
556
|
generateReadme(stats = {}) {
|
|
533
557
|
const date = new Date(stats.timestamp || Date.now());
|
|
534
|
-
const formattedDate = date.toLocaleString('en-US', {
|
|
535
|
-
weekday: 'long',
|
|
536
|
-
year: 'numeric',
|
|
537
|
-
month: 'long',
|
|
558
|
+
const formattedDate = date.toLocaleString('en-US', {
|
|
559
|
+
weekday: 'long',
|
|
560
|
+
year: 'numeric',
|
|
561
|
+
month: 'long',
|
|
538
562
|
day: 'numeric',
|
|
539
563
|
hour: '2-digit',
|
|
540
564
|
minute: '2-digit',
|
|
541
565
|
timeZoneName: 'short'
|
|
542
566
|
});
|
|
543
|
-
|
|
567
|
+
|
|
544
568
|
let readme = `# ${this.cacheType} Cache Branch
|
|
545
569
|
|
|
546
570
|
This branch stores ${this.description}.
|
|
@@ -580,7 +604,7 @@ This branch stores ${this.description}.
|
|
|
580
604
|
- **Files From Cache:** ${stats.fromCache || 0}
|
|
581
605
|
- **Newly Processed:** ${stats.newlyProcessed || 0}
|
|
582
606
|
`;
|
|
583
|
-
|
|
607
|
+
|
|
584
608
|
// Add percentage if both values exist
|
|
585
609
|
if (stats.processedNow && stats.fromCache !== undefined) {
|
|
586
610
|
const cacheRate = ((stats.fromCache / stats.processedNow) * 100).toFixed(1);
|
|
@@ -634,7 +658,7 @@ This branch stores ${this.description}.
|
|
|
634
658
|
- **Optimized:** ${optimized || 0}
|
|
635
659
|
- **Skipped (from cache):** ${skipped || 0}
|
|
636
660
|
`;
|
|
637
|
-
|
|
661
|
+
|
|
638
662
|
if (totalSizeBefore && totalSizeAfter) {
|
|
639
663
|
const savedPercent = ((totalSaved / totalSizeBefore) * 100).toFixed(1);
|
|
640
664
|
readme += `
|
|
@@ -668,7 +692,7 @@ ${stats.details}
|
|
|
668
692
|
const seconds = Math.floor(ms / 1000);
|
|
669
693
|
const minutes = Math.floor(seconds / 60);
|
|
670
694
|
const hours = Math.floor(minutes / 60);
|
|
671
|
-
|
|
695
|
+
|
|
672
696
|
if (hours > 0) {
|
|
673
697
|
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
674
698
|
} else if (minutes > 0) {
|
|
@@ -695,4 +719,4 @@ ${stats.details}
|
|
|
695
719
|
}
|
|
696
720
|
}
|
|
697
721
|
|
|
698
|
-
module.exports = GitHubCache;
|
|
722
|
+
module.exports = GitHubCache;
|
package/firebase-debug.log
CHANGED
|
@@ -54,3 +54,11 @@
|
|
|
54
54
|
[debug] [2025-10-22T06:51:09.274Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
55
55
|
[debug] [2025-10-22T06:51:09.274Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
56
56
|
[debug] [2025-10-22T06:51:09.274Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
57
|
+
[debug] [2025-10-22T08:40:01.642Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
58
|
+
[debug] [2025-10-22T08:40:01.642Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
59
|
+
[debug] [2025-10-22T08:40:01.644Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
60
|
+
[debug] [2025-10-22T08:40:01.644Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
61
|
+
[debug] [2025-10-22T08:40:01.644Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
62
|
+
[debug] [2025-10-22T08:40:01.644Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
|
63
|
+
[debug] [2025-10-22T08:40:01.644Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
|
|
64
|
+
[debug] [2025-10-22T08:40:01.644Z] > authorizing via signed-in user (ian.wiedenman@gmail.com)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-jekyll-manager",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.94",
|
|
4
4
|
"description": "Ultimate Jekyll dependency manager",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"@babel/core": "^7.28.4",
|
|
63
63
|
"@babel/preset-env": "^7.28.3",
|
|
64
64
|
"@fullhuman/postcss-purgecss": "^7.0.2",
|
|
65
|
+
"@minify-html/node": "^0.16.4",
|
|
65
66
|
"@octokit/rest": "^22.0.0",
|
|
66
67
|
"@popperjs/core": "^2.11.8",
|
|
67
68
|
"@prettier/plugin-xml": "^3.4.2",
|