html-minifier-next 6.0.0 → 6.1.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/cli.js +34 -18
- package/dist/htmlminifier.cjs +126 -67
- package/dist/types/htmlminifier.d.ts +1 -1
- package/dist/types/htmlminifier.d.ts.map +1 -1
- package/dist/types/lib/options.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/htmlminifier.js +118 -65
- package/src/htmlparser.js +1 -1
- package/src/lib/attributes.js +1 -1
- package/src/lib/options.js +6 -4
package/cli.js
CHANGED
|
@@ -173,32 +173,48 @@ function readFile(file) {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
* Load config from a file path
|
|
176
|
+
* Load config from a file path—for unambiguous extensions (.json, .cjs, .mjs) only the
|
|
177
|
+
* matching format is attempted and its error shown on failure; for .js or unknown extensions
|
|
178
|
+
* all formats are tried and the most relevant error is reported
|
|
177
179
|
* @param {string} configPath - Path to config file
|
|
178
180
|
* @returns {Promise<object>} Loaded config object
|
|
179
181
|
*/
|
|
180
182
|
async function loadConfigFromPath(configPath) {
|
|
181
|
-
const
|
|
183
|
+
const abs = path.resolve(configPath);
|
|
184
|
+
const ext = path.extname(configPath).toLowerCase();
|
|
182
185
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
const abs = path.resolve(configPath);
|
|
186
|
+
if (ext === '.json') {
|
|
187
|
+
try { return JSON.parse(readFile(abs).replace(/^\uFEFF/, '')); }
|
|
188
|
+
catch (err) { fatal(`Cannot parse config file as JSON: ${err.message}`); }
|
|
189
|
+
}
|
|
188
190
|
|
|
189
|
-
|
|
191
|
+
if (ext === '.cjs') {
|
|
190
192
|
try {
|
|
191
193
|
const result = require(abs);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
194
|
+
return (result && typeof result === 'object' && result.__esModule === true) ? result.default : result;
|
|
195
|
+
} catch (err) { fatal(`Cannot load config file: ${err.message}`); }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (ext === '.mjs') {
|
|
199
|
+
try { const mod = await import(pathToFileURL(abs).href); return 'default' in mod ? mod.default : mod; }
|
|
200
|
+
catch (err) { fatal(`Cannot load config file: ${err.message}`); }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// For .js or extension-less files, try JSON first, then CJS, then ESM
|
|
204
|
+
let jsonErr;
|
|
205
|
+
try { return JSON.parse(readFile(abs).replace(/^\uFEFF/, '')); }
|
|
206
|
+
catch (err) { jsonErr = err; }
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const result = require(abs);
|
|
210
|
+
// Handle ESM interop: If `require()` loads an ESM file, it may return `{__esModule: true, default: …}`
|
|
211
|
+
return (result && typeof result === 'object' && result.__esModule === true) ? result.default : result;
|
|
212
|
+
} catch (cjsErr) {
|
|
213
|
+
try { const mod = await import(pathToFileURL(abs).href); return 'default' in mod ? mod.default : mod; }
|
|
214
|
+
catch (esmErr) {
|
|
215
|
+
fatal(ext === '.js'
|
|
216
|
+
? `Cannot load config file: ${cjsErr.message}\nAs module: ${esmErr.message}`
|
|
217
|
+
: `Cannot read the specified config file.\nAs JSON: ${jsonErr.message}\nAs CJS: ${cjsErr.message}\nAs module: ${esmErr.message}`);
|
|
202
218
|
}
|
|
203
219
|
}
|
|
204
220
|
}
|
package/dist/htmlminifier.cjs
CHANGED
|
@@ -607,7 +607,7 @@ class HTMLParser {
|
|
|
607
607
|
// Note: Unquoted attribute values are intentionally not handled here.
|
|
608
608
|
// Per HTML spec, unquoted values cannot contain spaces or special chars,
|
|
609
609
|
// making a 20 KB+ unquoted value practically impossible. If encountered,
|
|
610
|
-
// it
|
|
610
|
+
// it’s malformed HTML and using the truncated regex match is acceptable.
|
|
611
611
|
}
|
|
612
612
|
}
|
|
613
613
|
}
|
|
@@ -1893,7 +1893,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1893
1893
|
cont: !!options.continueOnMinifyError
|
|
1894
1894
|
});
|
|
1895
1895
|
|
|
1896
|
-
options.minifyJS = async function (text, inline) {
|
|
1896
|
+
options.minifyJS = async function (text, inline, isModule) {
|
|
1897
1897
|
const start = text.match(/^\s*<!--.*/);
|
|
1898
1898
|
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
1899
1899
|
|
|
@@ -1913,7 +1913,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1913
1913
|
|
|
1914
1914
|
// For large inputs, use length and content fingerprint to prevent collisions
|
|
1915
1915
|
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|'))
|
|
1916
|
-
+ (inline ? '1' : '0') + '|' + useEngine + '|' + optsSig;
|
|
1916
|
+
+ (inline ? '1' : '0') + '|' + (isModule ? 'm' : '') + '|' + useEngine + '|' + optsSig;
|
|
1917
1917
|
|
|
1918
1918
|
const cached = jsMinifyCache.get(jsKey);
|
|
1919
1919
|
if (cached) {
|
|
@@ -1929,7 +1929,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1929
1929
|
parse: {
|
|
1930
1930
|
...terserOptions.parse,
|
|
1931
1931
|
bare_returns: inline
|
|
1932
|
-
}
|
|
1932
|
+
},
|
|
1933
|
+
...(isModule ? { module: true } : {}) // Overrides user options: module detection takes precedence for `<script type=module>`
|
|
1933
1934
|
};
|
|
1934
1935
|
const terser = await getTerser();
|
|
1935
1936
|
const result = await terser(code, terserCallOptions);
|
|
@@ -1940,7 +1941,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
1940
1941
|
const result = await swc.minify(code, {
|
|
1941
1942
|
compress: true,
|
|
1942
1943
|
mangle: true,
|
|
1943
|
-
...swcOptions,
|
|
1944
|
+
...swcOptions,
|
|
1945
|
+
...(isModule ? { module: true } : {}) // Overrides user options: module detection takes precedence for `<script type=module>`
|
|
1944
1946
|
});
|
|
1945
1947
|
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
1946
1948
|
}
|
|
@@ -2630,7 +2632,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
2630
2632
|
attrValue = attrValue.replace(/'/g, ''');
|
|
2631
2633
|
}
|
|
2632
2634
|
} else {
|
|
2633
|
-
// `preventAttributesEscaping` mode: Choose safe quotes but don
|
|
2635
|
+
// `preventAttributesEscaping` mode: Choose safe quotes but don’t escape
|
|
2634
2636
|
// except when both quote types are present—then escape to prevent invalid HTML
|
|
2635
2637
|
const hasDoubleQuote = attrValue.indexOf('"') !== -1;
|
|
2636
2638
|
const hasSingleQuote = attrValue.indexOf("'") !== -1;
|
|
@@ -3000,6 +3002,8 @@ let svgMinifyCache = null;
|
|
|
3000
3002
|
|
|
3001
3003
|
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
3002
3004
|
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
3005
|
+
const RE_SCRIPT_OPEN = /<script(?=[\s>])/gi; // Finds tag start; use `findTagEnd()` for the actual closing `>`
|
|
3006
|
+
const RE_SCRIPT_CLOSE = /<\/script\s*>/gi;
|
|
3003
3007
|
const SCRIPT_BOOL_ATTRS = new Set(['async', 'defer', 'nomodule']);
|
|
3004
3008
|
const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript']);
|
|
3005
3009
|
|
|
@@ -3012,6 +3016,28 @@ const RE_HTML_ENCODING = /^(text\/html|application\/xhtml\+xml)$/i;
|
|
|
3012
3016
|
|
|
3013
3017
|
// Script merging
|
|
3014
3018
|
|
|
3019
|
+
/**
|
|
3020
|
+
* Find the index of the `>` that closes an opening tag, correctly skipping
|
|
3021
|
+
* over quoted attribute values (which may contain `>`).
|
|
3022
|
+
* @param {string} html
|
|
3023
|
+
* @param {number} pos - Start position (just after the tag name)
|
|
3024
|
+
* @returns {number} Index of the closing `>`, or -1 if not found
|
|
3025
|
+
*/
|
|
3026
|
+
function findTagEnd(html, pos) {
|
|
3027
|
+
let i = pos;
|
|
3028
|
+
while (i < html.length) {
|
|
3029
|
+
const ch = html[i];
|
|
3030
|
+
if (ch === '>') return i;
|
|
3031
|
+
if (ch === '"' || ch === "'") {
|
|
3032
|
+
const q = ch;
|
|
3033
|
+
i++;
|
|
3034
|
+
while (i < html.length && html[i] !== q) i++;
|
|
3035
|
+
}
|
|
3036
|
+
i++;
|
|
3037
|
+
}
|
|
3038
|
+
return -1;
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3015
3041
|
/**
|
|
3016
3042
|
* Merge consecutive inline script tags into one (`mergeConsecutiveScripts`).
|
|
3017
3043
|
* Only merges scripts that are compatible:
|
|
@@ -3019,77 +3045,104 @@ const RE_HTML_ENCODING = /^(text\/html|application\/xhtml\+xml)$/i;
|
|
|
3019
3045
|
* - Same `type` (or both default JavaScript)
|
|
3020
3046
|
* - No conflicting attributes (`async`, `defer`, `nomodule`, different `nonce`)
|
|
3021
3047
|
*
|
|
3022
|
-
*
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3025
|
-
* HTML, such strings should be escaped as `<\/script>` or split like
|
|
3026
|
-
* `'</scr' + 'ipt>'`, so this limitation rarely affects real-world code. The
|
|
3027
|
-
* earlier `minifyJS` step (if enabled) typically handles this escaping already.
|
|
3048
|
+
* Uses a scanner rather than a regex to locate script boundaries, so literal
|
|
3049
|
+
* `</script>` strings inside script content are handled correctly per the HTML
|
|
3050
|
+
* spec (raw text ends at the first `</script>`).
|
|
3028
3051
|
*
|
|
3029
3052
|
* @param {string} html - The HTML string to process
|
|
3030
3053
|
* @returns {string} HTML with consecutive scripts merged
|
|
3031
3054
|
*/
|
|
3032
3055
|
function mergeConsecutiveScripts(html) {
|
|
3033
|
-
//
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3056
|
+
// Parse an attribute string into a name→value map
|
|
3057
|
+
const parseAttrs = (attrStr) => {
|
|
3058
|
+
const attrs = {};
|
|
3059
|
+
RE_SCRIPT_ATTRS.lastIndex = 0;
|
|
3060
|
+
let m;
|
|
3061
|
+
while ((m = RE_SCRIPT_ATTRS.exec(attrStr)) !== null) {
|
|
3062
|
+
const name = m[1].toLowerCase();
|
|
3063
|
+
const value = m[2] ?? m[3] ?? m[4] ?? '';
|
|
3064
|
+
attrs[name] = value;
|
|
3065
|
+
}
|
|
3066
|
+
return attrs;
|
|
3067
|
+
};
|
|
3068
|
+
|
|
3044
3069
|
let changed = true;
|
|
3045
3070
|
|
|
3046
3071
|
// Keep merging until no more changes (handles chains of 3+ scripts)
|
|
3047
3072
|
while (changed) {
|
|
3048
3073
|
changed = false;
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3074
|
+
RE_SCRIPT_OPEN.lastIndex = 0;
|
|
3075
|
+
let m1;
|
|
3076
|
+
|
|
3077
|
+
while ((m1 = RE_SCRIPT_OPEN.exec(html)) !== null) {
|
|
3078
|
+
// Use findTagEnd() to get the real closing '>', skipping quoted attribute values
|
|
3079
|
+
const tagEnd1 = findTagEnd(html, m1.index + 7);
|
|
3080
|
+
if (tagEnd1 === -1) break;
|
|
3081
|
+
|
|
3082
|
+
const attrs1Str = html.slice(m1.index + 7, tagEnd1);
|
|
3083
|
+
const contentStart1 = tagEnd1 + 1;
|
|
3084
|
+
|
|
3085
|
+
// Find end of this script’s content (first `</script>`—per HTML spec, raw text ends here)
|
|
3086
|
+
RE_SCRIPT_CLOSE.lastIndex = contentStart1;
|
|
3087
|
+
const close1 = RE_SCRIPT_CLOSE.exec(html);
|
|
3088
|
+
if (!close1) break;
|
|
3089
|
+
|
|
3090
|
+
const content1 = html.slice(contentStart1, close1.index);
|
|
3091
|
+
const afterClose1 = close1.index + close1[0].length;
|
|
3092
|
+
|
|
3093
|
+
// Skip optional whitespace and check for a consecutive <script> tag
|
|
3094
|
+
let i = afterClose1;
|
|
3095
|
+
while (i < html.length && (html[i] === ' ' || html[i] === '\t' || html[i] === '\n' || html[i] === '\r' || html[i] === '\f')) i++;
|
|
3096
|
+
if (html.slice(i, i + 7).toLowerCase() !== '<script' || (html[i + 7] !== '>' && !/\s/.test(html[i + 7]))) {
|
|
3097
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
const tagStart2 = i;
|
|
3102
|
+
const tagEnd2 = findTagEnd(html, tagStart2 + 7);
|
|
3103
|
+
if (tagEnd2 === -1) break;
|
|
3104
|
+
|
|
3105
|
+
const attrs2Str = html.slice(tagStart2 + 7, tagEnd2);
|
|
3106
|
+
const contentStart2 = tagEnd2 + 1;
|
|
3107
|
+
|
|
3108
|
+
// Find end of second script’s content
|
|
3109
|
+
RE_SCRIPT_CLOSE.lastIndex = contentStart2;
|
|
3110
|
+
const close2 = RE_SCRIPT_CLOSE.exec(html);
|
|
3111
|
+
if (!close2) break;
|
|
3062
3112
|
|
|
3063
|
-
const
|
|
3064
|
-
const
|
|
3113
|
+
const content2 = html.slice(contentStart2, close2.index);
|
|
3114
|
+
const afterClose2 = close2.index + close2[0].length;
|
|
3115
|
+
|
|
3116
|
+
const a1 = parseAttrs(attrs1Str);
|
|
3117
|
+
const a2 = parseAttrs(attrs2Str);
|
|
3065
3118
|
|
|
3066
3119
|
// Check for `src`—cannot merge external scripts
|
|
3067
3120
|
if ('src' in a1 || 'src' in a2) {
|
|
3068
|
-
|
|
3121
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3122
|
+
continue;
|
|
3069
3123
|
}
|
|
3070
3124
|
|
|
3071
|
-
// Check `type` compatibility (both must be
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3125
|
+
// Check `type` compatibility (both must be default JS)
|
|
3126
|
+
// Non-JS types (modules, JSON, etc.) must not be merged:
|
|
3127
|
+
// Module scripts have per-script lexical scope, and non-JS content (e.g., JSON)
|
|
3128
|
+
// is not concatenable; even identical non-JS types are incompatible
|
|
3129
|
+
const type1 = (a1.type || '').toLowerCase();
|
|
3130
|
+
const type2 = (a2.type || '').toLowerCase();
|
|
3131
|
+
if (!DEFAULT_JS_TYPES.has(type1) || !DEFAULT_JS_TYPES.has(type2)) {
|
|
3132
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3133
|
+
continue;
|
|
3078
3134
|
}
|
|
3079
3135
|
|
|
3080
|
-
// Check for conflicting boolean attributes
|
|
3136
|
+
// Check for conflicting boolean attributes
|
|
3137
|
+
let boolConflict = false;
|
|
3081
3138
|
for (const attr of SCRIPT_BOOL_ATTRS) {
|
|
3082
|
-
|
|
3083
|
-
const has2 = attr in a2;
|
|
3084
|
-
if (has1 !== has2) {
|
|
3085
|
-
// One has it, one doesn't - incompatible
|
|
3086
|
-
return match;
|
|
3087
|
-
}
|
|
3139
|
+
if ((attr in a1) !== (attr in a2)) { boolConflict = true; break; }
|
|
3088
3140
|
}
|
|
3089
3141
|
|
|
3090
3142
|
// Check `nonce`—must be same or both absent
|
|
3091
|
-
if (a1.nonce !== a2.nonce) {
|
|
3092
|
-
|
|
3143
|
+
if (boolConflict || a1.nonce !== a2.nonce) {
|
|
3144
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
3145
|
+
continue;
|
|
3093
3146
|
}
|
|
3094
3147
|
|
|
3095
3148
|
// Scripts are compatible—merge them
|
|
@@ -3110,11 +3163,12 @@ function mergeConsecutiveScripts(html) {
|
|
|
3110
3163
|
}
|
|
3111
3164
|
|
|
3112
3165
|
// Use first script’s attributes (they should be compatible)
|
|
3113
|
-
|
|
3114
|
-
|
|
3166
|
+
html = html.slice(0, m1.index) + `<script${attrs1Str}>${mergedContent}</script>` + html.slice(afterClose2);
|
|
3167
|
+
break; // Restart scanning (outer while loop)
|
|
3168
|
+
}
|
|
3115
3169
|
}
|
|
3116
3170
|
|
|
3117
|
-
return
|
|
3171
|
+
return html;
|
|
3118
3172
|
}
|
|
3119
3173
|
|
|
3120
3174
|
// Type definitions
|
|
@@ -3340,7 +3394,7 @@ function mergeConsecutiveScripts(html) {
|
|
|
3340
3394
|
* event handler attributes. If an object is provided, it can include:
|
|
3341
3395
|
* - `engine`: The minifier to use (`terser` or `swc`). Default: `terser`.
|
|
3342
3396
|
* Note: Inline event handlers (e.g., `onclick="…"`) always use Terser
|
|
3343
|
-
* regardless of engine setting, as
|
|
3397
|
+
* regardless of engine setting, as SWC doesn’t support bare return statements.
|
|
3344
3398
|
* - Engine-specific options (e.g., Terser options if `engine: 'terser'`,
|
|
3345
3399
|
* SWC options if `engine: 'swc'`).
|
|
3346
3400
|
* If a function is provided, it will be used to perform
|
|
@@ -3887,11 +3941,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
3887
3941
|
|
|
3888
3942
|
if (options.minifyJS) {
|
|
3889
3943
|
options.minifyJS = (function (fn) {
|
|
3890
|
-
return function (text,
|
|
3944
|
+
return function (text, inline, isModule) {
|
|
3891
3945
|
return fn(text.replace(uidPattern, function (match, prefix, index) {
|
|
3892
3946
|
const chunks = ignoredCustomMarkupChunks[+index];
|
|
3893
3947
|
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
3894
|
-
}),
|
|
3948
|
+
}), inline, isModule);
|
|
3895
3949
|
};
|
|
3896
3950
|
})(options.minifyJS);
|
|
3897
3951
|
}
|
|
@@ -4214,6 +4268,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4214
4268
|
const needsDecode = options.decodeEntities && text && !specialContentElements.has(currentTag) && text.indexOf('&') !== -1;
|
|
4215
4269
|
const needsProcessScript = specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs));
|
|
4216
4270
|
const needsMinifyJS = options.minifyJS !== identity && isExecutableScript(currentTag, currentAttrs);
|
|
4271
|
+
const isModuleScript = needsMinifyJS && currentAttrs.some(
|
|
4272
|
+
a => a.name.toLowerCase() === 'type' && (a.value ?? '').trim().toLowerCase() === 'module'
|
|
4273
|
+
);
|
|
4217
4274
|
const needsMinifyCSS = options.minifyCSS !== identity && isStyleElement(currentTag, currentAttrs);
|
|
4218
4275
|
|
|
4219
4276
|
// Whitespace collapsing phase (sync); captures `prevTag`/`nextTag`/`prevAttrs`/`nextAttrs` from outer scope
|
|
@@ -4340,7 +4397,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
4340
4397
|
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
4341
4398
|
}
|
|
4342
4399
|
if (needsMinifyJS) {
|
|
4343
|
-
text = await options.minifyJS(text);
|
|
4400
|
+
text = await options.minifyJS(text, false, isModuleScript);
|
|
4344
4401
|
}
|
|
4345
4402
|
if (needsMinifyCSS) {
|
|
4346
4403
|
text = await options.minifyCSS(text);
|
|
@@ -4557,7 +4614,7 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
4557
4614
|
* - Cache sizes are locked after first initialization—subsequent calls use the same caches
|
|
4558
4615
|
* even if different `cacheCSS`/`cacheJS`/`cacheSVG` options are provided
|
|
4559
4616
|
* - The first call’s options determine the cache sizes for subsequent calls
|
|
4560
|
-
* -
|
|
4617
|
+
* - Invalid values (NaN, Infinity) fall back to the default size (500); values below `1` are clamped to `1`
|
|
4561
4618
|
*/
|
|
4562
4619
|
function initCaches(options) {
|
|
4563
4620
|
// Only create caches once (on first call)—sizes are locked after this
|
|
@@ -4574,6 +4631,9 @@ function initCaches(options) {
|
|
|
4574
4631
|
return parsed;
|
|
4575
4632
|
};
|
|
4576
4633
|
|
|
4634
|
+
// Sanitize a cache size: Non-finite/NaN falls back to `defaultSize`; otherwise clamped to min 1 and floored
|
|
4635
|
+
const sanitizeSize = (size) => Number.isFinite(size) ? Math.max(1, Math.floor(size)) : defaultSize;
|
|
4636
|
+
|
|
4577
4637
|
// Get cache sizes with precedence: Options > env > default
|
|
4578
4638
|
const cssSize = options.cacheCSS !== undefined ? options.cacheCSS
|
|
4579
4639
|
: (parseEnvCacheSize(process.env.HMN_CACHE_CSS) ?? defaultSize);
|
|
@@ -4582,10 +4642,9 @@ function initCaches(options) {
|
|
|
4582
4642
|
const svgSize = options.cacheSVG !== undefined ? options.cacheSVG
|
|
4583
4643
|
: (parseEnvCacheSize(process.env.HMN_CACHE_SVG) ?? defaultSize);
|
|
4584
4644
|
|
|
4585
|
-
|
|
4586
|
-
const
|
|
4587
|
-
const
|
|
4588
|
-
const svgFinalSize = svgSize === 0 ? 1 : svgSize;
|
|
4645
|
+
const cssFinalSize = sanitizeSize(cssSize);
|
|
4646
|
+
const jsFinalSize = sanitizeSize(jsSize);
|
|
4647
|
+
const svgFinalSize = sanitizeSize(svgSize);
|
|
4589
4648
|
|
|
4590
4649
|
cssMinifyCache = new LRU(cssFinalSize);
|
|
4591
4650
|
jsMinifyCache = new LRU(jsFinalSize);
|
|
@@ -254,7 +254,7 @@ export type MinifierOptions = {
|
|
|
254
254
|
* event handler attributes. If an object is provided, it can include:
|
|
255
255
|
* - `engine`: The minifier to use (`terser` or `swc`). Default: `terser`.
|
|
256
256
|
* Note: Inline event handlers (e.g., `onclick="…"`) always use Terser
|
|
257
|
-
* regardless of engine setting, as
|
|
257
|
+
* regardless of engine setting, as SWC doesn’t support bare return statements.
|
|
258
258
|
* - Engine-specific options (e.g., Terser options if `engine: 'terser'`,
|
|
259
259
|
* SWC options if `engine: 'swc'`).
|
|
260
260
|
* If a function is provided, it will be used to perform
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"htmlminifier.d.ts","sourceRoot":"","sources":["../../src/htmlminifier.js"],"names":[],"mappings":"AA2uDO,8BAJI,MAAM,YACN,eAAe,GACb,OAAO,CAAC,MAAM,CAAC,CAwB3B;;;;;;;;;;;;UAh+CS,MAAM;;;;;;;;;;;;;;;;;;mCAaA,MAAM,SAAS,aAAa,EAAE,yBAAyB,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;+BAM3F,MAAM,GAAG,IAAI,SAAS,aAAa,EAAE,GAAG,SAAS,qBAAqB,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA6JtG,OAAO,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2HA2BiF,MAAM,SAAS,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM;;;;;;;;;;;;;;;;iBASxG,QAAQ,GAAG,KAAK;gBAAgC,MAAM,WAAW,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM;;;;;;;;;;;eAa/H,MAAM;gBAAY,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM;;;;;;;;;;;;;;;;;mBAiBzE,MAAM,KAAK,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kDA+DF,MAAM,OAAO,MAAM,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sCA2EpC,MAAM,SAAS,aAAa,EAAE,KAAK,IAAI;;;;;;;;;wCAQrC,MAAM,KAAK,MAAM;;;;;;;;;;;;;;;;;wBAjqBK,cAAc;0BAAd,cAAc;+BAAd,cAAc"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAYA,6DAUC;AAID;;;;;;;;;;;GAWG;AACH,6CAXW,OAAO,CAAC,eAAe,CAAC,mGAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAK9C,eAAe,
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/options.js"],"names":[],"mappings":"AAYA,6DAUC;AAID;;;;;;;;;;;GAWG;AACH,6CAXW,OAAO,CAAC,eAAe,CAAC,mGAEhC;IAAuB,eAAe;IACf,SAAS;IACT,MAAM;CAA2B,GAK9C,eAAe,CAiX3B"}
|
package/package.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"rollup": "^4.60.0",
|
|
24
24
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
25
25
|
"typescript": "^6.0.2",
|
|
26
|
-
"vite": "^8.0.
|
|
26
|
+
"vite": "^8.0.5"
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|
|
29
29
|
".": {
|
|
@@ -96,5 +96,5 @@
|
|
|
96
96
|
},
|
|
97
97
|
"type": "module",
|
|
98
98
|
"types": "./dist/types/htmlminifier.d.ts",
|
|
99
|
-
"version": "6.
|
|
99
|
+
"version": "6.1.2"
|
|
100
100
|
}
|
package/src/htmlminifier.js
CHANGED
|
@@ -112,6 +112,8 @@ let svgMinifyCache = null;
|
|
|
112
112
|
|
|
113
113
|
// Pre-compiled patterns for script merging (avoid repeated allocation in hot path)
|
|
114
114
|
const RE_SCRIPT_ATTRS = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
115
|
+
const RE_SCRIPT_OPEN = /<script(?=[\s>])/gi; // Finds tag start; use `findTagEnd()` for the actual closing `>`
|
|
116
|
+
const RE_SCRIPT_CLOSE = /<\/script\s*>/gi;
|
|
115
117
|
const SCRIPT_BOOL_ATTRS = new Set(['async', 'defer', 'nomodule']);
|
|
116
118
|
const DEFAULT_JS_TYPES = new Set(['', 'text/javascript', 'application/javascript']);
|
|
117
119
|
|
|
@@ -124,6 +126,28 @@ const RE_HTML_ENCODING = /^(text\/html|application\/xhtml\+xml)$/i;
|
|
|
124
126
|
|
|
125
127
|
// Script merging
|
|
126
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Find the index of the `>` that closes an opening tag, correctly skipping
|
|
131
|
+
* over quoted attribute values (which may contain `>`).
|
|
132
|
+
* @param {string} html
|
|
133
|
+
* @param {number} pos - Start position (just after the tag name)
|
|
134
|
+
* @returns {number} Index of the closing `>`, or -1 if not found
|
|
135
|
+
*/
|
|
136
|
+
function findTagEnd(html, pos) {
|
|
137
|
+
let i = pos;
|
|
138
|
+
while (i < html.length) {
|
|
139
|
+
const ch = html[i];
|
|
140
|
+
if (ch === '>') return i;
|
|
141
|
+
if (ch === '"' || ch === "'") {
|
|
142
|
+
const q = ch;
|
|
143
|
+
i++;
|
|
144
|
+
while (i < html.length && html[i] !== q) i++;
|
|
145
|
+
}
|
|
146
|
+
i++;
|
|
147
|
+
}
|
|
148
|
+
return -1;
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
/**
|
|
128
152
|
* Merge consecutive inline script tags into one (`mergeConsecutiveScripts`).
|
|
129
153
|
* Only merges scripts that are compatible:
|
|
@@ -131,81 +155,104 @@ const RE_HTML_ENCODING = /^(text\/html|application\/xhtml\+xml)$/i;
|
|
|
131
155
|
* - Same `type` (or both default JavaScript)
|
|
132
156
|
* - No conflicting attributes (`async`, `defer`, `nomodule`, different `nonce`)
|
|
133
157
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* HTML, such strings should be escaped as `<\/script>` or split like
|
|
138
|
-
* `'</scr' + 'ipt>'`, so this limitation rarely affects real-world code. The
|
|
139
|
-
* earlier `minifyJS` step (if enabled) typically handles this escaping already.
|
|
158
|
+
* Uses a scanner rather than a regex to locate script boundaries, so literal
|
|
159
|
+
* `</script>` strings inside script content are handled correctly per the HTML
|
|
160
|
+
* spec (raw text ends at the first `</script>`).
|
|
140
161
|
*
|
|
141
162
|
* @param {string} html - The HTML string to process
|
|
142
163
|
* @returns {string} HTML with consecutive scripts merged
|
|
143
164
|
*/
|
|
144
165
|
function mergeConsecutiveScripts(html) {
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
166
|
+
// Parse an attribute string into a name→value map
|
|
167
|
+
const parseAttrs = (attrStr) => {
|
|
168
|
+
const attrs = {};
|
|
169
|
+
RE_SCRIPT_ATTRS.lastIndex = 0;
|
|
170
|
+
let m;
|
|
171
|
+
while ((m = RE_SCRIPT_ATTRS.exec(attrStr)) !== null) {
|
|
172
|
+
const name = m[1].toLowerCase();
|
|
173
|
+
const value = m[2] ?? m[3] ?? m[4] ?? '';
|
|
174
|
+
attrs[name] = value;
|
|
175
|
+
}
|
|
176
|
+
return attrs;
|
|
177
|
+
};
|
|
178
|
+
|
|
156
179
|
let changed = true;
|
|
157
180
|
|
|
158
181
|
// Keep merging until no more changes (handles chains of 3+ scripts)
|
|
159
182
|
while (changed) {
|
|
160
183
|
changed = false;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
RE_SCRIPT_OPEN.lastIndex = 0;
|
|
185
|
+
let m1;
|
|
186
|
+
|
|
187
|
+
while ((m1 = RE_SCRIPT_OPEN.exec(html)) !== null) {
|
|
188
|
+
// Use findTagEnd() to get the real closing '>', skipping quoted attribute values
|
|
189
|
+
const tagEnd1 = findTagEnd(html, m1.index + 7);
|
|
190
|
+
if (tagEnd1 === -1) break;
|
|
191
|
+
|
|
192
|
+
const attrs1Str = html.slice(m1.index + 7, tagEnd1);
|
|
193
|
+
const contentStart1 = tagEnd1 + 1;
|
|
194
|
+
|
|
195
|
+
// Find end of this script’s content (first `</script>`—per HTML spec, raw text ends here)
|
|
196
|
+
RE_SCRIPT_CLOSE.lastIndex = contentStart1;
|
|
197
|
+
const close1 = RE_SCRIPT_CLOSE.exec(html);
|
|
198
|
+
if (!close1) break;
|
|
199
|
+
|
|
200
|
+
const content1 = html.slice(contentStart1, close1.index);
|
|
201
|
+
const afterClose1 = close1.index + close1[0].length;
|
|
202
|
+
|
|
203
|
+
// Skip optional whitespace and check for a consecutive <script> tag
|
|
204
|
+
let i = afterClose1;
|
|
205
|
+
while (i < html.length && (html[i] === ' ' || html[i] === '\t' || html[i] === '\n' || html[i] === '\r' || html[i] === '\f')) i++;
|
|
206
|
+
if (html.slice(i, i + 7).toLowerCase() !== '<script' || (html[i + 7] !== '>' && !/\s/.test(html[i + 7]))) {
|
|
207
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tagStart2 = i;
|
|
212
|
+
const tagEnd2 = findTagEnd(html, tagStart2 + 7);
|
|
213
|
+
if (tagEnd2 === -1) break;
|
|
174
214
|
|
|
175
|
-
const
|
|
176
|
-
const
|
|
215
|
+
const attrs2Str = html.slice(tagStart2 + 7, tagEnd2);
|
|
216
|
+
const contentStart2 = tagEnd2 + 1;
|
|
217
|
+
|
|
218
|
+
// Find end of second script’s content
|
|
219
|
+
RE_SCRIPT_CLOSE.lastIndex = contentStart2;
|
|
220
|
+
const close2 = RE_SCRIPT_CLOSE.exec(html);
|
|
221
|
+
if (!close2) break;
|
|
222
|
+
|
|
223
|
+
const content2 = html.slice(contentStart2, close2.index);
|
|
224
|
+
const afterClose2 = close2.index + close2[0].length;
|
|
225
|
+
|
|
226
|
+
const a1 = parseAttrs(attrs1Str);
|
|
227
|
+
const a2 = parseAttrs(attrs2Str);
|
|
177
228
|
|
|
178
229
|
// Check for `src`—cannot merge external scripts
|
|
179
230
|
if ('src' in a1 || 'src' in a2) {
|
|
180
|
-
|
|
231
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
232
|
+
continue;
|
|
181
233
|
}
|
|
182
234
|
|
|
183
|
-
// Check `type` compatibility (both must be
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// Incompatible types
|
|
193
|
-
return match;
|
|
235
|
+
// Check `type` compatibility (both must be default JS)
|
|
236
|
+
// Non-JS types (modules, JSON, etc.) must not be merged:
|
|
237
|
+
// Module scripts have per-script lexical scope, and non-JS content (e.g., JSON)
|
|
238
|
+
// is not concatenable; even identical non-JS types are incompatible
|
|
239
|
+
const type1 = (a1.type || '').toLowerCase();
|
|
240
|
+
const type2 = (a2.type || '').toLowerCase();
|
|
241
|
+
if (!DEFAULT_JS_TYPES.has(type1) || !DEFAULT_JS_TYPES.has(type2)) {
|
|
242
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
243
|
+
continue;
|
|
194
244
|
}
|
|
195
245
|
|
|
196
|
-
// Check for conflicting boolean attributes
|
|
246
|
+
// Check for conflicting boolean attributes
|
|
247
|
+
let boolConflict = false;
|
|
197
248
|
for (const attr of SCRIPT_BOOL_ATTRS) {
|
|
198
|
-
|
|
199
|
-
const has2 = attr in a2;
|
|
200
|
-
if (has1 !== has2) {
|
|
201
|
-
// One has it, one doesn't - incompatible
|
|
202
|
-
return match;
|
|
203
|
-
}
|
|
249
|
+
if ((attr in a1) !== (attr in a2)) { boolConflict = true; break; }
|
|
204
250
|
}
|
|
205
251
|
|
|
206
252
|
// Check `nonce`—must be same or both absent
|
|
207
|
-
if (a1.nonce !== a2.nonce) {
|
|
208
|
-
|
|
253
|
+
if (boolConflict || a1.nonce !== a2.nonce) {
|
|
254
|
+
RE_SCRIPT_OPEN.lastIndex = afterClose1;
|
|
255
|
+
continue;
|
|
209
256
|
}
|
|
210
257
|
|
|
211
258
|
// Scripts are compatible—merge them
|
|
@@ -226,11 +273,12 @@ function mergeConsecutiveScripts(html) {
|
|
|
226
273
|
}
|
|
227
274
|
|
|
228
275
|
// Use first script’s attributes (they should be compatible)
|
|
229
|
-
|
|
230
|
-
|
|
276
|
+
html = html.slice(0, m1.index) + `<script${attrs1Str}>${mergedContent}</script>` + html.slice(afterClose2);
|
|
277
|
+
break; // Restart scanning (outer while loop)
|
|
278
|
+
}
|
|
231
279
|
}
|
|
232
280
|
|
|
233
|
-
return
|
|
281
|
+
return html;
|
|
234
282
|
}
|
|
235
283
|
|
|
236
284
|
// Type definitions
|
|
@@ -456,7 +504,7 @@ function mergeConsecutiveScripts(html) {
|
|
|
456
504
|
* event handler attributes. If an object is provided, it can include:
|
|
457
505
|
* - `engine`: The minifier to use (`terser` or `swc`). Default: `terser`.
|
|
458
506
|
* Note: Inline event handlers (e.g., `onclick="…"`) always use Terser
|
|
459
|
-
* regardless of engine setting, as
|
|
507
|
+
* regardless of engine setting, as SWC doesn’t support bare return statements.
|
|
460
508
|
* - Engine-specific options (e.g., Terser options if `engine: 'terser'`,
|
|
461
509
|
* SWC options if `engine: 'swc'`).
|
|
462
510
|
* If a function is provided, it will be used to perform
|
|
@@ -1003,11 +1051,11 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1003
1051
|
|
|
1004
1052
|
if (options.minifyJS) {
|
|
1005
1053
|
options.minifyJS = (function (fn) {
|
|
1006
|
-
return function (text,
|
|
1054
|
+
return function (text, inline, isModule) {
|
|
1007
1055
|
return fn(text.replace(uidPattern, function (match, prefix, index) {
|
|
1008
1056
|
const chunks = ignoredCustomMarkupChunks[+index];
|
|
1009
1057
|
return chunks[1] + uidAttr + index + uidAttr + chunks[2];
|
|
1010
|
-
}),
|
|
1058
|
+
}), inline, isModule);
|
|
1011
1059
|
};
|
|
1012
1060
|
})(options.minifyJS);
|
|
1013
1061
|
}
|
|
@@ -1330,6 +1378,9 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1330
1378
|
const needsDecode = options.decodeEntities && text && !specialContentElements.has(currentTag) && text.indexOf('&') !== -1;
|
|
1331
1379
|
const needsProcessScript = specialContentElements.has(currentTag) && (options.processScripts || hasJsonScriptType(currentAttrs));
|
|
1332
1380
|
const needsMinifyJS = options.minifyJS !== identity && isExecutableScript(currentTag, currentAttrs);
|
|
1381
|
+
const isModuleScript = needsMinifyJS && currentAttrs.some(
|
|
1382
|
+
a => a.name.toLowerCase() === 'type' && (a.value ?? '').trim().toLowerCase() === 'module'
|
|
1383
|
+
);
|
|
1333
1384
|
const needsMinifyCSS = options.minifyCSS !== identity && isStyleElement(currentTag, currentAttrs);
|
|
1334
1385
|
|
|
1335
1386
|
// Whitespace collapsing phase (sync); captures `prevTag`/`nextTag`/`prevAttrs`/`nextAttrs` from outer scope
|
|
@@ -1456,7 +1507,7 @@ async function minifyHTML(value, options, partialMarkup) {
|
|
|
1456
1507
|
text = await processScript(text, options, currentAttrs, minifyHTML);
|
|
1457
1508
|
}
|
|
1458
1509
|
if (needsMinifyJS) {
|
|
1459
|
-
text = await options.minifyJS(text);
|
|
1510
|
+
text = await options.minifyJS(text, false, isModuleScript);
|
|
1460
1511
|
}
|
|
1461
1512
|
if (needsMinifyCSS) {
|
|
1462
1513
|
text = await options.minifyCSS(text);
|
|
@@ -1673,7 +1724,7 @@ function joinResultSegments(results, options, restoreCustom, restoreIgnore) {
|
|
|
1673
1724
|
* - Cache sizes are locked after first initialization—subsequent calls use the same caches
|
|
1674
1725
|
* even if different `cacheCSS`/`cacheJS`/`cacheSVG` options are provided
|
|
1675
1726
|
* - The first call’s options determine the cache sizes for subsequent calls
|
|
1676
|
-
* -
|
|
1727
|
+
* - Invalid values (NaN, Infinity) fall back to the default size (500); values below `1` are clamped to `1`
|
|
1677
1728
|
*/
|
|
1678
1729
|
function initCaches(options) {
|
|
1679
1730
|
// Only create caches once (on first call)—sizes are locked after this
|
|
@@ -1690,6 +1741,9 @@ function initCaches(options) {
|
|
|
1690
1741
|
return parsed;
|
|
1691
1742
|
};
|
|
1692
1743
|
|
|
1744
|
+
// Sanitize a cache size: Non-finite/NaN falls back to `defaultSize`; otherwise clamped to min 1 and floored
|
|
1745
|
+
const sanitizeSize = (size) => Number.isFinite(size) ? Math.max(1, Math.floor(size)) : defaultSize;
|
|
1746
|
+
|
|
1693
1747
|
// Get cache sizes with precedence: Options > env > default
|
|
1694
1748
|
const cssSize = options.cacheCSS !== undefined ? options.cacheCSS
|
|
1695
1749
|
: (parseEnvCacheSize(process.env.HMN_CACHE_CSS) ?? defaultSize);
|
|
@@ -1698,10 +1752,9 @@ function initCaches(options) {
|
|
|
1698
1752
|
const svgSize = options.cacheSVG !== undefined ? options.cacheSVG
|
|
1699
1753
|
: (parseEnvCacheSize(process.env.HMN_CACHE_SVG) ?? defaultSize);
|
|
1700
1754
|
|
|
1701
|
-
|
|
1702
|
-
const
|
|
1703
|
-
const
|
|
1704
|
-
const svgFinalSize = svgSize === 0 ? 1 : svgSize;
|
|
1755
|
+
const cssFinalSize = sanitizeSize(cssSize);
|
|
1756
|
+
const jsFinalSize = sanitizeSize(jsSize);
|
|
1757
|
+
const svgFinalSize = sanitizeSize(svgSize);
|
|
1705
1758
|
|
|
1706
1759
|
cssMinifyCache = new LRU(cssFinalSize);
|
|
1707
1760
|
jsMinifyCache = new LRU(jsFinalSize);
|
package/src/htmlparser.js
CHANGED
|
@@ -506,7 +506,7 @@ export class HTMLParser {
|
|
|
506
506
|
// Note: Unquoted attribute values are intentionally not handled here.
|
|
507
507
|
// Per HTML spec, unquoted values cannot contain spaces or special chars,
|
|
508
508
|
// making a 20 KB+ unquoted value practically impossible. If encountered,
|
|
509
|
-
// it
|
|
509
|
+
// it’s malformed HTML and using the truncated regex match is acceptable.
|
|
510
510
|
}
|
|
511
511
|
}
|
|
512
512
|
}
|
package/src/lib/attributes.js
CHANGED
|
@@ -580,7 +580,7 @@ function buildAttr(normalized, hasUnarySlash, options, isLast, uidAttr) {
|
|
|
580
580
|
attrValue = attrValue.replace(/'/g, ''');
|
|
581
581
|
}
|
|
582
582
|
} else {
|
|
583
|
-
// `preventAttributesEscaping` mode: Choose safe quotes but don
|
|
583
|
+
// `preventAttributesEscaping` mode: Choose safe quotes but don’t escape
|
|
584
584
|
// except when both quote types are present—then escape to prevent invalid HTML
|
|
585
585
|
const hasDoubleQuote = attrValue.indexOf('"') !== -1;
|
|
586
586
|
const hasSingleQuote = attrValue.indexOf("'") !== -1;
|
package/src/lib/options.js
CHANGED
|
@@ -228,7 +228,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
228
228
|
cont: !!options.continueOnMinifyError
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
options.minifyJS = async function (text, inline) {
|
|
231
|
+
options.minifyJS = async function (text, inline, isModule) {
|
|
232
232
|
const start = text.match(/^\s*<!--.*/);
|
|
233
233
|
const code = start ? text.slice(start[0].length).replace(/\n\s*-->\s*$/, '') : text;
|
|
234
234
|
|
|
@@ -248,7 +248,7 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
248
248
|
|
|
249
249
|
// For large inputs, use length and content fingerprint to prevent collisions
|
|
250
250
|
jsKey = (code.length > 2048 ? (code.length + '|' + code.slice(0, 50) + code.slice(-50) + '|') : (code + '|'))
|
|
251
|
-
+ (inline ? '1' : '0') + '|' + useEngine + '|' + optsSig;
|
|
251
|
+
+ (inline ? '1' : '0') + '|' + (isModule ? 'm' : '') + '|' + useEngine + '|' + optsSig;
|
|
252
252
|
|
|
253
253
|
const cached = jsMinifyCache.get(jsKey);
|
|
254
254
|
if (cached) {
|
|
@@ -264,7 +264,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
264
264
|
parse: {
|
|
265
265
|
...terserOptions.parse,
|
|
266
266
|
bare_returns: inline
|
|
267
|
-
}
|
|
267
|
+
},
|
|
268
|
+
...(isModule ? { module: true } : {}) // Overrides user options: module detection takes precedence for `<script type=module>`
|
|
268
269
|
};
|
|
269
270
|
const terser = await getTerser();
|
|
270
271
|
const result = await terser(code, terserCallOptions);
|
|
@@ -275,7 +276,8 @@ const processOptions = (inputOptions, { getLightningCSS, getTerser, getSwc, getS
|
|
|
275
276
|
const result = await swc.minify(code, {
|
|
276
277
|
compress: true,
|
|
277
278
|
mangle: true,
|
|
278
|
-
...swcOptions,
|
|
279
|
+
...swcOptions,
|
|
280
|
+
...(isModule ? { module: true } : {}) // Overrides user options: module detection takes precedence for `<script type=module>`
|
|
279
281
|
});
|
|
280
282
|
return result.code.replace(RE_TRAILING_SEMICOLON, '');
|
|
281
283
|
}
|