Skip to content

Commit 5f4bad6

Browse files
author
alban-bitfly
committed
feat: add support for typescript development in frontend using esbuild package
1 parent a7998df commit 5f4bad6

7 files changed

Lines changed: 244 additions & 60 deletions

File tree

.eslintignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
*.min.js
1+
*.min.js
2+
*.ts
3+
*.tsx

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,43 @@ Install golint. (see https://github.com/golang/lint)
101101

102102
The explorer uses Highsoft charts which are not free for commercial and governmental use. If you plan to use the explorer for commercial purposes you currently need to purchase an appropriate HighSoft license.
103103
We are planning to switch out the Highsoft chart library with a less restrictive charting library (suggestions are welcome).
104+
105+
106+
# TypeScript development
107+
TypeScript development support is provided via esbuild. The existing build pipeline and project structure is mostly unchanged; TypeScript files are compiled to JavaScript files which are then picked up by the existing build pipeline.
108+
Bundling is done by esbuild via the Go server (no Node build step required).
109+
110+
### Guidelines
111+
- Place feature code under `static/<feature>/` with a small entry module (e.g., `<feature>.entry.ts`), then reference the emitted JS from the template.
112+
- You may separate the code in different files, only `.entry.ts` files are compiled.
113+
- Templates load ESM bundles via `<script type="module" src="/js/.../<feature>.entry.js"></script>`.
114+
- If you add a new `<feature>.entry.ts`, restart the server so esbuild picks it up.
115+
116+
#### Compiling + Watch + sourcemaps
117+
Install first `npm` dependencies:
118+
```bash
119+
npm install
120+
```
121+
To compile TS files, run:
122+
```bash
123+
go run ./cmd/bundle -compile-ts
124+
```
125+
For continuous rebuilding while editing TypeScript:
126+
```bash
127+
go run ./cmd/bundle -watch-ts
128+
```
129+
to enable sourcemaps during development, run with the `-ts-sourcemap` flag:
130+
```bash
131+
go run ./cmd/bundle -watch-ts -ts-sourcemap
132+
```
133+
- `-compile-ts` compiles all `.entry.ts` files once.
134+
- `-watch-ts` enables incremental rebuilds on file change.
135+
- `-ts-sourcemap` emits sourcemaps so DevTools shows original `.ts` sources. Use external maps for prod-like dev; inline maps are fine for quick local debugging.
136+
137+
#### Typed globals (jQuery, Bootstrap, DataTables)
138+
- Make sure to run: `npm install` in order to install ambient types so you get IntelliSense on globals: @types/jquery, @types/bootstrap, @types/datatables.net
139+
- You can use $ / jQuery, bootstrap namespace, and DataTables without imports; the editor knows their types.
140+
- These libraries are considered to be provided as globals by the templates at runtime; do not import them in TS.
141+
142+
### Notes
143+
- No ESLint for TS yet; Solution for now relies on the editor to provide TypeScript diagnostics.

cmd/bundle/main.go

Lines changed: 180 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,169 @@ package main
22

33
import (
44
"crypto/md5"
5+
"flag"
56
"fmt"
7+
"io/fs"
68
"log"
79
"os"
810
"path"
11+
"path/filepath"
912
"strings"
13+
"time"
1014

15+
"github.com/fsnotify/fsnotify"
1116
"github.com/gobitfly/eth2-beaconchain-explorer/utils"
1217

1318
"github.com/evanw/esbuild/pkg/api"
1419
)
1520

21+
var tsSourceMap = flag.Bool("ts-sourcemap", false, "emit inline sourcemaps for TS (dev)")
22+
23+
// buildTypeScript compiles all TS/TSX under static/ into static/js/[name].js
24+
func buildTypeScript(staticDir string) error {
25+
// Only explicit entry files; imports will be bundled into those outputs.
26+
isEntry := func(p string) bool {
27+
return strings.HasSuffix(p, ".entry.ts") || strings.HasSuffix(p, ".entry.tsx")
28+
}
29+
30+
var entries []string
31+
err := filepath.WalkDir(staticDir, func(p string, d fs.DirEntry, walkErr error) error {
32+
if walkErr != nil {
33+
return walkErr
34+
}
35+
if d.IsDir() {
36+
switch d.Name() {
37+
case "js", "bundle", "node_modules":
38+
return filepath.SkipDir
39+
}
40+
return nil
41+
}
42+
if strings.HasSuffix(p, ".d.ts") {
43+
return nil
44+
}
45+
if (strings.HasSuffix(p, ".ts") || strings.HasSuffix(p, ".tsx")) && isEntry(p) {
46+
entries = append(entries, p)
47+
}
48+
return nil
49+
})
50+
if err != nil {
51+
return err
52+
}
53+
if len(entries) == 0 {
54+
return nil
55+
}
56+
57+
opts := api.BuildOptions{
58+
EntryPoints: entries,
59+
Outdir: path.Join(staticDir, "js"),
60+
Outbase: staticDir,
61+
Bundle: true,
62+
Format: api.FormatESModule,
63+
Platform: api.PlatformBrowser,
64+
Loader: map[string]api.Loader{
65+
".ts": api.LoaderTS,
66+
".tsx": api.LoaderTSX,
67+
".json": api.LoaderJSON,
68+
},
69+
// Add source maps (inline for dev only)
70+
Sourcemap: func() api.SourceMap {
71+
if tsSourceMap != nil && *tsSourceMap {
72+
return api.SourceMapInline
73+
}
74+
return api.SourceMapNone
75+
}(),
76+
Write: true,
77+
LogLevel: api.LogLevelInfo,
78+
}
79+
80+
result := api.Build(opts)
81+
if len(result.Errors) > 0 {
82+
return fmt.Errorf("ts build failed: %v", result.Errors)
83+
}
84+
85+
return nil
86+
}
87+
88+
// Very small watcher for .ts/.tsx that calls buildTypeScript once per change.
89+
func watchTypeScript(staticDir string) error {
90+
// initial build
91+
if err := buildTypeScript(staticDir); err != nil {
92+
return err
93+
}
94+
95+
w, err := fsnotify.NewWatcher()
96+
if err != nil {
97+
return fmt.Errorf("watcher init: %w", err)
98+
}
99+
defer w.Close()
100+
101+
// watch all subdirs under static/, except outputs to avoid loops
102+
err = filepath.WalkDir(staticDir, func(p string, d fs.DirEntry, walkErr error) error {
103+
if walkErr != nil {
104+
return nil
105+
}
106+
if d.IsDir() {
107+
base := filepath.Base(p)
108+
switch base {
109+
case "js", "bundle", "node_modules":
110+
return filepath.SkipDir
111+
}
112+
_ = w.Add(p)
113+
}
114+
return nil
115+
})
116+
if err != nil {
117+
return fmt.Errorf("walk watch dirs: %w", err)
118+
}
119+
120+
isOutput := func(p string) bool {
121+
sep := string(filepath.Separator)
122+
return strings.Contains(p, sep+"js"+sep) || strings.Contains(p, sep+"bundle"+sep)
123+
}
124+
okExt := func(p string) bool {
125+
ext := strings.ToLower(filepath.Ext(p))
126+
return ext == ".ts" || ext == ".tsx"
127+
}
128+
129+
// debounce rapid events
130+
var timer *time.Timer
131+
trigger := func() {
132+
if timer != nil {
133+
timer.Stop()
134+
}
135+
timer = time.AfterFunc(200*time.Millisecond, func() {
136+
if err := buildTypeScript(staticDir); err != nil {
137+
log.Printf("TS rebuild failed: %v", err)
138+
} else {
139+
log.Println("TS rebuilt")
140+
}
141+
})
142+
}
143+
144+
log.Println("Watching TypeScript for changes...")
145+
for {
146+
select {
147+
case ev := <-w.Events:
148+
if ev.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Rename|fsnotify.Remove) == 0 {
149+
continue
150+
}
151+
if isOutput(ev.Name) || !okExt(ev.Name) {
152+
continue
153+
}
154+
155+
if ev.Op&fsnotify.Create != 0 {
156+
if fi, e := os.Stat(ev.Name); e == nil && fi.IsDir() {
157+
_ = w.Add(ev.Name)
158+
}
159+
}
160+
trigger()
161+
162+
case e := <-w.Errors:
163+
log.Printf("watch error: %v", e)
164+
}
165+
}
166+
}
167+
16168
func bundle(staticDir string) (map[string]string, error) {
17169

18170
nameMapping := make(map[string]string, 0)
@@ -72,9 +224,9 @@ func bundle(staticDir string) (map[string]string, error) {
72224
}
73225

74226
for _, match := range matches {
75-
code, err := os.ReadFile(match)
76-
if err != nil {
77-
return nameMapping, fmt.Errorf("error reading file %v", err)
227+
code, readErr := os.ReadFile(match)
228+
if readErr != nil {
229+
return nameMapping, fmt.Errorf("error reading file %v", readErr)
78230
}
79231
if !strings.Contains(match, ".min") {
80232
content := string(code)
@@ -135,12 +287,30 @@ func replaceFilesNames(files map[string]string) error {
135287
}
136288

137289
func main() {
138-
files, err := bundle("./static")
139-
if err != nil {
140-
log.Fatalf("error bundling: %v", err)
141-
}
290+
staticDir := flag.String("static", "./static", "path to static directory")
291+
watchTS := flag.Bool("watch-ts", false, "watch and rebuild TypeScript on changes (dev only)")
292+
compileTS := flag.Bool("compile-ts", false, "compile TypeScript assets before bundling (dev only)")
293+
flag.Parse()
142294

143-
if err := replaceFilesNames(files); err != nil {
144-
log.Fatalf("error replacing dependencies err: %v", err)
145-
}
295+
if *watchTS {
296+
if err := watchTypeScript(*staticDir); err != nil {
297+
log.Fatal(err)
298+
}
299+
return
300+
}
301+
302+
if *compileTS {
303+
if err := buildTypeScript(*staticDir); err != nil {
304+
log.Fatalf("error compiling typescript: %v", err)
305+
}
306+
}
307+
308+
files, err := bundle(*staticDir)
309+
if err != nil {
310+
log.Fatalf("error bundling: %v", err)
311+
}
312+
313+
if err := replaceFilesNames(files); err != nil {
314+
log.Fatalf("error replacing dependencies err: %v", err)
315+
}
146316
}

go.mod

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ require (
2121
github.com/davecgh/go-spew v1.1.1
2222
github.com/doug-martin/goqu/v9 v9.19.0
2323
github.com/ethereum/go-ethereum v1.14.6-0.20250124151602-75526bb8e01b
24-
github.com/evanw/esbuild v0.8.23
24+
github.com/evanw/esbuild v0.25.11
25+
github.com/fsnotify/fsnotify v1.6.0
2526
github.com/go-redis/redis/v8 v8.11.5
2627
github.com/gobitfly/eth-rewards v0.1.2-0.20230403064929-411ddc40a5f7
2728
github.com/gobitfly/eth.store v0.0.0-20250125090903-cce1f5e601a4
@@ -37,7 +38,6 @@ require (
3738
github.com/gorilla/websocket v1.5.3
3839
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
3940
github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e
40-
github.com/jackc/pgx/v4 v4.18.1
4141
github.com/jackc/pgx/v5 v5.4.3
4242
github.com/jmoiron/sqlx v1.2.0
4343
github.com/juliangruber/go-intersect v1.1.0
@@ -65,7 +65,6 @@ require (
6565
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
6666
github.com/skygeario/go-confusable-homoglyphs v0.0.0-20191212061114-e2b2a60df110
6767
github.com/stripe/stripe-go/v72 v72.50.0
68-
github.com/swaggo/swag v1.16.4
6968
github.com/urfave/negroni v1.0.0
7069
github.com/wealdtech/go-ens/v3 v3.6.0
7170
github.com/wealdtech/go-eth2-types/v2 v2.8.1
@@ -98,7 +97,6 @@ require (
9897
cloud.google.com/go/longrunning v0.6.4 // indirect
9998
cloud.google.com/go/monitoring v1.22.1 // indirect
10099
github.com/ClickHouse/ch-go v0.61.5 // indirect
101-
github.com/KyleBanks/depth v1.2.1 // indirect
102100
github.com/MicahParks/keyfunc v1.9.0 // indirect
103101
github.com/Microsoft/go-winio v0.6.2 // indirect
104102
github.com/VictoriaMetrics/fastcache v1.12.2 // indirect
@@ -133,16 +131,11 @@ require (
133131
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
134132
github.com/ethereum/go-verkle v0.2.2 // indirect
135133
github.com/felixge/httpsnoop v1.0.4 // indirect
136-
github.com/fsnotify/fsnotify v1.6.0 // indirect
137134
github.com/glendc/go-external-ip v0.1.0 // indirect
138135
github.com/go-faster/city v1.0.1 // indirect
139136
github.com/go-faster/errors v0.7.1 // indirect
140137
github.com/go-logr/logr v1.4.2 // indirect
141138
github.com/go-logr/stdr v1.2.2 // indirect
142-
github.com/go-openapi/jsonpointer v0.20.2 // indirect
143-
github.com/go-openapi/jsonreference v0.20.4 // indirect
144-
github.com/go-openapi/spec v0.20.14 // indirect
145-
github.com/go-openapi/swag v0.22.9 // indirect
146139
github.com/goccy/go-json v0.10.2 // indirect
147140
github.com/gofrs/flock v0.8.1 // indirect
148141
github.com/gogo/protobuf v1.3.2 // indirect
@@ -167,9 +160,6 @@ require (
167160
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
168161
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
169162
github.com/ipld/go-ipld-prime v0.20.0 // indirect
170-
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
171-
github.com/jackc/pgconn v1.14.0 // indirect
172-
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
173163
github.com/jackc/puddle/v2 v2.2.1 // indirect
174164
github.com/jbenet/goprocess v0.1.4 // indirect
175165
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
@@ -211,7 +201,6 @@ require (
211201
go.opentelemetry.io/otel/trace v1.31.0 // indirect
212202
go.uber.org/multierr v1.11.0 // indirect
213203
go.uber.org/zap v1.27.0 // indirect
214-
golang.org/x/tools v0.29.0 // indirect
215204
google.golang.org/appengine/v2 v2.0.2 // indirect
216205
google.golang.org/genproto v0.0.0-20241216192217-9240e9c98484 // indirect
217206
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect

0 commit comments

Comments
 (0)