gofront 1.2.1 → 1.2.3

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 CHANGED
@@ -387,16 +387,13 @@ gofront <dir> compile all *.go in directory → stdout
387
387
  gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
388
388
  gofront <input> --check type-check only
389
389
  gofront <input> --watch watch for changes and recompile
390
- gofront <input> -o out.js --serve watch + serve with live reload & SPA fallback (default port 3000)
390
+ gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
391
391
  gofront <input> -o out.js --serve --port 8080 use a custom port
392
- gofront <input> -o out.js --copy-assets compile and synchronize static assets from package.json assetCopy
393
392
  gofront <input> --source-map append inline source map (single file or directory; multi-file packages emit per-file mappings)
394
393
  gofront <input> --minify minify output (built-in minifier)
395
394
  gofront <input> --minify --mangle minify and rename local identifiers
396
395
  gofront <file.go> --ast dump AST (debug)
397
396
  gofront <file.go> --tokens dump tokens (debug)
398
- gofront test [dir] [--dom] run unit tests (-v verbose, -run <regex>)
399
- gofront prep [dir] copy static assets and bundle vendor dependencies
400
397
  gofront init [dir] scaffold a new project
401
398
  gofront --version / -v print version
402
399
  gofront --help / -h print this help
@@ -404,116 +401,6 @@ gofront --help / -h print this help
404
401
 
405
402
  ---
406
403
 
407
- ## Unit testing (`gofront test`)
408
-
409
- GoFront provides native, Go-idiomatic unit testing out of the box:
410
-
411
- - Files ending in `*_test.go` are automatically excluded from production builds (`gofront .`, `gofront -o app.js`) and compiled only when running tests.
412
- - Built-in `testing` standard library package with `*testing.T`.
413
- - Discover and execute `func TestXxx(t *testing.T)` functions with standard Go-style output.
414
-
415
- ### Writing a test
416
-
417
- ```go
418
- package store
419
-
420
- import "testing"
421
-
422
- func TestAddTodo(t *testing.T) {
423
- store := NewTodoStore()
424
- store.Add("Buy milk")
425
-
426
- if store.Count() != 1 {
427
- t.Errorf("expected 1 item, got %d", store.Count())
428
- }
429
- }
430
- ```
431
-
432
- Subtests and skips are fully supported:
433
-
434
- ```go
435
- func TestFilter(t *testing.T) {
436
- t.Run("active", func(t *testing.T) {
437
- // subtest logic
438
- })
439
- t.Run("completed", func(t *testing.T) {
440
- if testing.Short() {
441
- t.Skip("skipping in short mode")
442
- }
443
- })
444
- }
445
- ```
446
-
447
- ### Running tests
448
-
449
- ```sh
450
- gofront test # run tests in current directory
451
- gofront test ./src # run tests in specific directory
452
- gofront test -v # verbose output (RUN, PASS/FAIL, logs)
453
- gofront test -run "Todo" # run only tests matching regex pattern
454
- gofront test --dom # initialize JSDOM for DOM/gom/templ tests
455
- ```
456
-
457
- `--dom` requires `jsdom`. It is resolved from the project directory first and
458
- then from GoFront's own installation, so a globally installed `gofront` works
459
- without a project-local copy.
460
-
461
- Only `TestXxx` functions declared in `*_test.go` files are discovered, matching Go.
462
-
463
- ---
464
-
465
- ## Static assets & vendor bundling
466
-
467
- GoFront includes native tooling to manage static assets and package external npm dependencies for the browser without requiring external wrappers or complex build tools.
468
-
469
- ### Asset copying (`assetCopy`)
470
-
471
- Specify static assets (fonts, stylesheets, images) in `package.json` (or `gofront.json`):
472
-
473
- ```json
474
- {
475
- "assetCopy": [
476
- { "source": "node_modules/@fontsource/raleway/files", "dest": "app/fonts" },
477
- { "source": "node_modules/prismjs/themes", "dest": "app/css/prism-themes" }
478
- ]
479
- }
480
- ```
481
-
482
- Run `gofront prep` to copy them, or pass `--copy-assets` to automatically synchronize assets on build.
483
-
484
- ### Vendor bundling (`gofront prep`)
485
-
486
- Package external npm dependencies for the browser:
487
-
488
- ```sh
489
- gofront prep
490
- ```
491
-
492
- GoFront inspects your `dependencies`, dynamically uses `rolldown` or `esbuild` from your `devDependencies`, and bundles them into an ES module (defaulting to `app/vendor.js` or `vendor.js`).
493
-
494
- Each bundled package is exposed on `window` under its full name, its unscoped name,
495
- and a sanitised identifier (e.g. `fuse.js` → `window["fuse.js"]`, `window.fuse_js`).
496
- To expose additional global names, add a `vendor.globals` mapping:
497
-
498
- ```json
499
- {
500
- "vendor": {
501
- "dest": ["app/vendor.js", "public/vendor.js"],
502
- "minify": true,
503
- "globals": {
504
- "fuse.js": ["Fuse"],
505
- "prismjs": ["Prism"],
506
- "@emailjs/browser": ["emailjs"]
507
- }
508
- }
509
- }
510
- ```
511
-
512
- `dest` accepts a string or an array (the bundle is written once and copied to each
513
- extra destination). `gofront prep --minify` overrides `minify` from the command line.
514
-
515
- ---
516
-
517
404
  ## Multi-file packages
518
405
 
519
406
  All `.go` files in a directory share the same namespace and are compiled as one unit.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -333,10 +333,23 @@ export class CodeGen {
333
333
  if (fields.length === 0) {
334
334
  this.line("constructor() {}");
335
335
  } else {
336
- const params = fields.map((f) => `${f.name} = ${f.zero}`).join(", ");
336
+ // A field named like a type used in a default (e.g. `Project Project`)
337
+ // would shadow that class inside the parameter scope, so alias it.
338
+ const binding = (f) => {
339
+ const re = new RegExp(`\\b${f.name}\\b`);
340
+ return fields.some((o) => re.test(o.zero)) ? `${f.name}$` : f.name;
341
+ };
342
+ const params = fields
343
+ .map((f) => {
344
+ const b = binding(f);
345
+ return b === f.name
346
+ ? `${f.name} = ${f.zero}`
347
+ : `${f.name}: ${b} = ${f.zero}`;
348
+ })
349
+ .join(", ");
337
350
  this.line(`constructor({ ${params} } = {}) {`);
338
351
  this.indented(() => {
339
- for (const f of fields) this.line(`this.${f.name} = ${f.name};`);
352
+ for (const f of fields) this.line(`this.${f.name} = ${binding(f)};`);
340
353
  });
341
354
  this.line("}");
342
355
  }
@@ -448,7 +448,10 @@ export const statementGenMethods = {
448
448
  },
449
449
 
450
450
  _genRangeIterExpr(iterType, iteree, lhs) {
451
- if (this._isMapRangeType(iterType)) return `Object.entries(${iteree})`;
451
+ if (this._isMapRangeType(iterType)) {
452
+ if (lhs.length === 1) return `Object.keys(${iteree})`;
453
+ return `Object.entries(${iteree})`;
454
+ }
452
455
  if (this._isStringRangeType(iterType)) {
453
456
  if (lhs.length === 1) return `Array.from(${iteree}).keys()`;
454
457
  return `Array.from(${iteree}, (__c, __i) => [__i, __c.codePointAt(0)])`;
package/src/minifier.js CHANGED
@@ -379,8 +379,7 @@ function needsSpaceBetween(left, right) {
379
379
  return false;
380
380
  }
381
381
 
382
- function compress(code) {
383
- const tokens = tokenize(code);
382
+ function compressTokens(tokens) {
384
383
  let out = "";
385
384
 
386
385
  for (let i = 0; i < tokens.length; i++) {
@@ -430,6 +429,10 @@ function compress(code) {
430
429
  return out;
431
430
  }
432
431
 
432
+ function compress(code) {
433
+ return compressTokens(tokenize(code));
434
+ }
435
+
433
436
  function findPrevNonWs(tokens, index) {
434
437
  for (let i = index - 1; i >= 0; i--) {
435
438
  if (tokens[i].type !== "ws" && tokens[i].type !== "comment")
@@ -545,10 +548,18 @@ function _applyRenaming(tokens, renameMap) {
545
548
 
546
549
  function mangle(code) {
547
550
  const tokens = tokenize(code);
548
- const nameGen = shortNameGeneratorFn();
549
- const renameMap = new Map();
550
551
  const { declaredLocals } = _collectLocalDecls(tokens);
551
552
  _filterRenameCandidates(declaredLocals);
553
+
554
+ const occupied = new Set(MANGLE_GLOBALS);
555
+ for (const tok of tokens) {
556
+ if (tok.type === "ident" && !declaredLocals.has(tok.value)) {
557
+ occupied.add(tok.value);
558
+ }
559
+ }
560
+
561
+ const nameGen = shortNameGeneratorFn(occupied);
562
+ const renameMap = new Map();
552
563
  for (const name of declaredLocals) renameMap.set(name, nameGen());
553
564
  return _applyRenaming(tokens, renameMap);
554
565
  }
@@ -606,61 +617,180 @@ function collectParams(tokens, openIdx, locals) {
606
617
  }
607
618
  }
608
619
 
609
- function* shortNameGenerator() {
620
+ function* shortNameGenerator(occupied = new Set()) {
610
621
  const chars = "abcdefghijklmnopqrstuvwxyz";
611
622
  let index = 0;
612
623
  while (true) {
624
+ let name;
613
625
  if (index < 26) {
614
- yield chars[index++];
626
+ name = chars[index++];
615
627
  } else {
616
628
  const base = index - 26;
617
- yield chars[base % 26] + (Math.floor(base / 26) + 1);
629
+ name = chars[base % 26] + (Math.floor(base / 26) + 1);
618
630
  index++;
619
631
  }
632
+ if (!occupied.has(name)) {
633
+ yield name;
634
+ }
620
635
  }
621
636
  }
622
637
 
623
638
  // Make it callable (not an iterator interface)
624
- function shortNameGeneratorFn() {
625
- const gen = shortNameGenerator();
639
+ function shortNameGeneratorFn(occupied = new Set()) {
640
+ const gen = shortNameGenerator(occupied);
626
641
  return () => gen.next().value;
627
642
  }
628
643
 
629
644
  // ── Stage 4: Literal folding ─────────────────────────────────
630
645
 
631
- function foldLiterals(code) {
632
- // Fold patterns like: <number> <op> <number> where both are literal constants
633
- return code.replace(
634
- /\b(\d+(?:\.\d+)?)\s*(\+|-|\*|\/|%)\s*(\d+(?:\.\d+)?)\b/g,
635
- (match, left, op, right) => {
636
- const l = Number(left);
637
- const r = Number(right);
638
- let result;
639
- switch (op) {
640
- case "+":
641
- result = l + r;
642
- break;
643
- case "-":
644
- result = l - r;
645
- break;
646
- case "*":
647
- result = l * r;
648
- break;
649
- case "/":
650
- result = r !== 0 ? l / r : null;
651
- break;
652
- case "%":
653
- result = r !== 0 ? l % r : null;
654
- break;
655
- default:
656
- return match;
646
+ const FOLD_SAFE_PREV = new Set([
647
+ ";",
648
+ "(",
649
+ "[",
650
+ "{",
651
+ ",",
652
+ ":",
653
+ "?",
654
+ "=",
655
+ "+=",
656
+ "-=",
657
+ "*=",
658
+ "/=",
659
+ "%=",
660
+ "&&",
661
+ "||",
662
+ "??",
663
+ "==",
664
+ "!=",
665
+ "===",
666
+ "!==",
667
+ "<",
668
+ "<=",
669
+ ">",
670
+ ">=",
671
+ "return",
672
+ "throw",
673
+ "case",
674
+ "yield",
675
+ "await",
676
+ "=>",
677
+ ]);
678
+
679
+ function _foldBinaryOp(op, l, r) {
680
+ switch (op) {
681
+ case "+":
682
+ return l + r;
683
+ case "-":
684
+ return l - r;
685
+ case "*":
686
+ return l * r;
687
+ case "/":
688
+ return r !== 0 ? l / r : null;
689
+ case "%":
690
+ return r !== 0 ? l % r : null;
691
+ default:
692
+ return null;
693
+ }
694
+ }
695
+
696
+ function _isHigherPrecedenceOp(op, afterOp) {
697
+ if (
698
+ afterOp === "**" ||
699
+ afterOp === "++" ||
700
+ afterOp === "--" ||
701
+ afterOp === "."
702
+ ) {
703
+ return true;
704
+ }
705
+ if (
706
+ (op === "+" || op === "-") &&
707
+ (afterOp === "*" || afterOp === "/" || afterOp === "%")
708
+ ) {
709
+ return true;
710
+ }
711
+ return false;
712
+ }
713
+
714
+ function _canFoldLeft(next) {
715
+ const prevTok = findPrevNonWs(next, next.length);
716
+ return !prevTok || FOLD_SAFE_PREV.has(prevTok.value);
717
+ }
718
+
719
+ function _evaluateFold(tok, opTok, rightTok) {
720
+ const l = Number(tok.value);
721
+ const r = Number(rightTok.value);
722
+ if (!Number.isFinite(l) || !Number.isFinite(r)) return null;
723
+
724
+ let val = _foldBinaryOp(opTok.value, l, r);
725
+ if (val === null || !Number.isFinite(val) || val < 0) return null;
726
+ if (Object.is(val, -0)) val = 0;
727
+
728
+ const s = String(val);
729
+ const origLen = tok.value.length + opTok.value.length + rightTok.value.length;
730
+ return s.length <= origLen ? s : null;
731
+ }
732
+
733
+ function _tryFoldAt(current, i, next) {
734
+ const tok = current[i];
735
+ if (tok.type !== "num" || !_canFoldLeft(next)) return null;
736
+
737
+ const opInfo = nextNonWs(current, i);
738
+ if (opInfo?.tok.type !== "op") return null;
739
+
740
+ const op = opInfo.tok.value;
741
+ if (op !== "+" && op !== "-" && op !== "*" && op !== "/" && op !== "%") {
742
+ return null;
743
+ }
744
+
745
+ const rightInfo = nextNonWs(current, opInfo.idx);
746
+ if (rightInfo?.tok.type !== "num") return null;
747
+
748
+ const afterInfo = nextNonWs(current, rightInfo.idx);
749
+ if (
750
+ afterInfo?.tok.type === "op" &&
751
+ _isHigherPrecedenceOp(op, afterInfo.tok.value)
752
+ ) {
753
+ return null;
754
+ }
755
+
756
+ const folded = _evaluateFold(tok, opInfo.tok, rightInfo.tok);
757
+ if (!folded) return null;
758
+
759
+ next.push({ type: "num", value: folded });
760
+ return rightInfo.idx + 1;
761
+ }
762
+
763
+ function foldTokens(tokens) {
764
+ let changed = true;
765
+ let current = tokens;
766
+ let iterations = 0;
767
+
768
+ while (changed && iterations < 10) {
769
+ changed = false;
770
+ iterations++;
771
+ const next = [];
772
+ let i = 0;
773
+
774
+ while (i < current.length) {
775
+ const nextIdx = _tryFoldAt(current, i, next);
776
+ if (nextIdx !== null) {
777
+ i = nextIdx;
778
+ changed = true;
779
+ } else {
780
+ next.push(current[i]);
781
+ i++;
657
782
  }
658
- if (result === null || !Number.isFinite(result)) return match;
659
- // If result is integer, emit without decimal point
660
- const s = Number.isInteger(result) ? String(result) : String(result);
661
- return s;
662
- },
663
- );
783
+ }
784
+
785
+ current = next;
786
+ }
787
+
788
+ return current;
789
+ }
790
+
791
+ function foldLiterals(code) {
792
+ const tokens = tokenize(code);
793
+ return compressTokens(foldTokens(tokens));
664
794
  }
665
795
 
666
796
  // ── Public API ───────────────────────────────────────────────