gofront 0.0.3 → 0.0.5
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 +110 -135
- package/package.json +2 -3
- package/src/codegen/expressions.js +529 -234
- package/src/codegen/statements.js +259 -14
- package/src/codegen.js +98 -9
- package/src/dts-parser.js +1 -1
- package/src/index.js +13 -12
- package/src/lexer.js +24 -1
- package/src/minifier.js +698 -0
- package/src/parser/expressions.js +115 -15
- package/src/parser/statements.js +4 -4
- package/src/parser/types.js +108 -3
- package/src/parser.js +93 -18
- package/src/typechecker/expressions.js +370 -13
- package/src/typechecker/statements.js +103 -5
- package/src/typechecker/types.js +96 -28
- package/src/typechecker.js +658 -22
package/src/minifier.js
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
// GoFront built-in minifier
|
|
2
|
+
// Operates on JS string output (post-CodeGen), no AST needed.
|
|
3
|
+
|
|
4
|
+
const JS_KEYWORDS = new Set([
|
|
5
|
+
"break",
|
|
6
|
+
"case",
|
|
7
|
+
"catch",
|
|
8
|
+
"class",
|
|
9
|
+
"const",
|
|
10
|
+
"continue",
|
|
11
|
+
"debugger",
|
|
12
|
+
"default",
|
|
13
|
+
"delete",
|
|
14
|
+
"do",
|
|
15
|
+
"else",
|
|
16
|
+
"enum",
|
|
17
|
+
"export",
|
|
18
|
+
"extends",
|
|
19
|
+
"false",
|
|
20
|
+
"finally",
|
|
21
|
+
"for",
|
|
22
|
+
"function",
|
|
23
|
+
"if",
|
|
24
|
+
"import",
|
|
25
|
+
"in",
|
|
26
|
+
"instanceof",
|
|
27
|
+
"let",
|
|
28
|
+
"new",
|
|
29
|
+
"null",
|
|
30
|
+
"of",
|
|
31
|
+
"return",
|
|
32
|
+
"super",
|
|
33
|
+
"switch",
|
|
34
|
+
"this",
|
|
35
|
+
"throw",
|
|
36
|
+
"true",
|
|
37
|
+
"try",
|
|
38
|
+
"typeof",
|
|
39
|
+
"var",
|
|
40
|
+
"void",
|
|
41
|
+
"while",
|
|
42
|
+
"with",
|
|
43
|
+
"yield",
|
|
44
|
+
"async",
|
|
45
|
+
"await",
|
|
46
|
+
"static",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// Keywords that must keep a space after them when followed by an identifier/literal
|
|
50
|
+
const SPACE_AFTER_KW = new Set([
|
|
51
|
+
"return",
|
|
52
|
+
"let",
|
|
53
|
+
"const",
|
|
54
|
+
"var",
|
|
55
|
+
"typeof",
|
|
56
|
+
"new",
|
|
57
|
+
"case",
|
|
58
|
+
"throw",
|
|
59
|
+
"void",
|
|
60
|
+
"delete",
|
|
61
|
+
"in",
|
|
62
|
+
"of",
|
|
63
|
+
"instanceof",
|
|
64
|
+
"function",
|
|
65
|
+
"class",
|
|
66
|
+
"extends",
|
|
67
|
+
"import",
|
|
68
|
+
"export",
|
|
69
|
+
"async",
|
|
70
|
+
"await",
|
|
71
|
+
"yield",
|
|
72
|
+
"static",
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// ── Tokenizer ────────────────────────────────────────────────
|
|
76
|
+
// Splits JS source into tokens preserving strings, template literals, and regexes.
|
|
77
|
+
|
|
78
|
+
function tokenize(code) {
|
|
79
|
+
const tokens = [];
|
|
80
|
+
let i = 0;
|
|
81
|
+
const len = code.length;
|
|
82
|
+
|
|
83
|
+
while (i < len) {
|
|
84
|
+
const ch = code[i];
|
|
85
|
+
|
|
86
|
+
// Line comment
|
|
87
|
+
if (ch === "/" && code[i + 1] === "/") {
|
|
88
|
+
let end = code.indexOf("\n", i);
|
|
89
|
+
if (end === -1) end = len;
|
|
90
|
+
tokens.push({ type: "comment", value: code.slice(i, end) });
|
|
91
|
+
i = end;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Block comment
|
|
96
|
+
if (ch === "/" && code[i + 1] === "*") {
|
|
97
|
+
const end = code.indexOf("*/", i + 2);
|
|
98
|
+
const closeAt = end === -1 ? len : end + 2;
|
|
99
|
+
tokens.push({ type: "comment", value: code.slice(i, closeAt) });
|
|
100
|
+
i = closeAt;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Regex literal — only after certain tokens
|
|
105
|
+
if (ch === "/") {
|
|
106
|
+
const prev = lastNonWhitespaceToken(tokens);
|
|
107
|
+
const prevVal = prev?.value;
|
|
108
|
+
const isRegexContext =
|
|
109
|
+
!prev ||
|
|
110
|
+
prev.type === "op" ||
|
|
111
|
+
prevVal === "(" ||
|
|
112
|
+
prevVal === "[" ||
|
|
113
|
+
prevVal === "," ||
|
|
114
|
+
prevVal === ";" ||
|
|
115
|
+
prevVal === "!" ||
|
|
116
|
+
prevVal === "=" ||
|
|
117
|
+
prevVal === "==" ||
|
|
118
|
+
prevVal === "===" ||
|
|
119
|
+
prevVal === "!=" ||
|
|
120
|
+
prevVal === "!==" ||
|
|
121
|
+
prevVal === ":" ||
|
|
122
|
+
prevVal === "return" ||
|
|
123
|
+
prevVal === "case" ||
|
|
124
|
+
prevVal === "typeof" ||
|
|
125
|
+
prevVal === "{" ||
|
|
126
|
+
prevVal === "&&" ||
|
|
127
|
+
prevVal === "||";
|
|
128
|
+
if (isRegexContext) {
|
|
129
|
+
let j = i + 1;
|
|
130
|
+
let escaped = false;
|
|
131
|
+
let inCharClass = false;
|
|
132
|
+
while (j < len) {
|
|
133
|
+
const rc = code[j];
|
|
134
|
+
if (escaped) {
|
|
135
|
+
escaped = false;
|
|
136
|
+
} else if (rc === "\\") {
|
|
137
|
+
escaped = true;
|
|
138
|
+
} else if (rc === "[") {
|
|
139
|
+
inCharClass = true;
|
|
140
|
+
} else if (rc === "]") {
|
|
141
|
+
inCharClass = false;
|
|
142
|
+
} else if (rc === "/" && !inCharClass) {
|
|
143
|
+
j++;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
j++;
|
|
147
|
+
}
|
|
148
|
+
// consume flags
|
|
149
|
+
while (j < len && /[gimsuy]/.test(code[j])) j++;
|
|
150
|
+
tokens.push({ type: "regex", value: code.slice(i, j) });
|
|
151
|
+
i = j;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// String literals (double or single quote)
|
|
157
|
+
if (ch === '"' || ch === "'") {
|
|
158
|
+
let j = i + 1;
|
|
159
|
+
while (j < len) {
|
|
160
|
+
if (code[j] === "\\" && j + 1 < len) {
|
|
161
|
+
j += 2;
|
|
162
|
+
} else if (code[j] === ch) {
|
|
163
|
+
j++;
|
|
164
|
+
break;
|
|
165
|
+
} else {
|
|
166
|
+
j++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
tokens.push({ type: "string", value: code.slice(i, j) });
|
|
170
|
+
i = j;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Template literal
|
|
175
|
+
if (ch === "`") {
|
|
176
|
+
let j = i + 1;
|
|
177
|
+
let depth = 1;
|
|
178
|
+
let braceDepth = 0;
|
|
179
|
+
while (j < len && depth > 0) {
|
|
180
|
+
if (code[j] === "\\" && j + 1 < len) {
|
|
181
|
+
j += 2;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (braceDepth === 0 && code[j] === "`") {
|
|
185
|
+
depth--;
|
|
186
|
+
j++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (code[j] === "$" && code[j + 1] === "{" && braceDepth === 0) {
|
|
190
|
+
braceDepth = 1;
|
|
191
|
+
j += 2;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (braceDepth > 0) {
|
|
195
|
+
if (code[j] === "{") braceDepth++;
|
|
196
|
+
else if (code[j] === "}") {
|
|
197
|
+
braceDepth--;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
j++;
|
|
201
|
+
}
|
|
202
|
+
tokens.push({ type: "template", value: code.slice(i, j) });
|
|
203
|
+
i = j;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Whitespace
|
|
208
|
+
if (/\s/.test(ch)) {
|
|
209
|
+
let j = i + 1;
|
|
210
|
+
while (j < len && /\s/.test(code[j])) j++;
|
|
211
|
+
tokens.push({ type: "ws", value: code.slice(i, j) });
|
|
212
|
+
i = j;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Identifier or keyword
|
|
217
|
+
if (/[a-zA-Z_$]/.test(ch)) {
|
|
218
|
+
let j = i + 1;
|
|
219
|
+
while (j < len && /[a-zA-Z0-9_$]/.test(code[j])) j++;
|
|
220
|
+
const word = code.slice(i, j);
|
|
221
|
+
tokens.push({
|
|
222
|
+
type: JS_KEYWORDS.has(word) ? "kw" : "ident",
|
|
223
|
+
value: word,
|
|
224
|
+
});
|
|
225
|
+
i = j;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Number
|
|
230
|
+
if (
|
|
231
|
+
/[0-9]/.test(ch) ||
|
|
232
|
+
(ch === "." && i + 1 < len && /[0-9]/.test(code[i + 1]))
|
|
233
|
+
) {
|
|
234
|
+
let j = i + 1;
|
|
235
|
+
// hex
|
|
236
|
+
if (ch === "0" && j < len && (code[j] === "x" || code[j] === "X")) {
|
|
237
|
+
j++;
|
|
238
|
+
while (j < len && /[0-9a-fA-F]/.test(code[j])) j++;
|
|
239
|
+
} else {
|
|
240
|
+
while (j < len && /[0-9]/.test(code[j])) j++;
|
|
241
|
+
if (j < len && code[j] === ".") {
|
|
242
|
+
j++;
|
|
243
|
+
while (j < len && /[0-9]/.test(code[j])) j++;
|
|
244
|
+
}
|
|
245
|
+
if (j < len && (code[j] === "e" || code[j] === "E")) {
|
|
246
|
+
j++;
|
|
247
|
+
if (j < len && (code[j] === "+" || code[j] === "-")) j++;
|
|
248
|
+
while (j < len && /[0-9]/.test(code[j])) j++;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
tokens.push({ type: "num", value: code.slice(i, j) });
|
|
252
|
+
i = j;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Multi-char operators
|
|
257
|
+
const two = code.slice(i, i + 2);
|
|
258
|
+
const three = code.slice(i, i + 3);
|
|
259
|
+
if (
|
|
260
|
+
three === "===" ||
|
|
261
|
+
three === "!==" ||
|
|
262
|
+
three === "..." ||
|
|
263
|
+
three === ">>>" ||
|
|
264
|
+
three === "**="
|
|
265
|
+
) {
|
|
266
|
+
tokens.push({ type: "op", value: three });
|
|
267
|
+
i += 3;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (
|
|
271
|
+
two === "==" ||
|
|
272
|
+
two === "!=" ||
|
|
273
|
+
two === "<=" ||
|
|
274
|
+
two === ">=" ||
|
|
275
|
+
two === "&&" ||
|
|
276
|
+
two === "||" ||
|
|
277
|
+
two === "++" ||
|
|
278
|
+
two === "--" ||
|
|
279
|
+
two === "+=" ||
|
|
280
|
+
two === "-=" ||
|
|
281
|
+
two === "*=" ||
|
|
282
|
+
two === "/=" ||
|
|
283
|
+
two === "=>" ||
|
|
284
|
+
two === "**" ||
|
|
285
|
+
two === "??" ||
|
|
286
|
+
two === "?." ||
|
|
287
|
+
two === ">>" ||
|
|
288
|
+
two === "<<"
|
|
289
|
+
) {
|
|
290
|
+
tokens.push({ type: "op", value: two });
|
|
291
|
+
i += 2;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Single-char operator/punctuation
|
|
296
|
+
tokens.push({ type: "op", value: ch });
|
|
297
|
+
i += 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return tokens;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function lastNonWhitespaceToken(tokens) {
|
|
304
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
305
|
+
if (tokens[i].type !== "ws" && tokens[i].type !== "comment")
|
|
306
|
+
return tokens[i];
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Stage 1 & 2: Compress ────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
function isIdentLike(type) {
|
|
314
|
+
return type === "ident" || type === "kw" || type === "num";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function needsSpaceBetween(left, right) {
|
|
318
|
+
// Keywords that need space after when followed by ident/num/string/keyword
|
|
319
|
+
if (left.type === "kw" && SPACE_AFTER_KW.has(left.value)) {
|
|
320
|
+
if (
|
|
321
|
+
right.type === "ident" ||
|
|
322
|
+
right.type === "kw" ||
|
|
323
|
+
right.type === "num" ||
|
|
324
|
+
right.type === "string" ||
|
|
325
|
+
right.type === "template" ||
|
|
326
|
+
right.type === "regex"
|
|
327
|
+
) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
// e.g., `return(` or `return!` — no space needed
|
|
331
|
+
if (
|
|
332
|
+
right.type === "op" &&
|
|
333
|
+
(right.value === "(" ||
|
|
334
|
+
right.value === "[" ||
|
|
335
|
+
right.value === "!" ||
|
|
336
|
+
right.value === "-" ||
|
|
337
|
+
right.value === "+")
|
|
338
|
+
) {
|
|
339
|
+
// "return(" is fine, "delete(" is fine, "new(" is fine
|
|
340
|
+
// But "function(" needs no space, "class{" needs no space... actually they do need space before name
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Two adjacent ident-like tokens need a space
|
|
346
|
+
if (isIdentLike(left.type) && isIdentLike(right.type)) return true;
|
|
347
|
+
|
|
348
|
+
// ident followed by string/template/regex needs space in some cases
|
|
349
|
+
// but generally not for GoFront output
|
|
350
|
+
|
|
351
|
+
// Prevent ++ or -- from merging with adjacent + or -
|
|
352
|
+
if (
|
|
353
|
+
(left.value === "+" && right.value === "+") ||
|
|
354
|
+
(left.value === "-" && right.value === "-") ||
|
|
355
|
+
(left.value === "+" && right.value === "++") ||
|
|
356
|
+
(left.value === "-" && right.value === "--") ||
|
|
357
|
+
(left.value === "++" && right.value === "+") ||
|
|
358
|
+
(left.value === "--" && right.value === "-")
|
|
359
|
+
) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function compress(code) {
|
|
367
|
+
const tokens = tokenize(code);
|
|
368
|
+
let out = "";
|
|
369
|
+
|
|
370
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
371
|
+
const tok = tokens[i];
|
|
372
|
+
|
|
373
|
+
// Stage 1: strip comments
|
|
374
|
+
if (tok.type === "comment") continue;
|
|
375
|
+
// Stage 1: strip whitespace (handled by needsSpaceBetween)
|
|
376
|
+
if (tok.type === "ws") continue;
|
|
377
|
+
|
|
378
|
+
// Preserve strings, templates, regexes verbatim
|
|
379
|
+
if (
|
|
380
|
+
tok.type === "string" ||
|
|
381
|
+
tok.type === "template" ||
|
|
382
|
+
tok.type === "regex"
|
|
383
|
+
) {
|
|
384
|
+
if (out.length > 0) {
|
|
385
|
+
const lastChar = out[out.length - 1];
|
|
386
|
+
// Need space if previous was ident-like
|
|
387
|
+
if (/[a-zA-Z0-9_$]/.test(lastChar) && tok.type !== "regex") {
|
|
388
|
+
// Check if preceding token is a keyword needing space
|
|
389
|
+
const prevTok = findPrevNonWs(tokens, i);
|
|
390
|
+
if (
|
|
391
|
+
prevTok &&
|
|
392
|
+
prevTok.type === "kw" &&
|
|
393
|
+
SPACE_AFTER_KW.has(prevTok.value)
|
|
394
|
+
) {
|
|
395
|
+
out += " ";
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
out += tok.value;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// For all other tokens, check if space is needed
|
|
404
|
+
if (out.length > 0) {
|
|
405
|
+
const prevTok = findPrevNonWs(tokens, i);
|
|
406
|
+
if (prevTok && needsSpaceBetween(prevTok, tok)) {
|
|
407
|
+
out += " ";
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
out += tok.value;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function findPrevNonWs(tokens, index) {
|
|
418
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
419
|
+
if (tokens[i].type !== "ws" && tokens[i].type !== "comment")
|
|
420
|
+
return tokens[i];
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Stage 3: Identifier mangling ─────────────────────────────
|
|
426
|
+
|
|
427
|
+
function mangle(code) {
|
|
428
|
+
const tokens = tokenize(code);
|
|
429
|
+
|
|
430
|
+
// Collect local variable declarations and their scopes
|
|
431
|
+
const nameGen = shortNameGeneratorFn();
|
|
432
|
+
const renameMap = new Map();
|
|
433
|
+
|
|
434
|
+
// Find all identifiers that are local declarations (let, const, var, function params)
|
|
435
|
+
// and not class/method names or properties
|
|
436
|
+
const declaredLocals = new Set();
|
|
437
|
+
const protectedNames = new Set();
|
|
438
|
+
|
|
439
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
440
|
+
const tok = tokens[i];
|
|
441
|
+
if (tok.type !== "kw" && tok.type !== "op") continue;
|
|
442
|
+
|
|
443
|
+
// class Name or extends Name — protect
|
|
444
|
+
if (tok.value === "class" || tok.value === "extends") {
|
|
445
|
+
const next = nextNonWs(tokens, i);
|
|
446
|
+
if (next && next.tok.type === "ident") {
|
|
447
|
+
protectedNames.add(next.tok.value);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Property access: .name — protect
|
|
452
|
+
if (tok.value === ".") {
|
|
453
|
+
const next = nextNonWs(tokens, i);
|
|
454
|
+
if (next && next.tok.type === "ident") {
|
|
455
|
+
protectedNames.add(next.tok.value);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Method definitions: name( in class body — protect
|
|
460
|
+
// We approximate: ident followed by ( at class level
|
|
461
|
+
|
|
462
|
+
// let/const/var name — local
|
|
463
|
+
if (tok.value === "let" || tok.value === "const" || tok.value === "var") {
|
|
464
|
+
const next = nextNonWs(tokens, i);
|
|
465
|
+
if (next && next.tok.type === "ident") {
|
|
466
|
+
declaredLocals.add(next.tok.value);
|
|
467
|
+
}
|
|
468
|
+
// Destructuring: let [a, b] or let {a, b}
|
|
469
|
+
if (next && (next.tok.value === "[" || next.tok.value === "{")) {
|
|
470
|
+
collectDestructured(tokens, next.idx, declaredLocals);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Function params
|
|
475
|
+
if (tok.value === "(") {
|
|
476
|
+
const prev = findPrevNonWsToken(tokens, i);
|
|
477
|
+
if (
|
|
478
|
+
prev &&
|
|
479
|
+
(prev.value === "function" ||
|
|
480
|
+
prev.type === "ident" ||
|
|
481
|
+
prev.value === ")")
|
|
482
|
+
) {
|
|
483
|
+
collectParams(tokens, i, declaredLocals);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Arrow function params handled by ( above
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Remove protected names from locals
|
|
491
|
+
for (const name of protectedNames) {
|
|
492
|
+
declaredLocals.delete(name);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Also protect known globals and short names
|
|
496
|
+
const globals = new Set([
|
|
497
|
+
"console",
|
|
498
|
+
"document",
|
|
499
|
+
"window",
|
|
500
|
+
"Math",
|
|
501
|
+
"JSON",
|
|
502
|
+
"String",
|
|
503
|
+
"Number",
|
|
504
|
+
"Boolean",
|
|
505
|
+
"Array",
|
|
506
|
+
"Object",
|
|
507
|
+
"Error",
|
|
508
|
+
"Date",
|
|
509
|
+
"RegExp",
|
|
510
|
+
"Map",
|
|
511
|
+
"Set",
|
|
512
|
+
"Promise",
|
|
513
|
+
"Symbol",
|
|
514
|
+
"parseInt",
|
|
515
|
+
"parseFloat",
|
|
516
|
+
"isNaN",
|
|
517
|
+
"isFinite",
|
|
518
|
+
"undefined",
|
|
519
|
+
"null",
|
|
520
|
+
"NaN",
|
|
521
|
+
"Infinity",
|
|
522
|
+
"TextEncoder",
|
|
523
|
+
"TextDecoder",
|
|
524
|
+
"setTimeout",
|
|
525
|
+
"setInterval",
|
|
526
|
+
"clearTimeout",
|
|
527
|
+
"clearInterval",
|
|
528
|
+
"prototype",
|
|
529
|
+
"constructor",
|
|
530
|
+
"this",
|
|
531
|
+
"super",
|
|
532
|
+
"arguments",
|
|
533
|
+
]);
|
|
534
|
+
for (const name of globals) declaredLocals.delete(name);
|
|
535
|
+
|
|
536
|
+
// Don't mangle __ prefixed helpers or single-char names
|
|
537
|
+
for (const name of declaredLocals) {
|
|
538
|
+
if (name.startsWith("__") || name.length <= 1) {
|
|
539
|
+
declaredLocals.delete(name);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Assign short names
|
|
544
|
+
for (const name of declaredLocals) {
|
|
545
|
+
const short = nameGen();
|
|
546
|
+
renameMap.set(name, short);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Apply renaming
|
|
550
|
+
let out = "";
|
|
551
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
552
|
+
const tok = tokens[i];
|
|
553
|
+
if (tok.type === "ident" && renameMap.has(tok.value)) {
|
|
554
|
+
// Don't rename if preceded by dot (property access)
|
|
555
|
+
const prev = findPrevNonWsToken(tokens, i);
|
|
556
|
+
if (prev && prev.value === ".") {
|
|
557
|
+
out += tok.value;
|
|
558
|
+
} else {
|
|
559
|
+
out += renameMap.get(tok.value);
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
out += tok.value;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function nextNonWs(tokens, index) {
|
|
570
|
+
for (let i = index + 1; i < tokens.length; i++) {
|
|
571
|
+
if (tokens[i].type !== "ws" && tokens[i].type !== "comment") {
|
|
572
|
+
return { tok: tokens[i], idx: i };
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function findPrevNonWsToken(tokens, index) {
|
|
579
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
580
|
+
if (tokens[i].type !== "ws" && tokens[i].type !== "comment")
|
|
581
|
+
return tokens[i];
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function collectDestructured(tokens, startIdx, locals) {
|
|
587
|
+
const open = tokens[startIdx].value;
|
|
588
|
+
const close = open === "[" ? "]" : "}";
|
|
589
|
+
let depth = 1;
|
|
590
|
+
for (let i = startIdx + 1; i < tokens.length && depth > 0; i++) {
|
|
591
|
+
const t = tokens[i];
|
|
592
|
+
if (t.value === open) depth++;
|
|
593
|
+
else if (t.value === close) depth--;
|
|
594
|
+
else if (depth === 1 && t.type === "ident") {
|
|
595
|
+
locals.add(t.value);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function collectParams(tokens, openIdx, locals) {
|
|
601
|
+
let depth = 1;
|
|
602
|
+
for (let i = openIdx + 1; i < tokens.length && depth > 0; i++) {
|
|
603
|
+
const t = tokens[i];
|
|
604
|
+
if (t.value === "(") depth++;
|
|
605
|
+
else if (t.value === ")") depth--;
|
|
606
|
+
else if (depth === 1 && t.type === "ident") {
|
|
607
|
+
// Check if not a default value expression
|
|
608
|
+
const prev = findPrevNonWsToken(tokens, i);
|
|
609
|
+
if (
|
|
610
|
+
!prev ||
|
|
611
|
+
prev.value === "(" ||
|
|
612
|
+
prev.value === "," ||
|
|
613
|
+
prev.value === "[" ||
|
|
614
|
+
prev.value === "{"
|
|
615
|
+
) {
|
|
616
|
+
locals.add(t.value);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function* shortNameGenerator() {
|
|
623
|
+
const chars = "abcdefghijklmnopqrstuvwxyz";
|
|
624
|
+
let index = 0;
|
|
625
|
+
while (true) {
|
|
626
|
+
if (index < 26) {
|
|
627
|
+
yield chars[index++];
|
|
628
|
+
} else {
|
|
629
|
+
const base = index - 26;
|
|
630
|
+
yield chars[base % 26] + (Math.floor(base / 26) + 1);
|
|
631
|
+
index++;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Make it callable (not an iterator interface)
|
|
637
|
+
function shortNameGeneratorFn() {
|
|
638
|
+
const gen = shortNameGenerator();
|
|
639
|
+
return () => gen.next().value;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ── Stage 4: Literal folding ─────────────────────────────────
|
|
643
|
+
|
|
644
|
+
function foldLiterals(code) {
|
|
645
|
+
// Fold patterns like: <number> <op> <number> where both are literal constants
|
|
646
|
+
return code.replace(
|
|
647
|
+
/\b(\d+(?:\.\d+)?)\s*(\+|-|\*|\/|%)\s*(\d+(?:\.\d+)?)\b/g,
|
|
648
|
+
(match, left, op, right) => {
|
|
649
|
+
const l = Number(left);
|
|
650
|
+
const r = Number(right);
|
|
651
|
+
let result;
|
|
652
|
+
switch (op) {
|
|
653
|
+
case "+":
|
|
654
|
+
result = l + r;
|
|
655
|
+
break;
|
|
656
|
+
case "-":
|
|
657
|
+
result = l - r;
|
|
658
|
+
break;
|
|
659
|
+
case "*":
|
|
660
|
+
result = l * r;
|
|
661
|
+
break;
|
|
662
|
+
case "/":
|
|
663
|
+
result = r !== 0 ? l / r : null;
|
|
664
|
+
break;
|
|
665
|
+
case "%":
|
|
666
|
+
result = r !== 0 ? l % r : null;
|
|
667
|
+
break;
|
|
668
|
+
default:
|
|
669
|
+
return match;
|
|
670
|
+
}
|
|
671
|
+
if (result === null || !Number.isFinite(result)) return match;
|
|
672
|
+
// If result is integer, emit without decimal point
|
|
673
|
+
const s = Number.isInteger(result) ? String(result) : String(result);
|
|
674
|
+
return s;
|
|
675
|
+
},
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ── Public API ───────────────────────────────────────────────
|
|
680
|
+
|
|
681
|
+
export function minify(code, options = {}) {
|
|
682
|
+
const { mangle: doMangle = false } = options;
|
|
683
|
+
|
|
684
|
+
// Stage 1 + 2: compress (strips comments, whitespace, compresses tokens)
|
|
685
|
+
let result = compress(code);
|
|
686
|
+
|
|
687
|
+
// Stage 4: literal folding
|
|
688
|
+
result = foldLiterals(result);
|
|
689
|
+
|
|
690
|
+
// Stage 3: mangling (optional)
|
|
691
|
+
if (doMangle) {
|
|
692
|
+
result = mangle(result);
|
|
693
|
+
// Re-compress after mangling to clean up any leftover whitespace
|
|
694
|
+
result = compress(result);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return result;
|
|
698
|
+
}
|