@aetherframework/template-engine 1.0.1 → 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.
- package/README.md +430 -0
- package/index.js +17 -1
- package/package.json +1 -1
- package/src/engines/AetherEngine.js +162 -56
- package/src/engines/CompressionEngine.js +642 -0
- package/src/engines/SSRModeEngine.js +29 -4
- package/src/engines/TemplateModeEngine.js +29 -3
- package/examples/dist/basic-usage-result.html +0 -31
- package/examples/dist/layout-example-result.html +0 -210
- package/examples/dist/templates/layouts/main.aether +0 -58
- package/examples/dist/templates/pages/home.aether +0 -116
|
@@ -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
|
-
|
|
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
|