Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
*.min.js
*.min.js
*.ts
*.tsx
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,43 @@ Install golint. (see https://github.com/golang/lint)

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.
We are planning to switch out the Highsoft chart library with a less restrictive charting library (suggestions are welcome).


# TypeScript development
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.
Bundling is done by esbuild via the Go server (no Node build step required).

### Guidelines
- Place feature code under `static/<feature>/` with a small entry module (e.g., `<feature>.entry.ts`), then reference the emitted JS from the template.
- You may separate the code in different files, only `.entry.ts` files are compiled.
- Templates load ESM bundles via `<script type="module" src="/js/.../<feature>.entry.js"></script>`.
- If you add a new `<feature>.entry.ts`, restart the server so esbuild picks it up.

#### Compiling + Watch + sourcemaps
Install first `npm` dependencies:
```bash
npm install
```
To compile TS files, run:
```bash
go run ./cmd/bundle -compile-ts
```
For continuous rebuilding while editing TypeScript:
```bash
go run ./cmd/bundle -watch-ts
```
to enable sourcemaps during development, run with the `-ts-sourcemap` flag:
```bash
go run ./cmd/bundle -watch-ts -ts-sourcemap
```
- `-compile-ts` compiles all `.entry.ts` files once.
- `-watch-ts` enables incremental rebuilds on file change.
- `-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.

#### Typed globals (jQuery, Bootstrap, DataTables)
- 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
- You can use $ / jQuery, bootstrap namespace, and DataTables without imports; the editor knows their types.
- These libraries are considered to be provided as globals by the templates at runtime; do not import them in TS.

### Notes
- No ESLint for TS yet; Solution for now relies on the editor to provide TypeScript diagnostics.
194 changes: 182 additions & 12 deletions cmd/bundle/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,169 @@ package main

import (
"crypto/md5"
"flag"
"fmt"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"strings"
"time"

"github.com/fsnotify/fsnotify"
"github.com/gobitfly/eth2-beaconchain-explorer/utils"

"github.com/evanw/esbuild/pkg/api"
)

var tsSourceMap = flag.Bool("ts-sourcemap", false, "emit inline sourcemaps for TS (dev)")

// buildTypeScript compiles all TS/TSX under static/ into static/js/[name].js
func buildTypeScript(staticDir string) error {
// Only explicit entry files; imports will be bundled into those outputs.
isEntry := func(p string) bool {
return strings.HasSuffix(p, ".entry.ts") || strings.HasSuffix(p, ".entry.tsx")
}

var entries []string
err := filepath.WalkDir(staticDir, func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
switch d.Name() {
case "js", "bundle", "node_modules":
return filepath.SkipDir
}
return nil
}
if strings.HasSuffix(p, ".d.ts") {
return nil
}
if (strings.HasSuffix(p, ".ts") || strings.HasSuffix(p, ".tsx")) && isEntry(p) {
entries = append(entries, p)
}
return nil
})
if err != nil {
return err
}
if len(entries) == 0 {
return nil
}

opts := api.BuildOptions{
EntryPoints: entries,
Outdir: path.Join(staticDir, "js"),
Outbase: staticDir,
Bundle: true,
Format: api.FormatESModule,
Platform: api.PlatformBrowser,
Loader: map[string]api.Loader{
".ts": api.LoaderTS,
".tsx": api.LoaderTSX,
".json": api.LoaderJSON,
},
// Add source maps (inline for dev only)
Sourcemap: func() api.SourceMap {
if tsSourceMap != nil && *tsSourceMap {
return api.SourceMapInline
}
return api.SourceMapNone
}(),
Write: true,
LogLevel: api.LogLevelInfo,
}

result := api.Build(opts)
if len(result.Errors) > 0 {
return fmt.Errorf("ts build failed: %v", result.Errors)
}

return nil
}

// Very small watcher for .ts/.tsx that calls buildTypeScript once per change.
func watchTypeScript(staticDir string) error {
// initial build
if err := buildTypeScript(staticDir); err != nil {
return err
}

w, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("watcher init: %w", err)
}
defer w.Close()

// watch all subdirs under static/, except outputs to avoid loops
err = filepath.WalkDir(staticDir, func(p string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return nil
}
if d.IsDir() {
base := filepath.Base(p)
switch base {
case "js", "bundle", "node_modules":
return filepath.SkipDir
}
_ = w.Add(p)
}
return nil
})
if err != nil {
return fmt.Errorf("walk watch dirs: %w", err)
}

isOutput := func(p string) bool {
sep := string(filepath.Separator)
return strings.Contains(p, sep+"js"+sep) || strings.Contains(p, sep+"bundle"+sep)
}
okExt := func(p string) bool {
ext := strings.ToLower(filepath.Ext(p))
return ext == ".ts" || ext == ".tsx"
}

// debounce rapid events
var timer *time.Timer
trigger := func() {
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(200*time.Millisecond, func() {
if err := buildTypeScript(staticDir); err != nil {
log.Printf("TS rebuild failed: %v", err)
} else {
log.Println("TS rebuilt")
}
})
}

log.Println("Watching TypeScript for changes...")
for {
select {
case ev := <-w.Events:
if ev.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Rename|fsnotify.Remove) == 0 {
continue
}
if isOutput(ev.Name) || !okExt(ev.Name) {
continue
}

if ev.Op&fsnotify.Create != 0 {
if fi, e := os.Stat(ev.Name); e == nil && fi.IsDir() {
_ = w.Add(ev.Name)
}
}
trigger()

case e := <-w.Errors:
log.Printf("watch error: %v", e)
}
}
}

func bundle(staticDir string) (map[string]string, error) {

nameMapping := make(map[string]string, 0)
Expand All @@ -32,7 +184,7 @@ func bundle(staticDir string) (map[string]string, error) {

bundleDir := path.Join(staticDir, "bundle")
if _, err := os.Stat(bundleDir); os.IsNotExist(err) {
os.Mkdir(bundleDir, 0755)
os.MkdirAll(bundleDir, 0755)
} else if err != nil {
return nameMapping, fmt.Errorf("error getting stats about the bundle dir: %v", err)
}
Expand Down Expand Up @@ -72,9 +224,9 @@ func bundle(staticDir string) (map[string]string, error) {
}

for _, match := range matches {
code, err := os.ReadFile(match)
if err != nil {
return nameMapping, fmt.Errorf("error reading file %v", err)
code, readErr := os.ReadFile(match)
if readErr != nil {
return nameMapping, fmt.Errorf("error reading file %v", readErr)
}
if !strings.Contains(match, ".min") {
content := string(code)
Expand All @@ -87,7 +239,7 @@ func bundle(staticDir string) (map[string]string, error) {
matchBundle := strings.Replace(match, typeDir, bundleTypeDir, -1)

if _, err := os.Stat(path.Dir(matchBundle)); os.IsNotExist(err) {
os.Mkdir(path.Dir(matchBundle), 0755)
os.MkdirAll(path.Dir(matchBundle), 0755)
}

codeHash := fmt.Sprintf("%x", md5.Sum([]byte(code)))
Expand Down Expand Up @@ -135,12 +287,30 @@ func replaceFilesNames(files map[string]string) error {
}

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

if err := replaceFilesNames(files); err != nil {
log.Fatalf("error replacing dependencies err: %v", err)
}
if *watchTS {
if err := watchTypeScript(*staticDir); err != nil {
log.Fatal(err)
}
return
}

if *compileTS {
if err := buildTypeScript(*staticDir); err != nil {
log.Fatalf("error compiling typescript: %v", err)
}
}

files, err := bundle(*staticDir)
if err != nil {
log.Fatalf("error bundling: %v", err)
}

if err := replaceFilesNames(files); err != nil {
log.Fatalf("error replacing dependencies err: %v", err)
}
}
4 changes: 4 additions & 0 deletions cmd/explorer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,9 @@ func main() {
authRouter.HandleFunc("/mobile/delete", handlers.MobileDeviceDeletePOST).Methods("POST", "OPTIONS")
authRouter.HandleFunc("/authorize", handlers.UserAuthorizeConfirmPost).Methods("POST")
authRouter.HandleFunc("/settings", handlers.UserSettings).Methods("GET")

authRouter.HandleFunc("/api-key-management", handlers.APIKeyManagement).Methods("GET")

authRouter.HandleFunc("/settings/password", handlers.UserUpdatePasswordPost).Methods("POST")
authRouter.HandleFunc("/settings/flags", handlers.UserUpdateFlagsPost).Methods("POST")
authRouter.HandleFunc("/settings/delete", handlers.UserDeletePost).Methods("POST")
Expand Down Expand Up @@ -596,6 +599,7 @@ func main() {

authRouter.HandleFunc("/subscriptions/data", handlers.UserSubscriptionsData).Methods("GET")
authRouter.HandleFunc("/generateKey", handlers.GenerateAPIKey).Methods("POST")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: we should probably remove everything related to the old API key generation (handlers, templates, and this route - also some DB calls) - maybe @LuccaBitfly can help.
Can be done after this PR.


authRouter.HandleFunc("/ethClients", handlers.EthClientsServices).Methods("GET")
authRouter.HandleFunc("/webhooks", handlers.NotificationWebhookPage).Methods("GET")
authRouter.HandleFunc("/webhooks/add", handlers.UsersAddWebhook).Methods("POST")
Expand Down
8 changes: 2 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/doug-martin/goqu/v9 v9.19.0
github.com/ethereum/go-ethereum v1.14.6-0.20250124151602-75526bb8e01b
github.com/evanw/esbuild v0.8.23
github.com/evanw/esbuild v0.25.11
github.com/fsnotify/fsnotify v1.6.0
github.com/go-redis/redis/v8 v8.11.5
github.com/gobitfly/eth-rewards v0.1.2-0.20230403064929-411ddc40a5f7
github.com/gobitfly/scs/v2 v2.0.0-20240516120302-8754831e6b9b
Expand All @@ -36,7 +37,6 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e
github.com/jackc/pgx/v4 v4.18.1
github.com/jackc/pgx/v5 v5.4.3
github.com/jmoiron/sqlx v1.2.0
github.com/juliangruber/go-intersect v1.1.0
Expand Down Expand Up @@ -131,7 +131,6 @@ require (
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/glendc/go-external-ip v0.1.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
Expand Down Expand Up @@ -160,9 +159,6 @@ require (
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
github.com/ipld/go-ipld-prime v0.20.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
Expand Down
Loading