relay-serer: simplify build process
Kim committed
Nov 18, 2025 at 16:59 UTC
5f9ae1d51fb03cbd8b5737320bd4d530a76978b7
7 files changed
+57
-102
.gitignore
+2
-4
@@ -10,10 +10,8 @@
10
bin/
11
chat
12
13
-cmd/relay-server/dist/*
14
-!cmd/relay-server/dist/.gitkeep
15
-cmd/relay-server/app/*
16
-!cmd/relay-server/app/.gitkeep
13
+cmd/relay-server/dist/app
14
+cmd/relay-server/dist/wasm
15
16
# Test binary, built with `go test -c`
17
*.test
Dockerfile
+5
-48
@@ -1,76 +1,33 @@
1
# syntax=docker/dockerfile:1
2
3
-# Frontend build stage
4
-FROM --platform=$BUILDPLATFORM node:22-alpine AS frontend-builder
5
-
6
-WORKDIR /src
7
-
8
-# Copy frontend package files
9
-COPY cmd/relay-server/frontend/package*.json ./cmd/relay-server/frontend/
10
-
11
-# Install frontend dependencies
12
-RUN --mount=type=cache,target=/root/.npm \
13
- cd cmd/relay-server/frontend && \
14
- npm ci
15
-
16
-# Copy frontend source code
17
-COPY cmd/relay-server/frontend ./cmd/relay-server/frontend
18
-
19
-# Build frontend
20
-RUN cd cmd/relay-server/frontend && npm run build
21
-
22
-# Go builder stage
3
FROM --platform=$BUILDPLATFORM golang:1 AS builder
24
-
4
WORKDIR /src
5
27
-# Install make, binaryen (wasm-opt), and brotli CLI for WASM build/compression
6
RUN apt-get update && apt-get install -y --no-install-recommends \
29
- binaryen \
30
- brotli \
31
- make \
32
- && rm -rf /var/lib/apt/lists/*
7
+ nodejs npm brotli make && rm -rf /var/lib/apt/lists/*
8
34
-# Set GOMODCACHE to cache Go modules in cache volume
35
-RUN go env -w GOMODCACHE=/root/.cache/go-build
36
-
37
-# Copy go.mod and go.sum
9
COPY go.mod go.sum ./
10
+RUN --mount=type=cache,target=/go/pkg/mod go mod download
11
40
-# Download dependencies
41
-RUN --mount=type=cache,target=/go/pkg/mod \
42
- go mod download
43
-
44
-# Copy the rest of the source code
12
COPY . .
13
47
-# Copy built frontend from frontend-builder stage
48
-COPY --from=frontend-builder /src/cmd/relay-server/app ./cmd/relay-server/app
49
-
50
-RUN --mount=type=cache,target=/go/pkg/mod \
51
- --mount=type=cache,target=/root/.cache/go-build \
52
- make build-wasm
14
+RUN --mount=type=cache,target=/root/.npm \
15
+ make build-frontend
16
54
-ARG TARGETPLATFORM
17
ARG TARGETOS
18
ARG TARGETARCH
57
-
19
RUN --mount=type=cache,target=/go/pkg/mod \
20
--mount=type=cache,target=/root/.cache/go-build \
21
+ make build-wasm && \
22
GOOS=${TARGETOS} GOARCH=${TARGETARCH} make build-server
23
24
FROM gcr.io/distroless/static-debian12:nonroot
25
64
-# Copy server binary
26
COPY --from=builder /src/bin/relay-server /usr/bin/relay-server
27
67
-# Set default environment variables
28
ENV PORTAL_UI_URL=http://localhost:4017
29
ENV PORTAL_FRONTEND_URL=http://*.localhost:4017
30
ENV BOOTSTRAP_URIS=ws://localhost:4017/relay
31
72
-# Expose ports
73
-# 4017: relay server and portal frontend
32
EXPOSE 4017
75
-
33
ENTRYPOINT ["/usr/bin/relay-server"]
Makefile
+20
-17
@@ -33,12 +33,13 @@ build-protoc:
33
# Build WASM artifacts with wasm-opt optimization and generate manifest
34
build-wasm:
35
@echo "[wasm] building webclient WASM..."
36
- GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w" -o cmd/relay-server/dist/portal.wasm ./cmd/webclient
36
+ @mkdir -p cmd/relay-server/dist/wasm
37
+ GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w" -o cmd/relay-server/dist/wasm/portal.wasm ./cmd/webclient
38
39
@echo "[wasm] optimizing with wasm-opt..."
40
@if command -v wasm-opt >/dev/null 2>&1; then \
40
- wasm-opt -Oz --enable-bulk-memory cmd/relay-server/dist/portal.wasm -o cmd/relay-server/dist/portal.wasm.tmp && \
41
- mv cmd/relay-server/dist/portal.wasm.tmp cmd/relay-server/dist/portal.wasm; \
41
+ wasm-opt -Oz --enable-bulk-memory cmd/relay-server/dist/wasm/portal.wasm -o cmd/relay-server/dist/wasm/portal.wasm.tmp && \
42
+ mv cmd/relay-server/dist/wasm/portal.wasm.tmp cmd/relay-server/dist/wasm/portal.wasm; \
43
echo "[wasm] optimization complete"; \
44
else \
45
echo "[wasm] WARNING: wasm-opt not found, skipping optimization"; \
@@ -46,25 +47,25 @@ build-wasm:
47
fi
48
49
@echo "[wasm] calculating SHA256 hash..."
49
- @WASM_HASH=$$(shasum -a 256 cmd/relay-server/dist/portal.wasm | awk '{print $$1}'); \
50
+ @WASM_HASH=$$(shasum -a 256 cmd/relay-server/dist/wasm/portal.wasm | awk '{print $$1}'); \
51
echo "[wasm] SHA256: $$WASM_HASH"; \
52
echo "[wasm] cleaning old hash files..."; \
52
- find cmd/relay-server/dist -name '[0-9a-f]*.wasm' ! -name "$$WASM_HASH.wasm" -type f -delete 2>/dev/null || true; \
53
- cp cmd/relay-server/dist/portal.wasm cmd/relay-server/dist/$$WASM_HASH.wasm; \
54
- rm -f cmd/relay-server/dist/portal.wasm; \
55
- echo "[wasm] content-addressed WASM: $$WASM_HASH.wasm"
53
+ find cmd/relay-server/dist/wasm -name '[0-9a-f]*.wasm' ! -name "$$WASM_HASH.wasm" -type f -delete 2>/dev/null || true; \
54
+ cp cmd/relay-server/dist/wasm/portal.wasm cmd/relay-server/dist/wasm/$$WASM_HASH.wasm; \
55
+ rm -f cmd/relay-server/dist/wasm/portal.wasm; \
56
+ echo "[wasm] content-addressed WASM: dist/wasm/$$WASM_HASH.wasm"
57
58
@echo "[wasm] copying additional resources..."
58
- @cp cmd/webclient/wasm_exec.js cmd/relay-server/dist/wasm_exec.js
59
- @cp cmd/webclient/service-worker.js cmd/relay-server/dist/service-worker.js
60
- @cp cmd/webclient/index.html cmd/relay-server/dist/portal.html
61
- @cp cmd/webclient/portal.mp4 cmd/relay-server/dist/portal.mp4
59
+ @cp cmd/webclient/wasm_exec.js cmd/relay-server/dist/wasm/wasm_exec.js
60
+ @cp cmd/webclient/service-worker.js cmd/relay-server/dist/wasm/service-worker.js
61
+ @cp cmd/webclient/index.html cmd/relay-server/dist/wasm/portal.html
62
+ @cp cmd/webclient/portal.mp4 cmd/relay-server/dist/wasm/portal.mp4
63
@echo "[wasm] build complete"
64
65
@echo "[wasm] precompressing webclient WASM with brotli..."
65
- @WASM_FILE=$$(ls cmd/relay-server/dist/[0-9a-f]*.wasm 2>/dev/null | head -n1); \
66
+ @WASM_FILE=$$(ls cmd/relay-server/dist/wasm/[0-9a-f]*.wasm 2>/dev/null | head -n1); \
67
if [ -z "$$WASM_FILE" ]; then \
67
- echo "[wasm] ERROR: no content-addressed WASM found in cmd/relay-server/dist; run build-wasm first"; \
68
+ echo "[wasm] ERROR: no content-addressed WASM found in cmd/relay-server/dist/wasm; run build-wasm first"; \
69
exit 1; \
70
fi; \
71
WASM_HASH=$$(basename "$$WASM_FILE" .wasm); \
@@ -72,14 +73,15 @@ build-wasm:
73
echo "[wasm] ERROR: brotli not found; install brotli to build compressed WASM"; \
74
exit 1; \
75
fi; \
75
- brotli -f "$$WASM_FILE" -o "cmd/relay-server/dist/$$WASM_HASH.wasm.br"; \
76
+ brotli -f "$$WASM_FILE" -o "cmd/relay-server/dist/wasm/$$WASM_HASH.wasm.br"; \
77
rm -f "$$WASM_FILE"; \
77
- echo "[wasm] brotli: cmd/relay-server/dist/$$WASM_HASH.wasm.br"
78
+ echo "[wasm] brotli: cmd/relay-server/dist/wasm/$$WASM_HASH.wasm.br"
79
80
81
# Build React frontend with Tailwind CSS 4
82
build-frontend:
83
@echo "[frontend] building React frontend..."
84
+ @mkdir -p cmd/relay-server/dist/app
85
@cd cmd/relay-server/frontend && npm i && npm run build
86
@echo "[frontend] build complete"
87
@@ -95,4 +97,5 @@ build-tunnel:
97
98
clean:
99
rm -rf bin
98
- rm -rf app
100
+ rm -rf cmd/relay-server/dist/app
101
+ rm -rf cmd/relay-server/dist/wasm
cmd/relay-server/app/.gitkeep
cmd/relay-server/frontend/vite.config.ts
+2
-2
@@ -12,7 +12,7 @@ export default defineConfig({
12
{
13
name: "rename-index",
14
closeBundle() {
15
- const appDir = resolve(process.cwd(), "../app");
15
+ const appDir = resolve(process.cwd(), "../dist/app");
16
const indexPath = resolve(appDir, "index.html");
17
const portalPath = resolve(appDir, "portal.html");
18
try {
@@ -30,7 +30,7 @@ export default defineConfig({
30
},
31
},
32
build: {
33
- outDir: "../app",
33
+ outDir: "../dist/app",
34
emptyOutDir: false,
35
rollupOptions: {
36
output: {
cmd/relay-server/view.go
+11
-7
@@ -2,6 +2,7 @@ package main
2
3
import (
4
"context"
5
+ "embed"
6
"encoding/json"
7
"fmt"
8
"net/http"
@@ -18,11 +19,14 @@ import (
19
"gosuda.org/portal/sdk"
20
)
21
22
+//go:embed dist/*
23
+var distFS embed.FS
24
+
25
func serveAsset(mux *http.ServeMux, route, assetPath, contentType string) {
26
mux.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
23
-
24
- fullPath := pathpkg.Join("app", assetPath)
25
- b, err := appFS.ReadFile(fullPath)
27
+ // Read from dist/app subdirectory of the embedded FS
28
+ fullPath := pathpkg.Join("dist", "app", assetPath)
29
+ b, err := distFS.ReadFile(fullPath)
30
if err != nil {
31
http.NotFound(w, r)
32
return
@@ -44,10 +48,10 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
48
// Create app UI mux
49
appMux := http.NewServeMux()
50
47
- // Serve embedded favicons (ico/png/svg) for portal UI
48
- serveAsset(appMux, "/favicon.ico", "/favicon.ico", "image/x-icon")
49
- serveAsset(appMux, "/favicon.png", "/favicon.png", "image/png")
50
- serveAsset(appMux, "/favicon.svg", "/favicon.svg", "image/svg+xml")
51
+ // Serve favicons (ico/png/svg) from dist/app
52
+ serveAsset(appMux, "/favicon.ico", "favicon.ico", "image/x-icon")
53
+ serveAsset(appMux, "/favicon.png", "favicon.png", "image/png")
54
+ serveAsset(appMux, "/favicon.svg", "favicon.svg", "image/svg+xml")
55
56
// Portal app assets (JS, CSS, etc.) - served from /app/
57
appMux.HandleFunc("/app/", func(w http.ResponseWriter, r *http.Request) {
cmd/relay-server/wasm.go
renamed
+17
-24
@@ -1,7 +1,6 @@
1
package main
2
3
import (
4
- "embed"
4
"encoding/json"
5
"net/http"
6
pathpkg "path"
@@ -13,12 +12,6 @@ import (
12
"gosuda.org/portal/portal"
13
)
14
16
-//go:embed dist/*
17
-var wasmFS embed.FS
18
-
19
-//go:embed app/*
20
-var appFS embed.FS
21
-
15
// portalHost is the host for portal frontend.
16
var portalHost = "localhost"
17
@@ -44,8 +37,8 @@ var (
37
38
// initWasmCache loads pre-built WASM artifacts (precompressed) into memory on startup.
39
func initWasmCache() error {
47
- // Read all files in embedded dist directory
48
- entries, err := wasmFS.ReadDir("dist")
40
+ // Read all files in embedded dist/wasm directory
41
+ entries, err := distFS.ReadDir("dist/wasm")
42
if err != nil {
43
return err
44
}
@@ -60,7 +53,7 @@ func initWasmCache() error {
53
if strings.HasSuffix(name, ".wasm.br") && len(name) == 72 { // 64 + len(".wasm.br")
54
hash := strings.TrimSuffix(name, ".wasm.br")
55
if isHexString(hash) && len(hash) == 64 {
63
- fullPath := pathpkg.Join("dist", name)
56
+ fullPath := pathpkg.Join("dist", "wasm", name)
57
// Cache under the URL path (<hash>.wasm) while reading the
58
// brotli-compressed artifact (<hash>.wasm.br) from embed.FS.
59
cacheKey := hash + ".wasm"
@@ -86,7 +79,7 @@ func cacheWasmFile(name, fullPath string) error {
79
80
// Load precompressed variant (brotli) from embed.FS (<hash>.wasm.br)
81
var brData []byte
89
- data, err := wasmFS.ReadFile(fullPath)
82
+ data, err := distFS.ReadFile(fullPath)
83
if err != nil {
84
log.Warn().Err(err).Str("file", fullPath).Msg("failed to read brotli-compressed WASM")
85
} else {
@@ -168,8 +161,8 @@ func servePortalHTMLWithSSR(w http.ResponseWriter, r *http.Request, serv *portal
161
setCORSHeaders(w)
162
163
// Read portal.html from embedded FS
171
- fullPath := pathpkg.Join("app", "portal.html")
172
- htmlContent, err := appFS.ReadFile(fullPath)
164
+ fullPath := pathpkg.Join("dist", "app", "portal.html")
165
+ htmlContent, err := distFS.ReadFile(fullPath)
166
if err != nil {
167
log.Error().Err(err).Msg("Failed to read portal.html")
168
http.NotFound(w, r)
@@ -241,8 +234,8 @@ func serveCompressedWasm(w http.ResponseWriter, r *http.Request, filePath string
234
if !ok {
235
log.Debug().Str("path", filePath).Msg("WASM file not in cache")
236
// Fallback: try to serve uncompressed WASM from embedded FS
244
- fullPath := pathpkg.Join("dist", filePath)
245
- data, err := wasmFS.ReadFile(fullPath)
237
+ fullPath := pathpkg.Join("dist", "wasm", filePath)
238
+ data, err := distFS.ReadFile(fullPath)
239
if err != nil {
240
log.Debug().Err(err).Str("path", fullPath).Msg("WASM file not found in embedded FS")
241
http.NotFound(w, r)
@@ -310,8 +303,8 @@ func serveAppStatic(w http.ResponseWriter, r *http.Request, path string, serv *p
303
}
304
305
// Try to read from embedded FS
313
- fullPath := pathpkg.Join("app", path)
314
- data, err := appFS.ReadFile(fullPath)
306
+ fullPath := pathpkg.Join("dist", "app", path)
307
+ data, err := distFS.ReadFile(fullPath)
308
if err != nil {
309
// File not found - fallback to portal.html with SSR for SPA routing
310
log.Debug().Err(err).Str("path", path).Msg("app static file not found, falling back to SSR")
@@ -381,8 +374,8 @@ func servePortalStatic(w http.ResponseWriter, r *http.Request) {
374
func serveStaticFile(w http.ResponseWriter, r *http.Request, path string, contentType string) {
375
setCORSHeaders(w)
376
384
- fullPath := pathpkg.Join("dist", path)
385
- data, err := wasmFS.ReadFile(fullPath)
377
+ fullPath := pathpkg.Join("dist", "wasm", path)
378
+ data, err := distFS.ReadFile(fullPath)
379
if err != nil {
380
log.Debug().Err(err).Str("path", path).Msg("static file not found")
381
http.NotFound(w, r)
@@ -414,8 +407,8 @@ func serveStaticFile(w http.ResponseWriter, r *http.Request, path string, conten
407
func serveStaticFileWithFallback(w http.ResponseWriter, r *http.Request, path string, contentType string) {
408
setCORSHeaders(w)
409
417
- fullPath := pathpkg.Join("dist", path)
418
- data, err := wasmFS.ReadFile(fullPath)
410
+ fullPath := pathpkg.Join("dist", "wasm", path)
411
+ data, err := distFS.ReadFile(fullPath)
412
if err != nil {
413
// File not found - fallback to portal.html for SPA routing
414
log.Debug().Err(err).Str("path", path).Msg("static file not found, serving portal.html")
@@ -524,7 +517,7 @@ func serveDynamicManifest(w http.ResponseWriter) {
517
518
// Fallback: scan embedded WASM directory if cache is empty
519
if wasmHash == "" {
527
- entries, err := wasmFS.ReadDir("dist")
520
+ entries, err := distFS.ReadDir("dist/wasm")
521
if err == nil {
522
for _, entry := range entries {
523
if entry.IsDir() {
@@ -580,8 +573,8 @@ func serveDynamicServiceWorker(w http.ResponseWriter, r *http.Request) {
573
setCORSHeaders(w)
574
575
// Read the service-worker.js template
583
- fullPath := pathpkg.Join("dist", "service-worker.js")
584
- content, err := wasmFS.ReadFile(fullPath)
576
+ fullPath := pathpkg.Join("dist", "wasm", "service-worker.js")
577
+ content, err := distFS.ReadFile(fullPath)
578
if err != nil {
579
log.Error().Err(err).Msg("Failed to read service-worker.js")
580
http.NotFound(w, r)