diff options
| author | Jamal Carvalho <jamal@golang.org> | 2021-12-23 17:00:03 +0000 |
|---|---|---|
| committer | Jamal Carvalho <jamalcarvalho@google.com> | 2021-12-29 17:53:46 +0000 |
| commit | 56b50be2793a61a903f3f01cef39fb70ba4a6947 (patch) | |
| tree | edd1a058a9d035354ae6a48137ebcae7ec81060f | |
| parent | ee355832f53af0ca06a4739d035cac0927334ff9 (diff) | |
| download | go-x-website-56b50be2793a61a903f3f01cef39fb70ba4a6947.tar.xz | |
website: create typescript file handler
To allow for the use of TypeScript in go.dev pages
added a handler for TypeScript files served from
_content/ts. Files requested from this directory
are first transformed from TypeScript to JavaScript
using github.com/evanw/esbuild. JavaScript output is
written to a simple cache so subsequent requests skip
the tranformation step.
Change-Id: I0a161ce3dd20eaddddd5d369d359c65c90d9f607
Reviewed-on: https://go-review.googlesource.com/c/website/+/373718
Run-TryBot: Jamal Carvalho <jamal@golang.org>
Trust: Jamal Carvalho <jamalcarvalho@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alex Rakoczy <alex@golang.org>
| -rw-r--r-- | cmd/golangorg/server.go | 2 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 3 | ||||
| -rw-r--r-- | internal/esbuild/esbuild.go | 88 | ||||
| -rw-r--r-- | internal/esbuild/esbuild_test.go | 85 | ||||
| -rw-r--r-- | internal/esbuild/testdata/error.tmpl | 7 | ||||
| -rw-r--r-- | internal/esbuild/testdata/error.ts | 8 | ||||
| -rw-r--r-- | internal/esbuild/testdata/example.ts | 23 | ||||
| -rw-r--r-- | internal/esbuild/testdata/site.tmpl | 7 |
9 files changed, 224 insertions, 0 deletions
diff --git a/cmd/golangorg/server.go b/cmd/golangorg/server.go index ae9dea33..2b154b35 100644 --- a/cmd/golangorg/server.go +++ b/cmd/golangorg/server.go @@ -36,6 +36,7 @@ import ( "golang.org/x/website/internal/blog" "golang.org/x/website/internal/codewalk" "golang.org/x/website/internal/dl" + "golang.org/x/website/internal/esbuild" "golang.org/x/website/internal/gitfs" "golang.org/x/website/internal/history" "golang.org/x/website/internal/memcache" @@ -264,6 +265,7 @@ func newSite(mux *http.ServeMux, host string, content, goroot fs.FS) (*web.Site, mux.Handle(host+"/cmd/", docs) mux.Handle(host+"/pkg/", docs) mux.Handle(host+"/doc/codewalk/", codewalk.NewServer(fsys, site)) + mux.Handle(host+"/ts/", esbuild.NewServer(fsys, site)) return site, nil } @@ -7,6 +7,7 @@ require ( cloud.google.com/go/datastore v1.2.0 github.com/chromedp/cdproto v0.0.0-20211205231339-d2673e93eee4 github.com/chromedp/chromedp v0.7.6 + github.com/evanw/esbuild v0.14.7 github.com/gomodule/redigo v2.0.0+incompatible github.com/google/go-cmp v0.5.6 github.com/microcosm-cc/bluemonday v1.0.2 @@ -191,6 +191,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/esimov/stackblur-go v1.0.1/go.mod h1:a3zzeKuJKUpCcReHmEsuPaEnq42D2b/bHoCI8UjIuMY= +github.com/evanw/esbuild v0.14.7 h1:At4sSDNq+beZA+z6GUA/sRoqHys9qxKH1RT05eN6Kpo= +github.com/evanw/esbuild v0.14.7/go.mod h1:GG+zjdi59yh3ehDn4ZWfPcATxjPDUH53iU4ZJbp7dkY= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -936,6 +938,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= diff --git a/internal/esbuild/esbuild.go b/internal/esbuild/esbuild.go new file mode 100644 index 00000000..a356e1ef --- /dev/null +++ b/internal/esbuild/esbuild.go @@ -0,0 +1,88 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// package esbuild transforms TypeScript code into +// JavaScript code. +package esbuild + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "path" + "sync" + + "github.com/evanw/esbuild/pkg/api" + "golang.org/x/website/internal/web" +) + +const cacheHeader = "X-Go-Dev-Cache-Hit" + +type server struct { + fsys fs.FS + site *web.Site + cache sync.Map // TypeScript filepath -> JavaScript output +} + +// NewServer returns a new server for handling TypeScript files. +func NewServer(fsys fs.FS, site *web.Site) http.Handler { + return &server{fsys, site, sync.Map{}} +} + +type JSOut struct { + output []byte + stat fs.FileInfo // stat for file when page was loaded +} + +// Handler for TypeScript files. Transforms TypeScript code into +// JavaScript code before serving them. +func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + filename := path.Clean(r.URL.Path)[1:] + if cjs, ok := s.cache.Load(filename); ok { + js := cjs.(*JSOut) + info, err := fs.Stat(s.fsys, filename) + if err == nil && info.ModTime().Equal(js.stat.ModTime()) { + w.Header().Set("Content-Type", "text/javascript; charset=utf-8") + w.Header().Set(cacheHeader, "true") + http.ServeContent(w, r, filename, info.ModTime(), bytes.NewReader(js.output)) + return + } + } + file, err := s.fsys.Open(filename) + if err != nil { + s.site.ServeError(w, r, err) + return + } + var contents bytes.Buffer + _, err = io.Copy(&contents, file) + if err != nil { + s.site.ServeError(w, r, err) + return + } + result := api.Transform(contents.String(), api.TransformOptions{ + Loader: api.LoaderTS, + }) + var buf bytes.Buffer + for _, v := range result.Errors { + fmt.Fprintln(&buf, v.Text) + } + if buf.Len() > 0 { + s.site.ServeError(w, r, errors.New(buf.String())) + return + } + info, err := file.Stat() + if err != nil { + s.site.ServeError(w, r, err) + return + } + w.Header().Set("Content-Type", "text/javascript; charset=utf-8") + http.ServeContent(w, r, filename, info.ModTime(), bytes.NewReader(result.Code)) + s.cache.Store(filename, &JSOut{ + output: result.Code, + stat: info, + }) +} diff --git a/internal/esbuild/esbuild_test.go b/internal/esbuild/esbuild_test.go new file mode 100644 index 00000000..565c6e72 --- /dev/null +++ b/internal/esbuild/esbuild_test.go @@ -0,0 +1,85 @@ +package esbuild + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "syscall" + "testing" + + "github.com/google/go-cmp/cmp" + "golang.org/x/website/internal/web" +) + +func TestServeHTTP(t *testing.T) { + exampleOut := `/** + * @license + * Copyright 2021 The Go Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ +function sayHello(to) { + console.log("Hello, " + to + "!"); +} +const world = { + name: "World", + toString() { + return this.name; + } +}; +sayHello(world); +` + tests := []struct { + name string + path string + wantCode int + wantBody string + wantCacheHeader bool + }{ + { + name: "example code", + path: "/example.ts", + wantCode: 200, + wantBody: exampleOut, + }, + { + name: "example code cached", + path: "/example.ts", + wantCode: 200, + wantBody: exampleOut, + wantCacheHeader: true, + }, + { + name: "file not found", + path: "/notfound.ts", + wantCode: 500, + wantBody: fmt.Sprintf("\n\nopen testdata/notfound.ts: %s\n", syscall.ENOENT), + }, + { + name: "syntax error", + path: "/error.ts", + wantCode: 500, + wantBody: "\n\nExpected identifier but found "function"\n\n", + }, + } + fsys := os.DirFS("testdata") + server := NewServer(fsys, web.NewSite(fsys)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, tt.path, nil) + got := httptest.NewRecorder() + server.ServeHTTP(got, req) + gotHeader := got.Header().Get(cacheHeader) == "true" + if got.Code != tt.wantCode { + t.Errorf("got status %d but wanted %d", got.Code, http.StatusOK) + } + if (tt.wantCacheHeader && !gotHeader) || (!tt.wantCacheHeader && gotHeader) { + t.Errorf("got cache hit %v but wanted %v", gotHeader, tt.wantCacheHeader) + } + if diff := cmp.Diff(tt.wantBody, got.Body.String()); diff != "" { + t.Errorf("ServeHTTP() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/esbuild/testdata/error.tmpl b/internal/esbuild/testdata/error.tmpl new file mode 100644 index 00000000..d0a2b493 --- /dev/null +++ b/internal/esbuild/testdata/error.tmpl @@ -0,0 +1,7 @@ +<!-- + Copyright 2021 The Go Authors. All rights reserved. + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. +--> + +{{define "layout"}}{{.error}}{{end}} diff --git a/internal/esbuild/testdata/error.ts b/internal/esbuild/testdata/error.ts new file mode 100644 index 00000000..b60870b4 --- /dev/null +++ b/internal/esbuild/testdata/error.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2021 The Go Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +const function = () => {}; diff --git a/internal/esbuild/testdata/example.ts b/internal/esbuild/testdata/example.ts new file mode 100644 index 00000000..0bfd1263 --- /dev/null +++ b/internal/esbuild/testdata/example.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2021 The Go Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +interface Target { + toString(): string; +} + +function sayHello(to: Target): void { + console.log('Hello, ' + to + '!'); +} + +const world = { + name: 'World', + toString(): string { + return this.name; + }, +}; + +sayHello(world); diff --git a/internal/esbuild/testdata/site.tmpl b/internal/esbuild/testdata/site.tmpl new file mode 100644 index 00000000..650ac44a --- /dev/null +++ b/internal/esbuild/testdata/site.tmpl @@ -0,0 +1,7 @@ +<!-- + Copyright 2021 The Go Authors. All rights reserved. + Use of this source code is governed by a BSD-style + license that can be found in the LICENSE file. +--> + +{{block "entirepage" .}}{{block "layout" .}}{{.Content}}{{end}}{{end}} |
