Skip to content

Commit b0d19bf

Browse files
authored
Improve web pkg (#1117)
1 parent c7527e7 commit b0d19bf

File tree

10 files changed

+157
-63
lines changed

10 files changed

+157
-63
lines changed

cli/README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# esm.sh CLI
22

3-
A _nobuild_ tool for modern web development.
4-
53
> [!WARNING]
6-
> The `esm.sh` CLI is still in development and may not be stable. Use it at your own risk.
4+
> The `esm.sh` CLI is still in development. Use it at your own risk.
5+
6+
A _nobuild_ tool for modern web development.
77

88
## Installation
99

@@ -32,9 +32,13 @@ $ esm.sh --help
3232
Usage: esm.sh [command] <options>
3333
3434
Commands:
35-
add, i [...packages] Alias to 'importmap add'.
36-
importmap, im Manage "importmap" script.
37-
init Create a new web application.
38-
serve Serve a web application.
39-
dev Serve a web app in development mode.
35+
add, i [...packages] Add packages to the "importmap" script
36+
update Update packages in the "importmap" script
37+
tidy Tidy up the "importmap" script
38+
init Create a new web application
39+
serve, x Serve a web application
40+
dev Serve a web application in development mode
41+
42+
Options:
43+
--help Show help message
4044
```

main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ const helpMessage = "\033[30mesm.sh - A nobuild tool for modern web development.
1212
Usage: esm.sh [command] <options>
1313
1414
Commands:
15-
add, i [...packages] Add packages to "importmap" script
16-
update Update packages in "importmap" script
17-
tidy Tidy up "importmap" script
15+
add, i [...packages] Add packages to the "importmap" script
16+
update Update packages in the "importmap" script
17+
tidy Tidy up the "importmap" script
1818
init Create a new web application
1919
serve, x Serve a web application
2020
dev Serve a web application in development mode

server/build_analyzer.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (ctx *BuildContext) analyzeSplitting() (err error) {
8080
if e == nil && n <= len(a)-1 {
8181
ctx.splitting = set.NewReadOnly(a[1 : n+1]...)
8282
if DEBUG {
83-
ctx.logger.Debugf("build(%s): splitting.txt found with %d shared modules", ctx.esmPath.Specifier(), ctx.splitting.Len())
83+
ctx.logger.Debugf("build(%s): splitting.txt found with %d shared modules", ctx.esmPath.Specifier(), ctx.splitting.Len())
8484
}
8585
return true
8686
}
@@ -127,15 +127,15 @@ func (ctx *BuildContext) analyzeSplitting() (err error) {
127127

128128
refs := map[string]Ref{}
129129
for _, exportName := range exportNames.Values() {
130-
esmPath := ctx.esmPath
130+
esmPath := ctx.esmPath
131131
esmPath.SubPath = exportName
132132
esmPath.SubModuleName = stripEntryModuleExt(exportName)
133133
b := &BuildContext{
134134
npmrc: ctx.npmrc,
135135
logger: ctx.logger,
136136
db: ctx.db,
137137
storage: ctx.storage,
138-
esmPath: esmPath,
138+
esmPath: esmPath,
139139
args: ctx.args,
140140
externalAll: ctx.externalAll,
141141
target: ctx.target,
@@ -145,7 +145,7 @@ func (ctx *BuildContext) analyzeSplitting() (err error) {
145145
}
146146
_, includes, err := b.buildModule(true)
147147
if err != nil {
148-
return fmt.Errorf("failed to analyze %s: %v", esmPath.Specifier(), err)
148+
return fmt.Errorf("failed to analyze %s: %v", esmPath.Specifier(), err)
149149
}
150150
for _, include := range includes {
151151
module, importer := include[0], include[1]
@@ -198,7 +198,7 @@ func (ctx *BuildContext) analyzeSplitting() (err error) {
198198
}
199199
ctx.splitting = splitting.ReadOnly()
200200
if DEBUG {
201-
ctx.logger.Debugf("build(%s): found %d shared modules from %d modules", ctx.esmPath.Specifier(), shared.Len(), len(refs))
201+
ctx.logger.Debugf("build(%s): found %d shared modules from %d modules", ctx.esmPath.Specifier(), shared.Len(), len(refs))
202202
}
203203
}
204204
}

web/README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
# ESM App Handler for Go
1+
# Web App Handler for Go
2+
3+
> [!WARNING]
4+
> The `web` package is still in development. Use it at your own risk.
25
36
A golang `http.Handler` that serves _nobuild_ web applications.
47

58
- Web applications are served _as-is_ without any build step.
69
- Transpiles TypeScript, JSX, Vue, Svelte _on-the-fly_.
7-
- Staic files are served with _correct MIME types_.
810
- Built-in [UnoCSS](https://unocss.dev) generator.
9-
- Hot Module Replacement (HMR) for development.
11+
- Staic files are served from the application directory.
12+
- Support Hot Module Replacement (HMR) for development
1013

1114
## Installation
1215

@@ -31,8 +34,8 @@ import (
3134
func main() {
3235
http.Handle("GET /", web.NewHandler(web.Config{
3336
AppDir: "/path/to/webapp",
34-
Fallback: "/index.html", // fallback to root index.html for SPA
35-
Dev: false, // change to true to enable HMR
37+
Fallback: "/index.html", // fallback to root index.html (SPA mode)
38+
Dev: false, // change to `true` to enable HMR
3639
}))
3740
log.Fatal(http.ListenAndServe(":8080", nil))
3841
}

web/debug.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,11 @@
22

33
package web
44

5+
import (
6+
"fmt"
7+
"time"
8+
)
9+
10+
var VERSION = fmt.Sprintf("%x", time.Now().Unix())
11+
512
const DEBUG = true

web/handler.go

Lines changed: 93 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func NewHandler(config Config) *Handler {
4848
config.AppDir, _ = os.Getwd()
4949
}
5050
s := &Handler{config: &config}
51-
s.etagSuffix = fmt.Sprintf("-v%d", VERSION)
51+
s.etagSuffix = "-" + VERSION
5252
if s.config.Dev {
5353
s.etagSuffix += "-dev"
5454
}
@@ -252,6 +252,7 @@ func (s *Handler) ServeHtml(w http.ResponseWriter, r *http.Request, filename str
252252
defer htmlFile.Close()
253253

254254
u := url.URL{Path: filename}
255+
im := base64.RawURLEncoding.EncodeToString([]byte(filename))
255256
tokenizer := html.NewTokenizer(htmlFile)
256257
hotLinks := []string{}
257258
unocss := ""
@@ -295,7 +296,7 @@ func (s *Handler) ServeHtml(w http.ResponseWriter, r *http.Request, filename str
295296
switch attrKey {
296297
case "src":
297298
w.Write([]byte(" src=\""))
298-
w.Write([]byte(srcAttr + "?im=" + base64.RawURLEncoding.EncodeToString([]byte(filename))))
299+
w.Write([]byte(srcAttr + "?im=" + im))
299300
w.Write([]byte{'"'})
300301
default:
301302
w.Write([]byte{' '})
@@ -317,7 +318,7 @@ func (s *Handler) ServeHtml(w http.ResponseWriter, r *http.Request, filename str
317318
hrefAttr = u.ResolveReference(&url.URL{Path: hrefAttr}).Path
318319
if hrefAttr == "uno.css" || strings.HasSuffix(hrefAttr, "/uno.css") {
319320
if unocss == "" {
320-
unocssHref := hrefAttr + "?ctx=" + base64.RawURLEncoding.EncodeToString([]byte(filename))
321+
unocssHref := hrefAttr + "?ctx=" + im
321322
w.Write([]byte("<link rel=\"stylesheet\" href=\""))
322323
w.Write([]byte(unocssHref))
323324
w.Write([]byte{'"', '>'})
@@ -390,6 +391,7 @@ func (s *Handler) ServeHtml(w http.ResponseWriter, r *http.Request, filename str
390391
}
391392
w.Write(tokenizer.Raw())
392393
}
394+
393395
if s.config.Dev {
394396
// reload the page when the html file is modified
395397
w.Write([]byte(`<script type="module">import createHotContext from"/@hmr";const hot=createHotContext("`))
@@ -408,6 +410,7 @@ func (s *Handler) ServeHtml(w http.ResponseWriter, r *http.Request, filename str
408410
}
409411
w.Write([]byte(`]){const el=$("link[href='"+href+"']");hot.watch(href,kind=>{if(kind==="modify")el.href=href+"?t="+Date.now().toString(36)})}`))
410412
}
413+
411414
// reload the unocss when the module dependency tree is changed
412415
if unocss != "" {
413416
w.Write([]byte(`const uno="`))
@@ -420,6 +423,7 @@ func (s *Handler) ServeHtml(w http.ResponseWriter, r *http.Request, filename str
420423
w.Write([]byte(filename))
421424
w.Write([]byte("\",()=>location.reload());"))
422425
}
426+
423427
w.Write([]byte("</script>"))
424428
w.Write([]byte(`<script>console.log("%c💚 Built with esm.sh, please uncheck \"Disable cache\" in Network tab for better DX!", "color:green")</script>`))
425429
}
@@ -431,10 +435,13 @@ func (s *Handler) ServeModule(w http.ResponseWriter, r *http.Request, filename s
431435
http.Error(w, "Bad Request", 400)
432436
return
433437
}
434-
435-
importMapRaw, importMap, err := s.parseImportMap(string(im))
438+
imfi, err := os.Lstat(filepath.Join(s.config.AppDir, string(im)))
436439
if err != nil {
437-
http.Error(w, "could not parse import map: "+err.Error(), 500)
440+
if os.IsNotExist(err) {
441+
http.Error(w, "Bad Request", 400)
442+
} else {
443+
http.Error(w, "Internal Server Error", 500)
444+
}
438445
return
439446
}
440447

@@ -458,9 +465,7 @@ func (s *Handler) ServeModule(w http.ResponseWriter, r *http.Request, filename s
458465
modTime = uint64(fi.ModTime().UnixMilli())
459466
size = fi.Size()
460467
}
461-
xx := xxhash.New()
462-
xx.Write([]byte(importMapRaw))
463-
etag := fmt.Sprintf("w/\"%x-%x-%x%s\"", modTime, size, xx.Sum(nil), s.etagSuffix)
468+
etag := fmt.Sprintf("w/\"%x-%x-%x-%x%s\"", modTime, size, imfi.ModTime().UnixMilli(), imfi.Size(), s.etagSuffix)
464469
if r.Header.Get("If-None-Match") == etag && !query.Has("t") {
465470
w.WriteHeader(http.StatusNotModified)
466471
return
@@ -485,11 +490,16 @@ func (s *Handler) ServeModule(w http.ResponseWriter, r *http.Request, filename s
485490
http.Error(w, "Loader worker not started", 500)
486491
return
487492
}
493+
_, importMap, err := s.parseImportMap(string(im))
494+
if err != nil {
495+
http.Error(w, "could not parse import map: "+err.Error(), 500)
496+
return
497+
}
488498
args := []any{"tsx", filename, importMap, nil, s.config.Dev}
489499
if preTransform != nil {
490500
args[3] = string(preTransform)
491501
}
492-
_, js, err := s.callLoader(args...)
502+
_, js, err := s.callLoaderJS(args...)
493503
if err != nil {
494504
fmt.Println(term.Red("[error] " + err.Error()))
495505
http.Error(w, "Internal Server Error", 500)
@@ -700,12 +710,12 @@ func (s *Handler) ServeUnoCSS(w http.ResponseWriter, r *http.Request, query url.
700710
for moreAttr {
701711
var key, val []byte
702712
key, val, moreAttr = tokenizer.TagAttr()
703-
if bytes.Equal(key, []byte("src")) {
713+
if bytes.Equal(key, []byte("type")) {
714+
typeAttr = string(val)
715+
} else if bytes.Equal(key, []byte("src")) {
704716
srcAttr = string(val)
705717
} else if bytes.Equal(key, []byte("href")) {
706718
hrefAttr = string(val)
707-
} else if bytes.Equal(key, []byte("type")) {
708-
typeAttr = string(val)
709719
}
710720
}
711721
if typeAttr == "importmap" {
@@ -885,15 +895,14 @@ func (s *Handler) ServeHmrWS(w http.ResponseWriter, r *http.Request) {
885895
}
886896
}
887897

888-
func (s *Handler) parseImportMap(im string) (importMapRaw []byte, importMap importmap.ImportMap, err error) {
889-
imHtmlFilename := filepath.Join(s.config.AppDir, im)
890-
imHtmlFile, err := os.Open(imHtmlFilename)
898+
func (s *Handler) parseImportMap(filename string) (importMapRaw []byte, importMap importmap.ImportMap, err error) {
899+
file, err := os.Open(filepath.Join(s.config.AppDir, filename))
891900
if err != nil {
892901
return
893902
}
894-
defer imHtmlFile.Close()
903+
defer file.Close()
895904

896-
tokenizer := html.NewTokenizer(imHtmlFile)
905+
tokenizer := html.NewTokenizer(file)
897906
for {
898907
tt := tokenizer.Next()
899908
if tt == html.ErrorToken {
@@ -918,7 +927,7 @@ func (s *Handler) parseImportMap(im string) (importMapRaw []byte, importMap impo
918927
err = errors.New("invalid import map")
919928
return
920929
}
921-
importMap.Src = "file://" + string(im)
930+
importMap.Src = "file://" + string(filename)
922931
// todo: cache parsed import map
923932
break
924933
}
@@ -1004,7 +1013,7 @@ func (s *Handler) analyzeDependencyTree(entry string, importMap importmap.Import
10041013
if s.loaderWorker == nil {
10051014
return esbuild.OnLoadResult{}, errors.New("loader worker not started")
10061015
}
1007-
lang, code, err := s.callLoader(ext[1:], pathname, contents, importMap)
1016+
lang, code, err := s.callLoaderJS(ext[1:], pathname, contents, importMap)
10081017
if err != nil {
10091018
return esbuild.OnLoadResult{}, err
10101019
}
@@ -1083,22 +1092,75 @@ func (s *Handler) startLoaderWorker() (err error) {
10831092
if err != nil {
10841093
return err
10851094
}
1086-
go s.callLoader("tsx", "_.tsx", nil, "", false)
1087-
go func() {
1088-
entries, err := os.ReadDir(s.config.AppDir)
1089-
if err == nil {
1090-
for _, entry := range entries {
1091-
if entry.Type().IsRegular() && entry.Name() == "uno.css" {
1092-
go s.callLoader("unocss", "_uno.css", "flex")
1095+
go s.preload()
1096+
s.loaderWorker = loaderWorker
1097+
return
1098+
}
1099+
1100+
func (s *Handler) preload() {
1101+
indexHtmlFilename := filepath.Join(s.config.AppDir, "index.html")
1102+
indexHtmlFile, err := os.Open(indexHtmlFilename)
1103+
if err != nil {
1104+
return
1105+
}
1106+
defer indexHtmlFile.Close()
1107+
entries := map[string]struct{}{}
1108+
tokenizer := html.NewTokenizer(indexHtmlFile)
1109+
for {
1110+
tt := tokenizer.Next()
1111+
if tt == html.ErrorToken {
1112+
break
1113+
}
1114+
if tt == html.StartTagToken {
1115+
tagName, moreAttr := tokenizer.TagName()
1116+
if string(tagName) == "script" {
1117+
var (
1118+
srcAttr string
1119+
hrefAttr string
1120+
)
1121+
for moreAttr {
1122+
var key, val []byte
1123+
key, val, moreAttr = tokenizer.TagAttr()
1124+
if bytes.Equal(key, []byte("src")) {
1125+
srcAttr = string(val)
1126+
} else if bytes.Equal(key, []byte("href")) {
1127+
hrefAttr = string(val)
1128+
}
1129+
}
1130+
if hrefAttr != "" && isHttpSepcifier(srcAttr) {
1131+
if !isHttpSepcifier(hrefAttr) && (hrefAttr == "uno.css" || strings.HasSuffix(hrefAttr, "/uno.css") || isModulePath(hrefAttr)) {
1132+
entries[hrefAttr] = struct{}{}
1133+
}
1134+
} else if !isHttpSepcifier(srcAttr) && isModulePath(srcAttr) {
1135+
entries[srcAttr] = struct{}{}
10931136
}
10941137
}
10951138
}
1096-
}()
1097-
s.loaderWorker = loaderWorker
1098-
return
1139+
}
1140+
if len(entries) > 0 {
1141+
u := url.URL{Path: "/"}
1142+
im := base64.RawURLEncoding.EncodeToString([]byte("/index.html"))
1143+
w := &dummyResponseWriter{}
1144+
for entry := range entries {
1145+
pathname := u.ResolveReference(&url.URL{Path: entry}).Path
1146+
r := &http.Request{
1147+
Method: "GET",
1148+
URL: &url.URL{
1149+
Path: pathname,
1150+
},
1151+
}
1152+
if strings.HasSuffix(entry, "uno.css") {
1153+
r.URL.RawQuery = "ctx=" + im
1154+
s.ServeUnoCSS(w, r, r.URL.Query())
1155+
} else {
1156+
r.URL.RawQuery = "im=" + im
1157+
s.ServeModule(w, r, pathname, r.URL.Query(), nil)
1158+
}
1159+
}
1160+
}
10991161
}
11001162

1101-
func (s *Handler) callLoader(args ...any) (format string, code string, err error) {
1163+
func (s *Handler) callLoaderJS(args ...any) (format string, code string, err error) {
11021164
var data string
11031165
format, data, err = s.loaderWorker.Call(args...)
11041166
if err != nil {

web/release.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33
package web
44

5+
const VERSION = "v136"
56
const DEBUG = false

0 commit comments

Comments
 (0)