@aetherframework/template-engine 1.0.0 → 1.0.2

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,642 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/template-engine/src/engines/CompressionEngine
6
+ */
7
+
8
+ import fs from 'fs-extra';
9
+ import path from 'path';
10
+ import crypto from 'crypto';
11
+ /**
12
+ * Compression Engine - Handles HTML, CSS, and JavaScript minification and obfuscation
13
+ * Supports caching of compressed results for performance optimization
14
+ */
15
+ class CompressionEngine {
16
+ constructor(options = {}) {
17
+ this.options = {
18
+ minifyHTML: options.minifyHTML !== false,
19
+ minifyCSS: options.minifyCSS !== false,
20
+ minifyJS: options.minifyJS !== false,
21
+ mangleJS: options.mangleJS || false,
22
+ removeComments: options.removeComments || false,
23
+ collapseWhitespace: options.collapseWhitespace || true,
24
+ removeAttributeQuotes: options.removeAttributeQuotes || false,
25
+ removeEmptyAttributes: options.removeEmptyAttributes || false,
26
+ cacheCompressed: options.cacheCompressed !== false,
27
+ ...options
28
+ };
29
+
30
+ // Cache for compressed results
31
+ this.compressionCache = new Map();
32
+ this.cacheTTL = options.cacheTTL || 3600000; // 1 hour default
33
+ this.cacheTimestamps = new Map();
34
+ }
35
+
36
+ /**
37
+ * Generate cache key for compressed content
38
+ * @param {string} content - Original content
39
+ * @param {Object} options - Compression options
40
+ * @returns {string} Cache key
41
+ */
42
+ generateCacheKey(content, options = {}) {
43
+ const configString = JSON.stringify({
44
+ ...this.options,
45
+ ...options
46
+ });
47
+ return crypto.createHash('md5')
48
+ .update(content + configString)
49
+ .digest('hex');
50
+ }
51
+
52
+ /**
53
+ * Check if cached compressed content is still valid
54
+ * @param {string} cacheKey - Cache key
55
+ * @returns {boolean} True if cache is valid
56
+ */
57
+ isCacheValid(cacheKey) {
58
+ if (!this.options.cacheCompressed) return false;
59
+
60
+ const timestamp = this.cacheTimestamps.get(cacheKey);
61
+ if (!timestamp) return false;
62
+
63
+ return Date.now() - timestamp < this.cacheTTL;
64
+ }
65
+
66
+ /**
67
+ * Get cached compressed content
68
+ * @param {string} cacheKey - Cache key
69
+ * @returns {string|null} Cached content or null
70
+ */
71
+ getCached(cacheKey) {
72
+ if (this.options.cacheCompressed &&
73
+ this.compressionCache.has(cacheKey) &&
74
+ this.isCacheValid(cacheKey)) {
75
+ return this.compressionCache.get(cacheKey);
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Set cached compressed content
82
+ * @param {string} cacheKey - Cache key
83
+ * @param {string} content - Compressed content
84
+ */
85
+ setCached(cacheKey, content) {
86
+ if (this.options.cacheCompressed) {
87
+ this.compressionCache.set(cacheKey, content);
88
+ this.cacheTimestamps.set(cacheKey, Date.now());
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Clear compression cache
94
+ */
95
+ clearCompressionCache() {
96
+ this.compressionCache.clear();
97
+ this.cacheTimestamps.clear();
98
+ }
99
+
100
+ /**
101
+ * Minify HTML content with caching support
102
+ * @param {string} html - HTML content
103
+ * @param {Object} options - Minification options
104
+ * @returns {string} Minified HTML
105
+ */
106
+ minifyHTML(html, options = {}) {
107
+ const cacheKey = this.generateCacheKey(html, { type: 'html', ...options });
108
+ const cached = this.getCached(cacheKey);
109
+ if (cached) return cached;
110
+
111
+ const opts = { ...this.options, ...options };
112
+ let result = html;
113
+
114
+ // [Critical Fix 1]: Protect pre, textarea, script, and style tags from whitespace collapsing
115
+ // This preserves line breaks and indentation inside code blocks
116
+ const protectedBlocks = [];
117
+ result = result.replace(/<(pre|textarea|script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, (match) => {
118
+ protectedBlocks.push(match);
119
+ return `<!--PROTECTED_BLOCK_${protectedBlocks.length - 1}-->`;
120
+ });
121
+
122
+ // Remove HTML comments (preserve conditional comments and protected blocks)
123
+ if (opts.removeComments) {
124
+ // [Critical Fix 2]: Fixed conditional comment regex (changed invalid $if to \[if)
125
+ result = result.replace(/<!--(?!\[if\s|PROTECTED_BLOCK_).*?-->/gs, '');
126
+ }
127
+
128
+ // Collapse whitespace (only applies to unprotected HTML structure)
129
+ if (opts.collapseWhitespace) {
130
+ result = result
131
+ .replace(/\s+/g, ' ')
132
+ .replace(/>\s+</g, '><')
133
+ .replace(/\s+>/g, '>')
134
+ .replace(/<\s+/g, '<')
135
+ .replace(/\s+$/gm, '')
136
+ .replace(/^\s+/gm, '');
137
+ }
138
+
139
+ // Remove optional quotes from attributes
140
+ if (opts.removeAttributeQuotes) {
141
+ result = result.replace(/(\w+)=["']([^"']*)["']/g, (match, attr, value) => {
142
+ // Only remove quotes if value doesn't contain spaces or special characters
143
+ if (!/[ "'=<>`]/.test(value)) {
144
+ return `${attr}=${value}`;
145
+ }
146
+ return match;
147
+ });
148
+ }
149
+
150
+ // Remove empty attributes
151
+ if (opts.removeEmptyAttributes) {
152
+ result = result.replace(/\s+(\w+)=["']\s*["']/g, '');
153
+ }
154
+
155
+ // Remove redundant attributes
156
+ result = result
157
+ .replace(/\s+type=["']text\/javascript["']/gi, '')
158
+ .replace(/\s+type=["']text\/css["']/gi, '')
159
+ .replace(/\s+language=["']javascript["']/gi, '');
160
+
161
+ // [Critical Fix 3]: Restore protected blocks back to the HTML
162
+ result = result.replace(/<!--PROTECTED_BLOCK_(\d+)-->/g, (match, index) => {
163
+ return protectedBlocks[parseInt(index, 10)];
164
+ });
165
+
166
+ const finalResult = result.trim();
167
+ this.setCached(cacheKey, finalResult);
168
+ return finalResult;
169
+ }
170
+
171
+ /**
172
+ * Minify CSS content with caching support
173
+ * @param {string} css - CSS content
174
+ * @param {Object} options - Minification options
175
+ * @returns {string} Minified CSS
176
+ */
177
+ minifyCSS(css, options = {}) {
178
+ const cacheKey = this.generateCacheKey(css, { type: 'css', ...options });
179
+ const cached = this.getCached(cacheKey);
180
+ if (cached) return cached;
181
+
182
+ const opts = { ...this.options, ...options };
183
+ let result = css;
184
+
185
+ // [Fix 1]: Protect strings, calc(), and url() from whitespace collapsing
186
+ const protectedBlocks = [];
187
+ result = result.replace(/(["'])(?:(?!\1|\\).|\\.)*\1|url\([^)]*\)|calc\([^)]*\)/gi, (match) => {
188
+ protectedBlocks.push(match);
189
+ return `__CSS_PROTECTED_${protectedBlocks.length - 1}__`;
190
+ });
191
+
192
+ // Remove CSS comments (preserve important/license comments)
193
+ if (opts.removeComments) {
194
+ result = result.replace(/\/\*[\s\S]*?\*\//g, (match) => {
195
+ return (match.includes('!important') || match.includes('@license') || match.includes('@preserve') || match.includes('!')) ? match : '';
196
+ });
197
+ }
198
+
199
+ // Minify CSS structure
200
+ result = result
201
+ .replace(/\s+/g, ' ')
202
+ .replace(/;\s+/g, ';')
203
+ .replace(/:\s+/g, ':')
204
+ .replace(/,\s+/g, ',')
205
+ .replace(/\s*{\s*/g, '{')
206
+ .replace(/\s*}\s*/g, '}')
207
+ .replace(/;\s*}/g, '}')
208
+ .replace(/\s*;\s*/g, ';')
209
+ .replace(/\s*,\s*/g, ',')
210
+ .replace(/\s*:\s*/g, ':')
211
+ .replace(/\s*!\s*important/g, '!important')
212
+ .replace(/#([0-9a-fA-F])\1([0-9a-fA-F])\2([0-9a-fA-F])\3/g, '#$1$2$3')
213
+ .replace(/\b0(?:\.0+)?(?:px|em|rem|pt|pc|in|cm|mm|ex|ch|vh|vw|vmin|vmax)\b/gi, '0')
214
+ .replace(/\b0 0 0 0\b/g, '0')
215
+ .replace(/\b0 0\b/g, '0');
216
+
217
+ // [Fix 2]: Restore protected blocks back to the CSS
218
+ result = result.replace(/__CSS_PROTECTED_(\d+)__/g, (match, index) => {
219
+ return protectedBlocks[parseInt(index, 10)];
220
+ });
221
+
222
+ // [Fix 3]: Fix template engine escape residue (@@ -> @)
223
+ // This fixes issues where template engines fail to unescape @@import or @@keyframes
224
+ result = result.replace(/@@/g, '@');
225
+
226
+ const finalResult = result.trim();
227
+ this.setCached(cacheKey, finalResult);
228
+ return finalResult;
229
+ }
230
+
231
+
232
+ /**
233
+ * Minify JavaScript content with caching support
234
+ * @param {string} js - JavaScript code
235
+ * @param {Object} options - Minification options
236
+ * @returns {string} Minified JavaScript
237
+ */
238
+ minifyJS(js, options = {}) {
239
+ const cacheKey = this.generateCacheKey(js, { type: 'js', ...options });
240
+ const cached = this.getCached(cacheKey);
241
+ if (cached) return cached;
242
+
243
+ const opts = { ...this.options, ...options };
244
+ let result = js;
245
+
246
+ // Remove single-line comments (safely ignore URLs like http:// or https://)
247
+ if (opts.removeComments) {
248
+ result = result.replace(/(^|[^:"'])\/\/.*$/gm, '$1');
249
+ }
250
+
251
+ // Remove multi-line comments (preserve license and preserve comments)
252
+ if (opts.removeComments) {
253
+ result = result.replace(/\/\*[\s\S]*?\*\//g, (match) => {
254
+ if (match.includes('@license') || match.includes('@preserve') || match.includes('!')) {
255
+ return match;
256
+ }
257
+ return '';
258
+ });
259
+ }
260
+
261
+ // Safe JavaScript minification
262
+ result = result
263
+ // 1. Replace multiple whitespace/newlines with a single space
264
+ .replace(/\s+/g, ' ')
265
+ // 2. [Critical Fix]: Remove spaces around safe operators, BUT EXCLUDE < and > to prevent breaking HTML tags if mixed
266
+ .replace(/\s*([=+\-*/%&|^?:;,{}()[\]])\s*/g, '$1')
267
+ // 3. Restore spaces after JS keywords to prevent syntax errors (e.g., 'constapp' -> 'const app')
268
+ .replace(/\b(var|let|const|return|typeof|instanceof|in|new|delete|void|throw|case|break|continue|yield|await|async|function|class|extends|import|export|from|default|if|else|for|while|do|switch|try|catch|finally|with)\b/g, '$1 ')
269
+ // 4. Clean up specific patterns
270
+ .replace(/;\s*}/g, '}')
271
+ .replace(/\s*{\s*/g, '{')
272
+ .replace(/\s*}\s*/g, '}')
273
+ .replace(/else\s*{/g, 'else{')
274
+ .replace(/}\s*else/g, '}else')
275
+ .replace(/\s*=>\s*/g, '=>');
276
+
277
+ const finalResult = result.trim();
278
+ this.setCached(cacheKey, finalResult);
279
+ return finalResult;
280
+ }
281
+
282
+
283
+ /**
284
+ * Obfuscate JavaScript code with caching support
285
+ * Uses a strict character-by-character parser to prevent regex-related false positives
286
+ * @param {string} js - JavaScript code
287
+ * @param {Object} options - Obfuscation options
288
+ * @returns {string} Obfuscated JavaScript
289
+ */
290
+ obfuscateJS(js, options = {}) {
291
+ const cacheKey = this.generateCacheKey(js, { type: 'obfuscate', ...options });
292
+ const cached = this.getCached(cacheKey);
293
+ if (cached) return cached;
294
+
295
+ const opts = {
296
+ mangle: options.mangle !== false,
297
+ reserved: options.reserved || ['render', 'data', 'helpers', 'exports', 'module', 'require', 'window', 'document', 'console', 'alert', 'fetch', 'Promise', 'JSON', 'Math', 'Date', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Function', 'RegExp', 'Error', 'TypeError', 'RangeError', 'SyntaxError', 'ReferenceError', 'URIError', 'EvalError', 'InternalError', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'setImmediate', 'clearImmediate', 'requestAnimationFrame', 'cancelAnimationFrame', 'localStorage', 'sessionStorage', 'navigator', 'location', 'history', 'getElementById', 'addEventListener', 'clipboard', 'writeText', 'querySelector', 'querySelectorAll', 'innerHTML', 'innerText', 'textContent', 'className', 'classList', 'style', 'setAttribute', 'getAttribute', 'removeAttribute', 'appendChild', 'removeChild', 'createElement', 'preventDefault', 'stopPropagation', 'target', 'currentTarget', 'value', 'checked', 'disabled', 'length', 'push', 'pop', 'shift', 'unshift', 'map', 'filter', 'reduce', 'forEach', 'find', 'includes', 'indexOf', 'join', 'split', 'replace', 'match', 'test', 'trim', 'toLowerCase', 'toUpperCase', 'keys', 'values', 'entries', 'assign', 'parse', 'stringify', 'log', 'warn', 'error', 'info', 'debug'],
298
+ ...options
299
+ };
300
+
301
+ let result = js;
302
+
303
+ if (opts.mangle) {
304
+ const protectedBlocks = [];
305
+ const protect = (match) => {
306
+ protectedBlocks.push(match);
307
+ return `__PROTECTED_${protectedBlocks.length - 1}__`;
308
+ };
309
+
310
+ // [Critical Fix 1]: Strict Character-by-Character Lexer
311
+ // Replaces fragile regex parsing to perfectly handle nested template literals, strings, and comments
312
+ let parsedCode = '';
313
+ let i = 0;
314
+ const len = result.length;
315
+
316
+ while (i < len) {
317
+ const char = result[i];
318
+ const nextChar = result[i + 1];
319
+
320
+ // 1. Handle Single-line Comments (//)
321
+ if (char === '/' && nextChar === '/') {
322
+ let comment = '';
323
+ while (i < len && result[i] !== '\n') {
324
+ comment += result[i];
325
+ i++;
326
+ }
327
+ parsedCode += protect(comment);
328
+ continue;
329
+ }
330
+
331
+ // 2. Handle Multi-line Comments (/* */)
332
+ if (char === '/' && nextChar === '*') {
333
+ let comment = '/*';
334
+ i += 2;
335
+ while (i < len && !(result[i] === '*' && result[i + 1] === '/')) {
336
+ comment += result[i];
337
+ i++;
338
+ }
339
+ if (i < len) {
340
+ comment += '*/';
341
+ i += 2;
342
+ }
343
+ parsedCode += protect(comment);
344
+ continue;
345
+ }
346
+
347
+ // 3. Handle Strings (' and ")
348
+ if (char === '"' || char === "'") {
349
+ let str = char;
350
+ i++;
351
+ while (i < len && result[i] !== char) {
352
+ if (result[i] === '\\' && i + 1 < len) {
353
+ str += result[i] + result[i + 1];
354
+ i += 2;
355
+ } else {
356
+ str += result[i];
357
+ i++;
358
+ }
359
+ }
360
+ if (i < len) {
361
+ str += char;
362
+ i++;
363
+ }
364
+ parsedCode += protect(str);
365
+ continue;
366
+ }
367
+
368
+ // 4. Handle Template Literals (`) with Depth Tracking for ${...}
369
+ if (char === '`') {
370
+ let templateStr = '`';
371
+ i++;
372
+ while (i < len && result[i] !== '`') {
373
+ if (result[i] === '\\' && i + 1 < len) {
374
+ templateStr += result[i] + result[i + 1];
375
+ i += 2;
376
+ } else if (result[i] === '$' && result[i + 1] === '{') {
377
+ templateStr += '${';
378
+ i += 2;
379
+ let braceDepth = 1;
380
+ // Track nested braces inside the template expression
381
+ while (i < len && braceDepth > 0) {
382
+ if (result[i] === '{') braceDepth++;
383
+ else if (result[i] === '}') braceDepth--;
384
+
385
+ if (braceDepth > 0) templateStr += result[i];
386
+ i++;
387
+ }
388
+ templateStr += '}';
389
+ } else {
390
+ templateStr += result[i];
391
+ i++;
392
+ }
393
+ }
394
+ if (i < len) {
395
+ templateStr += '`';
396
+ i++;
397
+ }
398
+
399
+ // Desugar template literal into string concatenation to expose variables
400
+ if (!templateStr.includes('${')) {
401
+ parsedCode += protect(templateStr);
402
+ } else {
403
+ // Safely split by tracking braces instead of using flawed regex
404
+ const parts = [];
405
+ let currentText = '';
406
+ let expr = '';
407
+ let inExpr = false;
408
+ let depth = 0;
409
+
410
+ for (let k = 1; k < templateStr.length - 1; k++) {
411
+ const c = templateStr[k];
412
+ if (!inExpr && c === '$' && templateStr[k+1] === '{') {
413
+ parts.push({ type: 'text', value: currentText });
414
+ currentText = '';
415
+ inExpr = true;
416
+ depth = 1;
417
+ k++; // skip {
418
+ } else if (inExpr) {
419
+ if (c === '{') depth++;
420
+ else if (c === '}') depth--;
421
+
422
+ if (depth === 0) {
423
+ parts.push({ type: 'expr', value: expr });
424
+ expr = '';
425
+ inExpr = false;
426
+ } else {
427
+ expr += c;
428
+ }
429
+ } else {
430
+ currentText += c;
431
+ }
432
+ }
433
+ if (currentText) parts.push({ type: 'text', value: currentText });
434
+
435
+ let reconstructed = '';
436
+ parts.forEach(part => {
437
+ if (part.type === 'expr') {
438
+ reconstructed += ` + (${part.value}) + `;
439
+ } else if (part.value) {
440
+ reconstructed += protect(JSON.stringify(part.value));
441
+ }
442
+ });
443
+ parsedCode += reconstructed.replace(/^\s*\+\s*|\s*\+\s*$/g, '').replace(/\+\s*\+\s*/g, '+');
444
+ }
445
+ continue;
446
+ }
447
+
448
+ // 5. Handle Regular Expressions vs Division
449
+ if (char === '/') {
450
+ let prevToken = parsedCode.trim().slice(-1);
451
+ // If preceded by ), ], 0-9, or a word character, it's likely division
452
+ if (prevToken && /[\w\)\]]/.test(prevToken)) {
453
+ parsedCode += char;
454
+ i++;
455
+ continue;
456
+ }
457
+
458
+ let regexStr = '/';
459
+ i++;
460
+ let inCharClass = false;
461
+ while (i < len) {
462
+ if (result[i] === '\\' && i + 1 < len) {
463
+ regexStr += result[i] + result[i + 1];
464
+ i += 2;
465
+ } else if (result[i] === '[') {
466
+ inCharClass = true;
467
+ regexStr += result[i];
468
+ i++;
469
+ } else if (result[i] === ']') {
470
+ inCharClass = false;
471
+ regexStr += result[i];
472
+ i++;
473
+ } else if (result[i] === '/' && !inCharClass) {
474
+ regexStr += '/';
475
+ i++;
476
+ while (i < len && /[gimsuy]/.test(result[i])) {
477
+ regexStr += result[i];
478
+ i++;
479
+ }
480
+ break;
481
+ } else if (result[i] === '\n') {
482
+ // Not a valid regex, rollback and treat as division
483
+ i = i - regexStr.length + 1;
484
+ parsedCode += '/';
485
+ break;
486
+ } else {
487
+ regexStr += result[i];
488
+ i++;
489
+ }
490
+ }
491
+ parsedCode += protect(regexStr);
492
+ continue;
493
+ }
494
+
495
+ parsedCode += char;
496
+ i++;
497
+ }
498
+ result = parsedCode;
499
+
500
+ // [Critical Fix 2]: Extract Identifiers (Variables/Functions)
501
+ const identifierRegex = /(?<![.\w$])\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b(?!\s*:)/g;
502
+ const variables = new Set();
503
+ let match;
504
+
505
+ const jsKeywords = new Set([
506
+ 'var', 'let', 'const', 'function', 'return', 'if', 'else', 'for', 'while', 'do',
507
+ 'switch', 'case', 'break', 'continue', 'new', 'delete', 'typeof', 'instanceof',
508
+ 'in', 'of', 'void', 'throw', 'try', 'catch', 'finally', 'class', 'extends',
509
+ 'import', 'export', 'from', 'default', 'async', 'await', 'yield', 'true', 'false',
510
+ 'null', 'undefined', 'this', 'super', 'enum', 'implements', 'interface', 'package',
511
+ 'private', 'protected', 'public', 'static', 'debugger', 'with'
512
+ ]);
513
+
514
+ while ((match = identifierRegex.exec(result)) !== null) {
515
+ const varName = match[1];
516
+
517
+ // [Critical Fix 3]: Skip our own placeholders so they don't get renamed!
518
+ if (/^__PROTECTED_\d+__$/.test(varName)) continue;
519
+
520
+ if (!opts.reserved.includes(varName) &&
521
+ varName.length > 2 &&
522
+ !jsKeywords.has(varName)) {
523
+ variables.add(varName);
524
+ }
525
+ }
526
+
527
+ // Generate obfuscation mapping
528
+ const mapping = new Map();
529
+ let counter = 0;
530
+ variables.forEach(variable => {
531
+ mapping.set(variable, `_${counter.toString(36)}`);
532
+ counter++;
533
+ });
534
+
535
+ // [Critical Fix 4]: Safely Replace Variables
536
+ variables.forEach(variable => {
537
+ const newName = mapping.get(variable);
538
+ const escapedVar = variable.replace(/\$/g, '\\$');
539
+ const regex = new RegExp(`(?<![.\\w$])\\b${escapedVar}\\b(?!\\s*:)`, 'g');
540
+ result = result.replace(regex, newName);
541
+ });
542
+
543
+ // Restore all protected blocks (strings, comments, regex, template text)
544
+ result = result.replace(/__PROTECTED_(\d+)__/g, (match, index) => {
545
+ return protectedBlocks[parseInt(index, 10)];
546
+ });
547
+ }
548
+
549
+ const finalResult = result;
550
+ this.setCached(cacheKey, finalResult);
551
+ return finalResult;
552
+ }
553
+
554
+
555
+
556
+ /**
557
+ * Process HTML content with embedded CSS and JavaScript
558
+ * @param {string} html - HTML content
559
+ * @param {Object} options - Processing options
560
+ * @returns {string} Processed HTML
561
+ */
562
+ processHTML(html, options = {}) {
563
+ const cacheKey = this.generateCacheKey(html, { type: 'full', ...options });
564
+ const cached = this.getCached(cacheKey);
565
+ if (cached) return cached;
566
+
567
+ const opts = { ...this.options, ...options };
568
+ let result = html;
569
+
570
+ // Process embedded CSS
571
+ if (opts.minifyCSS) {
572
+ const styleRegex = /<style\b([^>]*)>([\s\S]*?)<\/style>/gi;
573
+ result = result.replace(styleRegex, (match, attrs, styleContent) => {
574
+ const minifiedCSS = this.minifyCSS(styleContent, opts);
575
+ return match.replace(styleContent, minifiedCSS);
576
+ });
577
+ }
578
+
579
+ // Process embedded JavaScript
580
+ if (opts.minifyJS || opts.mangleJS) {
581
+ // [Critical Fix]: Use a simpler regex and check for 'src' inside the callback.
582
+ // The previous negative lookahead (?![\s\S]*?\bsrc\s*=) would fail if ANY subsequent script had a src.
583
+ const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
584
+ result = result.replace(scriptRegex, (match, attrs, scriptContent) => {
585
+ // Skip external scripts that have a src attribute
586
+ if (/\bsrc\s*=/i.test(attrs)) {
587
+ return match;
588
+ }
589
+
590
+ let processedJS = scriptContent;
591
+ if (opts.minifyJS) {
592
+ processedJS = this.minifyJS(processedJS, opts);
593
+ }
594
+ if (opts.mangleJS) {
595
+ processedJS = this.obfuscateJS(processedJS, opts);
596
+ }
597
+ return match.replace(scriptContent, processedJS);
598
+ });
599
+ }
600
+
601
+ // Process inline styles
602
+ if (opts.minifyCSS) {
603
+ const styleAttrRegex = /style=["']([^"']*)["']/gi;
604
+ result = result.replace(styleAttrRegex, (match, styleContent) => {
605
+ const minifiedCSS = this.minifyCSS(styleContent, opts);
606
+ return `style="${minifiedCSS}"`;
607
+ });
608
+ }
609
+
610
+ // Process inline event handlers
611
+ if (opts.minifyJS) {
612
+ const eventRegex = /(on\w+)=["']([^"']*)["']/gi;
613
+ result = result.replace(eventRegex, (match, eventName, handler) => {
614
+ const minifiedJS = this.minifyJS(handler, opts);
615
+ return `${eventName}="${minifiedJS}"`;
616
+ });
617
+ }
618
+
619
+ // Finally minify the HTML structure
620
+ if (opts.minifyHTML) {
621
+ result = this.minifyHTML(result, opts);
622
+ }
623
+
624
+ this.setCached(cacheKey, result);
625
+ return result;
626
+ }
627
+
628
+
629
+ /**
630
+ * Get compression statistics
631
+ * @returns {Object} Compression statistics
632
+ */
633
+ getStats() {
634
+ return {
635
+ cacheSize: this.compressionCache.size,
636
+ cacheHits: 0, // You can implement hit tracking if needed
637
+ cacheTTL: this.cacheTTL,
638
+ options: this.options
639
+ };
640
+ }
641
+ }
642
+ export default CompressionEngine;
@@ -22,12 +22,21 @@ class SSRModeEngine extends AetherEngine {
22
22
  this.ssrOptions = {
23
23
  hydrate: options.hydrate !== false,
24
24
  stream: options.stream || false,
25
+ // SSR compression options
26
+ compressSSR: options.compressSSR !== false,
27
+ minifySSR: options.minifySSR !== false,
25
28
  ...options
26
29
  };
30
+
31
+ // Merge compression options
32
+ this.options = {
33
+ ...this.options,
34
+ ...this.ssrOptions
35
+ };
27
36
  }
28
37
 
29
- /**
30
- * Render template with SSR optimizations
38
+ /**
39
+ * Render template with SSR optimizations and compression
31
40
  * @param {string} templateName - Template name or content
32
41
  * @param {Object} data - Template data
33
42
  * @param {Object} options - Render options
@@ -38,9 +47,25 @@ class SSRModeEngine extends AetherEngine {
38
47
  const content = await super.render(templateName, data, options);
39
48
 
40
49
  // Wrap and enhance for SSR
41
- return this.enhanceForSSR(content, data, options);
50
+ const enhancedHtml = this.enhanceForSSR(content, data, options);
51
+
52
+ // Apply SSR-specific compression if enabled
53
+ const shouldCompress = this.options.compressionEnabled &&
54
+ (this.ssrOptions.compressSSR ||
55
+ (process.env.NODE_ENV === 'production' && this.ssrOptions.minifySSR));
56
+
57
+ if (shouldCompress) {
58
+ return this.processWithCompression(enhancedHtml, {
59
+ ...options.compression,
60
+ minifyHTML: this.options.minifyHTML,
61
+ minifyCSS: this.options.minifyCSS,
62
+ minifyJS: this.options.minifyJS,
63
+ mangleJS: this.options.mangleJS
64
+ });
65
+ }
66
+
67
+ return enhancedHtml;
42
68
  }
43
-
44
69
  /**
45
70
  * Enhance rendered content for SSR by wrapping it in a full HTML document
46
71
  * @param {string} content - Rendered HTML content