cmd/tsconnect: stop writing build artifacts into src/

We can't write to src/ when tsconnect is used a dependency in another
repo (see also b763a12331). We therefore
need to switch from writing to src/ to using esbuild plugins to handle
the requests for wasm_exec.js (the Go JS runtime for Wasm) and the
Wasm build of the Go module.

This has the benefit of allowing Go/Wasm changes to be picked up without
restarting the server when in dev mode (Go compilation is fast enough
that we can do this on every request, CSS compilation continues to be
the long pole).

Fixes #5382

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
pull/5421/head
Mihai Parparita 2 years ago committed by Mihai Parparita
parent facafd8819
commit 78b90c3685

@ -1,4 +1,2 @@
src/wasm_exec.js
src/main.wasm
node_modules/ node_modules/
dist/ dist/

@ -6,6 +6,7 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"os/exec" "os/exec"
@ -32,65 +33,91 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
return nil, fmt.Errorf("Cannot change cwd: %w", err) return nil, fmt.Errorf("Cannot change cwd: %w", err)
} }
} }
if err := buildDeps(dev); err != nil { if err := installJSDeps(); err != nil {
return nil, fmt.Errorf("Cannot build deps: %w", err) return nil, fmt.Errorf("Cannot install JS deps: %w", err)
} }
return &esbuild.BuildOptions{ return &esbuild.BuildOptions{
EntryPoints: []string{"src/index.ts", "src/index.css"}, EntryPoints: []string{"src/index.ts", "src/index.css"},
Loader: map[string]esbuild.Loader{".wasm": esbuild.LoaderFile},
Outdir: *distDir, Outdir: *distDir,
Bundle: true, Bundle: true,
Sourcemap: esbuild.SourceMapLinked, Sourcemap: esbuild.SourceMapLinked,
LogLevel: esbuild.LogLevelInfo, LogLevel: esbuild.LogLevelInfo,
Define: map[string]string{"DEBUG": strconv.FormatBool(dev)}, Define: map[string]string{"DEBUG": strconv.FormatBool(dev)},
Target: esbuild.ES2017, Target: esbuild.ES2017,
Plugins: []esbuild.Plugin{{ Plugins: []esbuild.Plugin{
Name: "tailscale-tailwind", {
Setup: func(build esbuild.PluginBuild) { Name: "tailscale-tailwind",
setupEsbuildTailwind(build, dev) Setup: func(build esbuild.PluginBuild) {
setupEsbuildTailwind(build, dev)
},
},
{
Name: "tailscale-go-wasm-exec-js",
Setup: setupEsbuildWasmExecJS,
}, },
}}, {
Name: "tailscale-wasm",
Setup: func(build esbuild.PluginBuild) {
setupEsbuildWasm(build, dev)
},
},
},
JSXMode: esbuild.JSXModeAutomatic, JSXMode: esbuild.JSXModeAutomatic,
}, nil }, nil
} }
// buildDeps builds the static assets that are needed for the server (except for // setupEsbuildWasmExecJS generates an esbuild plugin that serves the current
// JS/CSS bundling, which is handled by esbuild). // wasm_exec.js runtime helper library from the Go toolchain.
func buildDeps(dev bool) error { func setupEsbuildWasmExecJS(build esbuild.PluginBuild) {
if err := copyWasmExec(); err != nil { wasmExecSrcPath := filepath.Join(runtime.GOROOT(), "misc", "wasm", "wasm_exec.js")
return fmt.Errorf("Cannot copy wasm_exec.js: %w", err) build.OnResolve(esbuild.OnResolveOptions{
} Filter: "./wasm_exec$",
if err := buildWasm(dev); err != nil { }, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {
return fmt.Errorf("Cannot build main.wasm: %w", err) return esbuild.OnResolveResult{Path: wasmExecSrcPath}, nil
} })
if err := installJSDeps(); err != nil {
return fmt.Errorf("Cannot install JS deps: %w", err)
}
return nil
} }
// copyWasmExec grabs the current wasm_exec.js runtime helper library from the // setupEsbuildWasm generates an esbuild plugin that builds the Tailscale wasm
// Go toolchain. // binary and serves it as a file that the JS can load.
func copyWasmExec() error { func setupEsbuildWasm(build esbuild.PluginBuild, dev bool) {
log.Printf("Copying wasm_exec.js...\n") // Add a resolve hook to convince esbuild that the path exists.
wasmExecSrcPath := filepath.Join(runtime.GOROOT(), "misc", "wasm", "wasm_exec.js") build.OnResolve(esbuild.OnResolveOptions{
wasmExecDstPath := filepath.Join("src", "wasm_exec.js") Filter: "./main.wasm$",
contents, err := os.ReadFile(wasmExecSrcPath) }, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {
return esbuild.OnResolveResult{
Path: "./src/main.wasm",
Namespace: "generated",
}, nil
})
build.OnLoad(esbuild.OnLoadOptions{
Filter: "./src/main.wasm$",
}, func(args esbuild.OnLoadArgs) (esbuild.OnLoadResult, error) {
contents, err := buildWasm(dev)
if err != nil {
return esbuild.OnLoadResult{}, fmt.Errorf("Cannot build main.wasm: %w", err)
}
contentsStr := string(contents)
return esbuild.OnLoadResult{
Contents: &contentsStr,
Loader: esbuild.LoaderFile,
}, nil
})
}
func buildWasm(dev bool) ([]byte, error) {
start := time.Now()
outputFile, err := ioutil.TempFile("", "main.*.wasm")
if err != nil { if err != nil {
return err return nil, fmt.Errorf("Cannot create main.wasm output file: %w", err)
} }
return os.WriteFile(wasmExecDstPath, contents, 0600) outputPath := outputFile.Name()
} defer os.Remove(outputPath)
// buildWasm builds the Tailscale wasm binary and places it where the JS can
// load it.
func buildWasm(dev bool) error {
log.Printf("Building wasm...\n")
args := []string{"build", "-tags", "tailscale_go,osusergo,netgo,nethttpomithttp2,omitidna,omitpemdecrypt"} args := []string{"build", "-tags", "tailscale_go,osusergo,netgo,nethttpomithttp2,omitidna,omitpemdecrypt"}
if !dev { if !dev {
if *devControl != "" { if *devControl != "" {
return fmt.Errorf("Development control URL can only be used in dev mode.") return nil, fmt.Errorf("Development control URL can only be used in dev mode.")
} }
// Omit long paths and debug symbols in release builds, to reduce the // Omit long paths and debug symbols in release builds, to reduce the
// generated WASM binary size. // generated WASM binary size.
@ -98,13 +125,19 @@ func buildWasm(dev bool) error {
} else if *devControl != "" { } else if *devControl != "" {
args = append(args, "-ldflags", fmt.Sprintf("-X 'main.ControlURL=%v'", *devControl)) args = append(args, "-ldflags", fmt.Sprintf("-X 'main.ControlURL=%v'", *devControl))
} }
args = append(args, "-o", "src/main.wasm", "./wasm")
args = append(args, "-o", outputPath, "./wasm")
cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...) cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), args...)
cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm") cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd.Run() err = cmd.Run()
if err != nil {
return nil, fmt.Errorf("Cannot build main.wasm: %w", err)
}
log.Printf("Built wasm in %v\n", time.Since(start))
return os.ReadFile(outputPath)
} }
// installJSDeps installs the JavaScript dependencies specified by package.json // installJSDeps installs the JavaScript dependencies specified by package.json

Loading…
Cancel
Save