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>
This commit is contained in:
Родитель
ee355832f5
Коммит
56b50be279
|
@ -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
|
||||
}
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -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
|
||||
|
|
3
go.sum
3
go.sum
|
@ -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=
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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}}
|
|
@ -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 = () => {};
|
|
@ -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);
|
|
@ -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}}
|
Загрузка…
Ссылка в новой задаче