gofront 0.0.4 → 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.
@@ -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
+ }