feat(build): integrate frontend build into backend binary and update scripts
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
* text=auto
|
||||
|
||||
# cmd.exe misparses a batch file whose lines end in a bare LF: it loses
|
||||
# characters at the start of later lines, so a working script silently turns
|
||||
# into "command not recognized" errors. Force CRLF on checkout.
|
||||
*.bat text eol=crlf
|
||||
|
||||
# The mirror image: a CR at the end of a shebang or line breaks these.
|
||||
*.sh text eol=lf
|
||||
@@ -48,10 +48,10 @@ jobs:
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
# --frontend builds the Vue app inside the script. The second call omits
|
||||
# it: the frontend is platform independent and dist/ is already built.
|
||||
# Each script builds the Vue console itself and then compiles it into the
|
||||
# binary (web/embed.go), so the uploaded executables are self-contained.
|
||||
- name: Build Linux (amd64)
|
||||
run: ${{ matrix.build_linux }} --frontend
|
||||
run: ${{ matrix.build_linux }}
|
||||
|
||||
- name: Build Windows (amd64)
|
||||
run: ${{ matrix.build_windows }}
|
||||
@@ -64,14 +64,3 @@ jobs:
|
||||
nukumizu-linux-amd64
|
||||
nukumizu-windows-amd64.exe
|
||||
if-no-files-found: error
|
||||
|
||||
# The binaries read frontend/dist at runtime (os.DirFS in web/embed.go),
|
||||
# they do not embed it. Uploaded once — the frontend is platform
|
||||
# independent, so both matrix legs produce the same files.
|
||||
- name: Upload frontend bundle
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nukumizu-frontend
|
||||
path: frontend/dist
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -6,6 +6,11 @@ bot_node_config.json
|
||||
*.exe
|
||||
nukumizu-linux-amd64
|
||||
|
||||
# The built web console. web/embed.go compiles it into the binary, and the
|
||||
# build-*.sh / build-*.bat scripts rebuild it before every compile.
|
||||
/web/dist/
|
||||
/frontend/dist/
|
||||
|
||||
# SQLite database files. They live on a network share (Y:), and git/cloud
|
||||
# sync touching them while a WAL database is open corrupts the WAL index and
|
||||
# crashes the process (EXCEPTION_IN_PAGE_ERROR). Never track these.
|
||||
|
||||
@@ -15,7 +15,7 @@ Nukumizu connects to a Komari Dashboard instance, keeps an in-memory view of eve
|
||||
- **Customizable message templates** — every bot/notification message is rendered from a template in `config.json`.
|
||||
- **Storage** — SQLite (pure-Go driver) for `user.db` and `log.db`; safe on network shares (WAL disabled).
|
||||
- **Dashboard API** — token-authenticated REST API plus a live log-streaming WebSocket.
|
||||
- **Web console** — a Vue 3 admin UI for browsing nodes, editing `config.json`, managing bot trust, and tailing logs. The Go server serves the built bundle from `frontend/dist`.
|
||||
- **Web console** — a Vue 3 admin UI for browsing nodes, editing `config.json`, managing bot trust, and tailing logs. The built bundle is embedded in the binary, so a single executable serves both the API and the console.
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -52,7 +52,8 @@ nukumizu-backend/
|
||||
│ ├── logBroadcaster.go # Fan-out to WebSocket clients
|
||||
│ └── logSocketHandler.go # /api/system/getLogs WebSocket handler
|
||||
├── web/
|
||||
│ ├── embed.go # Locates the built console (frontend/dist)
|
||||
│ ├── embed.go # Embeds the built console (web/dist) in the binary
|
||||
│ ├── dist/ # Vite build output — generated, gitignored
|
||||
│ └── handler.go # Static file serving + SPA fallback
|
||||
├── internal/
|
||||
│ ├── komari/
|
||||
@@ -344,7 +345,7 @@ QQ and Telegram are *interactive* channels. Email, ntfy, and webhook are **statu
|
||||
|
||||
Requires Go 1.25+ and — to build the web console — Node.js 22+.
|
||||
|
||||
Helper scripts in the repo root bake the current git commit and build time into the binary via `-ldflags`. The name states the **target** platform, and each target has a Windows (`.bat`) and a Linux/macOS (`.sh`) flavor: run the flavor for the host you are building on, since every script cross-compiles to its target.
|
||||
Helper scripts in the repo root build the Vue console first, then compile the backend with it embedded. They also bake the current git commit and build time into the binary via `-ldflags`. The name states the **target** platform, and each target has a Windows (`.bat`) and a Linux/macOS (`.sh`) flavor: run the flavor for the host you are building on, since every script cross-compiles to its target.
|
||||
|
||||
| Script | Output |
|
||||
|---|---|
|
||||
@@ -361,17 +362,17 @@ build-linux-x86_64.bat
|
||||
build-win-x86_64.bat
|
||||
```
|
||||
|
||||
Pass `--frontend` to build the Vue console first (`npm ci` + `npm run build` inside `frontend/`); without it only the backend is compiled:
|
||||
The console is **embedded in the binary**. Vite writes it to `web/dist` and `web/embed.go` compiles that directory in with `go:embed`, so the executable serves the whole frontend on its own — copy it anywhere, with neither `frontend/` nor `web/dist` next to it, and `/` still returns the console. The build scripts run `npm ci` when `frontend/node_modules` is missing and `npm run build` on every run, so they need Node.js 22+ on the build machine (not on the machine that runs the binary).
|
||||
|
||||
```bash
|
||||
./build-linux-x86_64.sh --frontend
|
||||
```
|
||||
|
||||
The console is **not embedded in the binary** — `web/` reads `frontend/dist` from disk at runtime. A binary built without `--frontend` still starts and serves the API, but `/` answers `500 index.html not found` until a built `frontend/dist` sits in the working directory. Build it once with `--frontend`, then re-run any of the four scripts without the flag.
|
||||
Building the backend therefore requires the console to have been built at least once: `web/dist` is a generated, gitignored directory, and `go build` fails with `pattern all:dist: no matching files found` until it exists. Any `build-*` script handles that ordering for you.
|
||||
|
||||
Equivalent manual builds:
|
||||
|
||||
```bash
|
||||
# 1. Console (once per frontend change)
|
||||
cd frontend && npm ci && npm run build && cd ..
|
||||
|
||||
# 2. Backend
|
||||
# Linux / macOS
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -ldflags "-X main.CommitHash=$(git rev-parse --short HEAD) -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||
@@ -385,7 +386,7 @@ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
|
||||
|
||||
`main.CommitHash` and `main.BuildTime` are surfaced in logs and in the `BOT_STARTED` message.
|
||||
|
||||
CI (`.github/workflows/build.yml`) runs both flavors — `.sh` on `ubuntu-latest`, `.bat` on `windows-latest` — and uploads the two binaries plus the frontend bundle as artifacts.
|
||||
CI (`.github/workflows/build.yml`) runs both flavors — `.sh` on `ubuntu-latest`, `.bat` on `windows-latest` — and uploads the two self-contained binaries as artifacts.
|
||||
|
||||
## Running
|
||||
|
||||
@@ -403,7 +404,7 @@ On startup the program logs in to Komari, loads node state, connects the status
|
||||
|
||||
### Frontend development
|
||||
|
||||
`run.bat` only runs the Go backend — it does not build the console. While working on the frontend, run the two sides separately:
|
||||
`run.bat` only runs the Go backend — it does not build the console, so `web/dist` must already exist (run any `build-*` script once, or `npm run build` in `frontend/`) or `go run` fails to compile. While working on the frontend, run the two sides separately:
|
||||
|
||||
```bash
|
||||
# Terminal 1 — backend (API + WebSocket) on :8080
|
||||
@@ -421,7 +422,7 @@ Open http://localhost:5173. The dev server proxies `/api` — including the log
|
||||
NUKUMIZU_API=http://192.168.1.10:8080 npm run dev
|
||||
```
|
||||
|
||||
For a production build the Go server serves `frontend/dist` itself, on the normal listen address — see [Building](#building).
|
||||
For a production build the Go server serves the embedded console itself, on the normal listen address — see [Building](#building). Vite is not involved at runtime, so `npm run build` alone does not change what a running binary serves: rebuild the binary to pick up frontend changes.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+22
-10
@@ -4,21 +4,33 @@ setlocal enabledelayedexpansion
|
||||
:: Build from the repository root, however the script was invoked.
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: Optional first argument: --frontend also builds the Vue frontend, which web/
|
||||
:: serves at runtime from frontend/dist. Omitted, only the backend is compiled.
|
||||
set BUILD_FRONTEND=0
|
||||
if /i "%~1"=="--frontend" set BUILD_FRONTEND=1
|
||||
|
||||
if not "%BUILD_FRONTEND%"=="1" goto :backend
|
||||
|
||||
:: The Vue console is built first and embedded into the binary (web\dist, see
|
||||
:: web\embed.go), so the executable serves the whole frontend on its own:
|
||||
:: neither frontend\ nor web\dist\ is needed where it runs.
|
||||
echo Building frontend...
|
||||
cd frontend
|
||||
call npm ci
|
||||
if errorlevel 1 goto :fail
|
||||
|
||||
:: node_modules is gitignored, so a fresh checkout (CI included) installs from
|
||||
:: the lockfile; a warm tree only rebuilds.
|
||||
if not exist "node_modules" (
|
||||
call npm ci
|
||||
if errorlevel 1 goto :frontend_failed
|
||||
)
|
||||
|
||||
call npm run build
|
||||
if errorlevel 1 goto :fail
|
||||
if errorlevel 1 goto :frontend_failed
|
||||
cd ..
|
||||
|
||||
:: go:embed on web\dist fails anyway, but this names the real problem.
|
||||
if exist "web\dist\index.html" goto :backend
|
||||
echo Frontend build produced no web\dist\index.html.
|
||||
goto :fail
|
||||
|
||||
:frontend_failed
|
||||
cd ..
|
||||
echo Frontend build failed.
|
||||
goto :fail
|
||||
|
||||
:backend
|
||||
echo Building for Linux (amd64)...
|
||||
|
||||
|
||||
+17
-14
@@ -1,27 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Builds the Linux (amd64) binary.
|
||||
# ./build-linux-x86_64.sh backend only
|
||||
# ./build-linux-x86_64.sh --frontend also build the Vue frontend
|
||||
#
|
||||
# The frontend is needed at runtime: web/ serves it from frontend/dist.
|
||||
# The Vue console is built first and embedded into the binary (web/dist, see
|
||||
# web/embed.go), so the executable serves the whole frontend on its own —
|
||||
# neither frontend/ nor web/dist/ is needed where it runs.
|
||||
set -euo pipefail
|
||||
|
||||
# Build from the repository root, however the script was invoked.
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
BUILD_FRONTEND=0
|
||||
if [ "${1:-}" = "--frontend" ]; then
|
||||
BUILD_FRONTEND=1
|
||||
fi
|
||||
|
||||
if [ "$BUILD_FRONTEND" = "1" ]; then
|
||||
echo "Building frontend..."
|
||||
(
|
||||
cd frontend
|
||||
echo "Building frontend..."
|
||||
(
|
||||
cd frontend
|
||||
# node_modules is gitignored, so a fresh checkout (CI included) installs
|
||||
# from the lockfile; a warm tree only rebuilds.
|
||||
if [ ! -d node_modules ]; then
|
||||
npm ci
|
||||
npm run build
|
||||
)
|
||||
fi
|
||||
npm run build
|
||||
)
|
||||
|
||||
# go:embed on web/dist fails anyway, but this names the real problem.
|
||||
if [ ! -f web/dist/index.html ]; then
|
||||
echo "Frontend build produced no web/dist/index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building for Linux (amd64)..."
|
||||
|
||||
+22
-10
@@ -4,21 +4,33 @@ setlocal enabledelayedexpansion
|
||||
:: Build from the repository root, however the script was invoked.
|
||||
cd /d "%~dp0"
|
||||
|
||||
:: Optional first argument: --frontend also builds the Vue frontend, which web/
|
||||
:: serves at runtime from frontend/dist. Omitted, only the backend is compiled.
|
||||
set BUILD_FRONTEND=0
|
||||
if /i "%~1"=="--frontend" set BUILD_FRONTEND=1
|
||||
|
||||
if not "%BUILD_FRONTEND%"=="1" goto :backend
|
||||
|
||||
:: The Vue console is built first and embedded into the binary (web\dist, see
|
||||
:: web\embed.go), so the executable serves the whole frontend on its own:
|
||||
:: neither frontend\ nor web\dist\ is needed where it runs.
|
||||
echo Building frontend...
|
||||
cd frontend
|
||||
call npm ci
|
||||
if errorlevel 1 goto :fail
|
||||
|
||||
:: node_modules is gitignored, so a fresh checkout (CI included) installs from
|
||||
:: the lockfile; a warm tree only rebuilds.
|
||||
if not exist "node_modules" (
|
||||
call npm ci
|
||||
if errorlevel 1 goto :frontend_failed
|
||||
)
|
||||
|
||||
call npm run build
|
||||
if errorlevel 1 goto :fail
|
||||
if errorlevel 1 goto :frontend_failed
|
||||
cd ..
|
||||
|
||||
:: go:embed on web\dist fails anyway, but this names the real problem.
|
||||
if exist "web\dist\index.html" goto :backend
|
||||
echo Frontend build produced no web\dist\index.html.
|
||||
goto :fail
|
||||
|
||||
:frontend_failed
|
||||
cd ..
|
||||
echo Frontend build failed.
|
||||
goto :fail
|
||||
|
||||
:backend
|
||||
echo Building for Windows (amd64)...
|
||||
|
||||
|
||||
+17
-14
@@ -1,27 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Builds the Windows (amd64) binary.
|
||||
# ./build-win-x86_64.sh backend only
|
||||
# ./build-win-x86_64.sh --frontend also build the Vue frontend
|
||||
#
|
||||
# The frontend is needed at runtime: web/ serves it from frontend/dist.
|
||||
# The Vue console is built first and embedded into the binary (web/dist, see
|
||||
# web/embed.go), so the executable serves the whole frontend on its own —
|
||||
# neither frontend/ nor web/dist/ is needed where it runs.
|
||||
set -euo pipefail
|
||||
|
||||
# Build from the repository root, however the script was invoked.
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
BUILD_FRONTEND=0
|
||||
if [ "${1:-}" = "--frontend" ]; then
|
||||
BUILD_FRONTEND=1
|
||||
fi
|
||||
|
||||
if [ "$BUILD_FRONTEND" = "1" ]; then
|
||||
echo "Building frontend..."
|
||||
(
|
||||
cd frontend
|
||||
echo "Building frontend..."
|
||||
(
|
||||
cd frontend
|
||||
# node_modules is gitignored, so a fresh checkout (CI included) installs
|
||||
# from the lockfile; a warm tree only rebuilds.
|
||||
if [ ! -d node_modules ]; then
|
||||
npm ci
|
||||
npm run build
|
||||
)
|
||||
fi
|
||||
npm run build
|
||||
)
|
||||
|
||||
# go:embed on web/dist fails anyway, but this names the real problem.
|
||||
if [ ! -f web/dist/index.html ]; then
|
||||
echo "Frontend build produced no web/dist/index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building for Windows (amd64)..."
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
## Ver.0.1.2.4
|
||||
### Bug Fixes
|
||||
- [Integrate frontend build into backend binary]("")
|
||||
|
||||
## Ver.0.1.2.3-1c4ad61.pre-release
|
||||
### Features
|
||||
- [Add frontend webpage to make everything easy to control]("https://gitea.nanami.tech/NanamiAdmin/Nukumizu/commit/a90b4f5497dfae5f9b6a3132d091a528fdbdf612")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.local
|
||||
.DS_Store
|
||||
|
||||
+3
-3
@@ -24,14 +24,14 @@ The dev server proxies `/api` (and the log websocket) to the backend. By default
|
||||
$env:NUKUMIZU_API = "http://192.168.20.4:8080"; npm run dev
|
||||
```
|
||||
|
||||
Production build (static assets only):
|
||||
Production build:
|
||||
|
||||
```bash
|
||||
npm run build # outputs dist/
|
||||
npm run build # outputs ../web/dist
|
||||
npm run preview
|
||||
```
|
||||
|
||||
The backend does not serve static files, so put `dist/` behind any static server and proxy `/api` (and `ws://…/api/system/getLogs`) to the Nukumizu backend.
|
||||
Vite writes to `../web/dist` rather than `frontend/dist` (see `build.outDir` in `vite.config.js`) because the Go backend embeds that directory into the binary — `go:embed` cannot reach outside the package it sits in, so the output has to live under `web/`. The repo's `build-*` scripts run this build for you and compile the backend afterwards; `npm run build` alone does not change what an already-built binary serves.
|
||||
|
||||
## API contract notes
|
||||
|
||||
|
||||
@@ -6,6 +6,16 @@ const BACKEND = process.env.NUKUMIZU_API || 'http://127.0.0.1:8080';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
// web/embed.go embeds this directory into the Go binary. It has to live
|
||||
// inside the web package: go:embed cannot reach outside its own
|
||||
// directory, so ../web/dist is as close as it gets.
|
||||
outDir: '../web/dist',
|
||||
// The directory is outside the project root, so Vite refuses to empty
|
||||
// it unless told to. Without this, stale hashed assets from earlier
|
||||
// builds pile up in the binary.
|
||||
emptyOutDir: true
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: Get git commit hash (shortened to 7 characters, can also use full)
|
||||
for /f %%i in ('git rev-parse --short HEAD') do set COMMIT=%%i
|
||||
|
||||
:: Get UTC time
|
||||
for /f %%i in ('powershell -Command "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'"') do set BUILD_DATE=%%i
|
||||
|
||||
:: Build -ldflags
|
||||
set LDFLAGS=-X main.BuildTime=%BUILD_DATE% -X main.CommitHash=%COMMIT%
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: The console is embedded in the binary (web\embed.go), so compiling without
|
||||
:: web\dist fails. Say that plainly rather than leaving go:embed's error.
|
||||
if not exist "web\dist\index.html" (
|
||||
echo The web console is not built: web\dist is missing.
|
||||
echo Run build-win-x86_64.bat once, or "npm run build" in frontend\.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Get git commit hash (shortened to 7 characters, can also use full)
|
||||
for /f %%i in ('git rev-parse --short HEAD') do set COMMIT=%%i
|
||||
|
||||
:: Get UTC time
|
||||
for /f %%i in ('powershell -Command "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'"') do set BUILD_DATE=%%i
|
||||
|
||||
:: Build -ldflags
|
||||
set LDFLAGS=-X main.BuildTime=%BUILD_DATE% -X main.CommitHash=%COMMIT%
|
||||
go run -ldflags "%LDFLAGS%" .
|
||||
+24
-12
@@ -1,23 +1,35 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// StaticFiles is the built frontend, rooted at the directory Vite writes to
|
||||
// (frontend/dist), so paths inside it are relative to that directory, e.g.
|
||||
// "index.html" or "assets/app.js". The path is relative to the working
|
||||
// directory, so the server is expected to run from the repository root.
|
||||
// distFS holds the built console. Vite writes it to web/dist (see the outDir
|
||||
// in frontend/vite.config.js) and it is compiled into the binary here, so a
|
||||
// running executable serves the whole frontend on its own: neither the
|
||||
// frontend sources nor web/dist need to exist on the machine that runs it.
|
||||
//
|
||||
// The all: prefix also picks up files whose names start with "_" or ".", which
|
||||
// the default pattern skips.
|
||||
//
|
||||
//go:embed all:dist
|
||||
var distFS embed.FS
|
||||
|
||||
// StaticFiles is the built frontend, rooted at the directory Vite writes to,
|
||||
// so paths inside it are relative to that directory, e.g. "index.html" or
|
||||
// "assets/app.js".
|
||||
//
|
||||
// web/dist is a build artifact and is not in a fresh checkout, so the console
|
||||
// has to be built before the backend compiles — any build-*.sh / build-*.bat
|
||||
// does it first, or run `npm run build` in frontend/ yourself. Compiling
|
||||
// without it fails with "pattern all:dist: no matching files found".
|
||||
var StaticFiles fs.FS
|
||||
|
||||
func init() {
|
||||
dir := filepath.Join("frontend", "dist")
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
StaticFiles = os.DirFS(dir)
|
||||
return
|
||||
sub, err := fs.Sub(distFS, "dist")
|
||||
if err != nil {
|
||||
panic("web: embedded frontend is unreadable: " + err.Error())
|
||||
}
|
||||
|
||||
StaticFiles = os.DirFS(".")
|
||||
StaticFiles = sub
|
||||
}
|
||||
|
||||
+7
-8
@@ -7,13 +7,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var staticFS http.FileSystem
|
||||
|
||||
// This init runs after embed.go's, which sets StaticFiles (Go initializes a
|
||||
// package's files in lexical file-name order). StaticFiles is already rooted
|
||||
// at frontend/dist, so it is served as-is — no further fs.Sub is needed.
|
||||
func init() {
|
||||
staticFS = http.FS(StaticFiles)
|
||||
// staticFS wraps the embedded frontend for net/http. StaticFiles is already
|
||||
// rooted at the directory Vite writes to, so it is served as-is — no further
|
||||
// fs.Sub is needed. http.FS copies a file that is not an io.Seeker into memory
|
||||
// before serving it, which keeps this working whatever fs.FS StaticFiles is.
|
||||
func staticFS() http.FileSystem {
|
||||
return http.FS(StaticFiles)
|
||||
}
|
||||
|
||||
func ServeStatic(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -25,7 +24,7 @@ func ServeStatic(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
filePath := strings.TrimPrefix(urlPath, "/")
|
||||
f, err := staticFS.Open(filePath)
|
||||
f, err := staticFS().Open(filePath)
|
||||
if err != nil {
|
||||
serveIndexHTML(w)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user