Merge pull request 'feat: web UI scaffold with embedded SPA and multi-stage Docker build' (#16) from dev into main
This commit was merged in pull request #16.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
name: test
|
name: test
|
||||||
|
|
||||||
# Lint + short unit tests only. Integration tests needing Postgres/ffmpeg
|
# Lint + short unit tests. Integration tests needing Postgres/ffmpeg run
|
||||||
# run locally via docker-compose; they should guard with testing.Short().
|
# locally via docker-compose; they should guard with testing.Short().
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -17,6 +17,25 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
|
||||||
|
- name: Install web deps
|
||||||
|
working-directory: web
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Web build (populates web/build/ for go:embed)
|
||||||
|
working-directory: web
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Web test (vitest)
|
||||||
|
working-directory: web
|
||||||
|
run: npm test
|
||||||
|
|
||||||
- name: Detect Go project
|
- name: Detect Go project
|
||||||
id: gomod
|
id: gomod
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
# syntax=docker/dockerfile:1.6
|
# syntax=docker/dockerfile:1.6
|
||||||
|
|
||||||
|
FROM node:22-bookworm-slim AS web
|
||||||
|
WORKDIR /web
|
||||||
|
COPY web/package.json web/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY web/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
FROM golang:1.23-bookworm AS builder
|
FROM golang:1.23-bookworm AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
|
# Overwrite the committed placeholder with the freshly-built SPA assets.
|
||||||
|
COPY --from=web /web/build ./web/build
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
RUN go build -trimpath -ldflags="-s -w" -o /out/minstrel ./cmd/minstrel
|
RUN go build -trimpath -ldflags="-s -w" -o /out/minstrel ./cmd/minstrel
|
||||||
|
|
||||||
|
|||||||
@@ -25,3 +25,20 @@ Forgejo Actions handles:
|
|||||||
- Container image build + push to the Forgejo registry on merge to `main` (`:main`) and on `v*` tags (`:<version>`).
|
- Container image build + push to the Forgejo registry on merge to `main` (`:main`) and on `v*` tags (`:<version>`).
|
||||||
|
|
||||||
Task and milestone tracking: Fable (`Minstrel` project, id 12).
|
Task and milestone tracking: Fable (`Minstrel` project, id 12).
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Minstrel has two concurrent dev processes:
|
||||||
|
|
||||||
|
1. **Backend:** `docker compose up` — starts Postgres and Minstrel on `:4533`.
|
||||||
|
2. **Frontend:** `cd web && npm install && npm run dev` — Vite dev server on `:5173` with HMR.
|
||||||
|
|
||||||
|
The Vite dev server proxies `/api/*` and `/rest/*` to the backend on `:4533`,
|
||||||
|
so session cookies work correctly when the SPA is loaded from the Vite origin.
|
||||||
|
|
||||||
|
### Production build
|
||||||
|
|
||||||
|
`docker build -t minstrel .` runs the SvelteKit build inside a `node` stage,
|
||||||
|
copies the output into the `golang` stage, and `//go:embed`s it into the
|
||||||
|
final binary. The resulting container serves the SPA from `/` alongside the
|
||||||
|
API surfaces. No separate static-file server is required.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
||||||
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
||||||
"git.fabledsword.com/bvandeusen/minstrel/internal/subsonic"
|
"git.fabledsword.com/bvandeusen/minstrel/internal/subsonic"
|
||||||
|
"git.fabledsword.com/bvandeusen/minstrel/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ScanTrigger is the subset of the scanner the HTTP handler needs. Kept as an
|
// ScanTrigger is the subset of the scanner the HTTP handler needs. Kept as an
|
||||||
@@ -50,6 +52,18 @@ func (s *Server) Router() http.Handler {
|
|||||||
})
|
})
|
||||||
subsonic.Mount(r, s.Pool, s.Logger, s.SubsonicCfg)
|
subsonic.Mount(r, s.Pool, s.Logger, s.SubsonicCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spa := web.Handler()
|
||||||
|
r.NotFound(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
p := req.URL.Path
|
||||||
|
if strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/rest/") {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
_, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"not found"}}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spa.ServeHTTP(w, req)
|
||||||
|
})
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.fabledsword.com/bvandeusen/minstrel/internal/subsonic"
|
"git.fabledsword.com/bvandeusen/minstrel/internal/subsonic"
|
||||||
@@ -33,3 +34,79 @@ func TestHealthz(t *testing.T) {
|
|||||||
t.Errorf("body = %v, want status=ok", body)
|
t.Errorf("body = %v, want status=ok", body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouter_ServesSPAAtRoot(t *testing.T) {
|
||||||
|
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{})
|
||||||
|
ts := httptest.NewServer(s.Router())
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
resp, err := http.Get(ts.URL + "/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GET /: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||||
|
t.Errorf("Content-Type = %q, want text/html*", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouter_DeepLinkFallbackReturnsSPA(t *testing.T) {
|
||||||
|
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{})
|
||||||
|
ts := httptest.NewServer(s.Router())
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
resp, err := http.Get(ts.URL + "/artists/00000000-0000-0000-0000-000000000001")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GET deep link: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("status = %d, want 200", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||||
|
t.Errorf("Content-Type = %q, want text/html*", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouter_APIPathNotSwallowedBySPA(t *testing.T) {
|
||||||
|
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{})
|
||||||
|
ts := httptest.NewServer(s.Router())
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
resp, err := http.Get(ts.URL + "/api/does-not-exist")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GET /api/does-not-exist: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusNotFound {
|
||||||
|
t.Errorf("status = %d, want 404", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if ct := resp.Header.Get("Content-Type"); strings.Contains(ct, "text/html") {
|
||||||
|
t.Errorf("Content-Type = %q; /api/* must never return text/html", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouter_RestPathNotSwallowedBySPA(t *testing.T) {
|
||||||
|
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{})
|
||||||
|
ts := httptest.NewServer(s.Router())
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
resp, err := http.Get(ts.URL + "/rest/does-not-exist")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GET /rest/does-not-exist: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusNotFound {
|
||||||
|
t.Errorf("status = %d, want 404", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if ct := resp.Header.Get("Content-Type"); strings.Contains(ct, "text/html") {
|
||||||
|
t.Errorf("Content-Type = %q; /rest/* must never return text/html", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
.svelte-kit/
|
||||||
|
build/*
|
||||||
|
!build/index.html
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Minstrel</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
Minstrel SPA has not been built. Run
|
||||||
|
<code>cd web && npm install && npm run build</code> or
|
||||||
|
build the container image, which runs the web build during
|
||||||
|
<code>docker build</code>.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Package web embeds the SvelteKit SPA build output and serves it over HTTP.
|
||||||
|
// Requests for real files under build/ are served verbatim; anything else —
|
||||||
|
// deep links like /artists/abc, missing asset paths — returns build/index.html
|
||||||
|
// so the SPA can resolve the route client-side.
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:build
|
||||||
|
var buildFS embed.FS
|
||||||
|
|
||||||
|
// Handler returns an http.Handler that serves the embedded SPA.
|
||||||
|
//
|
||||||
|
// Behavior:
|
||||||
|
// - GET / -> build/index.html
|
||||||
|
// - GET /<path that exists in build/> -> that file, via http.FileServer
|
||||||
|
// - GET /<anything else> -> build/index.html (SPA fallback)
|
||||||
|
//
|
||||||
|
// Callers are expected to register this as the router's NotFound/catch-all so
|
||||||
|
// explicitly-routed paths (like /api/* and /rest/*) take precedence.
|
||||||
|
func Handler() http.Handler {
|
||||||
|
sub, err := fs.Sub(buildFS, "build")
|
||||||
|
if err != nil {
|
||||||
|
// fs.Sub on a valid embed path can only fail if the embed directive
|
||||||
|
// is malformed, which the compile would have caught.
|
||||||
|
panic("web: fs.Sub(build) failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
index, err := fs.ReadFile(sub, "index.html")
|
||||||
|
if err != nil {
|
||||||
|
panic("web: embedded build/index.html missing: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fileServer := http.FileServer(http.FS(sub))
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clean := path.Clean(r.URL.Path)
|
||||||
|
if clean == "/" || clean == "." {
|
||||||
|
serveIndex(w, index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := strings.TrimPrefix(clean, "/")
|
||||||
|
if info, err := fs.Stat(sub, name); err == nil && !info.IsDir() {
|
||||||
|
fileServer.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveIndex(w, index)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveIndex(w http.ResponseWriter, index []byte) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
_, _ = w.Write(index)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_ServesIndexAtRoot(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
Handler().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", rec.Code)
|
||||||
|
}
|
||||||
|
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||||
|
t.Errorf("Content-Type = %q, want text/html*", ct)
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(rec.Body)
|
||||||
|
if !strings.Contains(strings.ToLower(string(body)), "<html") {
|
||||||
|
t.Errorf("body missing <html tag; got: %q", string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_UnknownPathFallsBackToIndex(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/artists/some-uuid", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
Handler().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200 (SPA fallback)", rec.Code)
|
||||||
|
}
|
||||||
|
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||||
|
t.Errorf("Content-Type = %q, want text/html* for SPA fallback", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandler_MissingAssetFallsBackToIndex(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/_app/immutable/chunks/does-not-exist.js", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
Handler().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200 (fallback to index.html)", rec.Code)
|
||||||
|
}
|
||||||
|
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||||
|
t.Errorf("Content-Type = %q, want text/html* for missing asset", ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+3798
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "minstrel-web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"test": "svelte-kit sync && vitest run"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
|
"@sveltejs/kit": "^2.8.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"svelte": "^5.1.9",
|
||||||
|
"svelte-check": "^4.0.5",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"tslib": "^2.8.0",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vitest": "^2.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Minstrel</title>
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body class="bg-surface-900 text-text-primary">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('scaffold smoke', () => {
|
||||||
|
it('runs vitest', () => {
|
||||||
|
expect(1 + 1).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import '../app.css';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<main class="flex h-screen items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-4xl font-semibold">Minstrel</h1>
|
||||||
|
<p class="mt-2 text-text-secondary">
|
||||||
|
Scaffold — UI features land in subsequent plans.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
@@ -0,0 +1,18 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
pages: 'build',
|
||||||
|
assets: 'build',
|
||||||
|
fallback: 'index.html',
|
||||||
|
precompress: false,
|
||||||
|
strict: false
|
||||||
|
}),
|
||||||
|
files: {
|
||||||
|
assets: 'static'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
content: ['./src/**/*.{html,js,ts,svelte}'],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
surface: {
|
||||||
|
900: '#14161a',
|
||||||
|
800: '#1a1d22',
|
||||||
|
700: '#2a2f36'
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#e8ecf2',
|
||||||
|
secondary: '#cdd3db'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:4533',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/rest': {
|
||||||
|
target: 'http://localhost:4533',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['src/**/*.test.ts']
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user