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.
@@ -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-minifier-terser');
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
- collapseWhitespace: true,
39
- removeComments: true,
40
- removeAttributeQuotes: true,
41
- removeRedundantAttributes: true,
42
- removeScriptTypeAttributes: true,
43
- removeStyleLinkTypeAttributes: true,
44
- useShortDoctype: true,
45
- removeEmptyAttributes: true,
46
- removeOptionalTags: false,
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(async function(file, enc, callback) {
88
+ .pipe(through2.obj(function(file, _enc, callback) {
54
89
  if (file.isBuffer()) {
55
- try {
56
- let htmlContent = file.contents.toString();
57
-
58
- // Extract and temporarily replace JSON-LD scripts
59
- const jsonLdScripts = [];
60
- const jsonLdRegex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
61
-
62
- htmlContent = htmlContent.replace(jsonLdRegex, (match, jsonContent) => {
63
- // Minify the JSON content
64
- try {
65
- const parsed = JSON.parse(jsonContent);
66
- const minifiedJson = JSON.stringify(parsed);
67
- jsonLdScripts.push(minifiedJson);
68
- } catch (e) {
69
- jsonLdScripts.push(jsonContent);
70
- }
71
- return `__JSON_LD_PLACEHOLDER_${jsonLdScripts.length - 1}__`;
72
- });
73
-
74
- // Extract and temporarily replace IE conditional comments
75
- const conditionalComments = [];
76
- const conditionalRegex = /<!--\[if[^>]*\]>([\s\S]*?)<!\[endif\]-->/gi;
77
-
78
- htmlContent = htmlContent.replace(conditionalRegex, (match, content) => {
79
- // Minify the content inside the conditional comment
80
- try {
81
- const minifiedContent = content
82
- .replace(/\s+/g, ' ')
83
- .replace(/>\s+</g, '><')
84
- .trim();
85
- conditionalComments.push(match.replace(content, minifiedContent));
86
- } catch (e) {
87
- conditionalComments.push(match);
88
- }
89
- return `__CONDITIONAL_COMMENT_PLACEHOLDER_${conditionalComments.length - 1}__`;
90
- });
91
-
92
- // Minify the HTML content
93
- const minified = await minify(htmlContent, options);
94
-
95
- // Restore the JSON-LD scripts and conditional comments
96
- let finalHtml = minified;
97
- jsonLdScripts.forEach((jsonContent, index) => {
98
- const scriptTag = `<script type=application/ld+json>${jsonContent}</script>`;
99
- finalHtml = finalHtml.replace(`__JSON_LD_PLACEHOLDER_${index}__`, scriptTag);
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
- return complete();
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 !!(process.env.GH_TOKEN && process.env.GITHUB_REPOSITORY);
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;
@@ -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.92",
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",