Skip to content

Commit 3686ac9

Browse files
author
alban-bitfly
committed
feat: add support for typescript development in frontend using esbuild package
1 parent 7ad170c commit 3686ac9

File tree

7 files changed

+246
-43
lines changed

7 files changed

+246
-43
lines changed

.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: 182 additions & 12 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)
@@ -32,7 +184,7 @@ func bundle(staticDir string) (map[string]string, error) {
32184

33185
bundleDir := path.Join(staticDir, "bundle")
34186
if _, err := os.Stat(bundleDir); os.IsNotExist(err) {
35-
os.Mkdir(bundleDir, 0755)
187+
os.MkdirAll(bundleDir, 0755)
36188
} else if err != nil {
37189
return nameMapping, fmt.Errorf("error getting stats about the bundle dir: %v", err)
38190
}
@@ -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)
@@ -87,7 +239,7 @@ func bundle(staticDir string) (map[string]string, error) {
87239
matchBundle := strings.Replace(match, typeDir, bundleTypeDir, -1)
88240

89241
if _, err := os.Stat(path.Dir(matchBundle)); os.IsNotExist(err) {
90-
os.Mkdir(path.Dir(matchBundle), 0755)
242+
os.MkdirAll(path.Dir(matchBundle), 0755)
91243
}
92244

93245
codeHash := fmt.Sprintf("%x", md5.Sum([]byte(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 & 6 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/scs/v2 v2.0.0-20240516120302-8754831e6b9b
@@ -36,7 +37,6 @@ require (
3637
github.com/gorilla/websocket v1.5.3
3738
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
3839
github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e
39-
github.com/jackc/pgx/v4 v4.18.1
4040
github.com/jackc/pgx/v5 v5.4.3
4141
github.com/jmoiron/sqlx v1.2.0
4242
github.com/juliangruber/go-intersect v1.1.0
@@ -131,7 +131,6 @@ require (
131131
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
132132
github.com/ethereum/go-verkle v0.2.2 // indirect
133133
github.com/felixge/httpsnoop v1.0.4 // indirect
134-
github.com/fsnotify/fsnotify v1.6.0 // indirect
135134
github.com/glendc/go-external-ip v0.1.0 // indirect
136135
github.com/go-faster/city v1.0.1 // indirect
137136
github.com/go-faster/errors v0.7.1 // indirect
@@ -160,9 +159,6 @@ require (
160159
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
161160
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
162161
github.com/ipld/go-ipld-prime v0.20.0 // indirect
163-
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
164-
github.com/jackc/pgconn v1.14.0 // indirect
165-
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
166162
github.com/jackc/puddle/v2 v2.2.1 // indirect
167163
github.com/jbenet/goprocess v0.1.4 // indirect
168164
github.com/libp2p/go-buffer-pool v0.1.0 // indirect

0 commit comments

Comments
 (0)