3 Commits
15 changed files with 98 additions and 119 deletions

No files matched your search

+1 -3
View File
@@ -1,8 +1,6 @@
# frontend-svelte
Copier template for a SvelteKit frontend, either as a `frontend/` part next to other parts or alone at the root of the project (`layout`): SvelteKit on Bun with Tailwind v4, ultracite (biome) or ESLint + Prettier (`linter`) and optional shadcn-svelte, served by a Bun server with SSR, as a static SPA behind `serve.ts`, or as a static SPA that the backend api serves from `frontend/build`.
Ships `frontend/docker-compose.yml` with root-relative paths and profiles per service, `docker-compose.override.yml.example` (the `caddy` network aliases, copied by `make env`), a `Makefile` with `fmt`, `check`, `build` and `dev`, `.env.example`, `CLAUDE.md`, `.claude/rules` for Svelte (and ultracite with biome). The image is built in stages: `.env` is in the build context, since `$env/static/*` are inlined at build time, and stays in the builder stage; the runtime image gets `build/` and production dependencies only. The Svelte MCP server comes from `infra`. Applied on top of the `infra` template, `layout: parts` or `layout: single` respectively.
Copier template for a SvelteKit frontend on Bun with Tailwind v4, biome or ESLint + Prettier and optional shadcn-svelte: SSR on Bun, a static SPA behind `serve.ts`, or a static SPA served by the backend api. A `frontend/` part or the whole project (`layout`), on top of `infra`.
```sh
copier copy --trust -d project_name="My Thing" -d serve=bun <this-repo> <dest>
+3 -3
View File
@@ -66,6 +66,6 @@ prefix:
default: "{{ 'frontend/' if layout == 'part' else '' }}"
_tasks:
# prettier-plugin-tailwindcss and biome sort classes differently, the
# sources follow biome: an eslint project is formatted once, right away.
- "{% if _copier_operation == 'copy' and install_deps %}sh -c 'cd {{ part_dir }} && bun install{% if linter == 'eslint' %} && bun run fix{% endif %}'{% else %}true{% endif %}"
# Generated values (project name) can push lines past the formatter width,
# so every fresh copy is formatted once by its own linter.
- "{% if _copier_operation == 'copy' and install_deps %}sh -c 'cd {{ part_dir }} && bun install && bun --bun run fix'{% else %}true{% endif %}"
+1 -16
View File
@@ -1,9 +1,6 @@
COMPOSE := {% if layout == 'part' %}cd .. && {% endif %}docker compose
{% if serve != 'api' %}IMAGE := {{ project_slug | replace('_', '-') }}/frontend
{% endif %}changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null
stamp = { git update-ref $(1) HEAD 2>/dev/null || true; }
.PHONY: {% if layout == 'root' %}env recreate rebuild restart logs down deploy {% endif %}fmt check build pre-deploy post-deploy dev
.PHONY: {% if layout == 'root' %}env recreate rebuild restart logs down {% endif %}fmt check build dev
fmt:
bun run fix
@@ -16,14 +13,6 @@ build:
{% if serve == 'api' %} $(COMPOSE) run --rm frontend-dev sh -c "bun install && bun run build"
{% else %} $(COMPOSE) build frontend
{% endif %}
pre-deploy:
@if $(call changed,refs/deploy/frontend,.) || {% if serve == 'api' %}! test -d build{% else %}! docker image inspect $(IMAGE) >/dev/null 2>&1{% endif %}; then \
$(MAKE) build && $(call stamp,refs/deploy/frontend); \
fi
post-deploy:
@:
dev:
bun run dev
{% if layout == 'root' %}
@@ -52,8 +41,4 @@ logs:
down:
$(COMPOSE) down
deploy: pre-deploy
$(COMPOSE) up -d
$(MAKE) post-deploy
{% endif %}
+10 -6
View File
@@ -3,6 +3,10 @@
"version": "0.0.1",
"private": true,
"type": "module",
"imports": {
"#lib": "./src/lib/index.js",
"#lib/*": "./src/lib/*"
},
"scripts": {
"build": "vite build",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -20,19 +24,19 @@
},
"devDependencies": {
{%- set deps = {
"@sveltejs/kit": "^2.63.0",
"@sveltejs/kit": "^3.0.1",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tailwindcss/vite": "^4.3.0",
"@types/bun": "^1.3.0",
"svelte": "^5.56.1",
"svelte-check": "^4.6.0",
"svelte": "^5.57.1",
"svelte-check": "^4.7.6",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3",
"vite": "^8.0.16",
} -%}
{%- if linter == 'biome' %}{% set _ = deps.update({
"@biomejs/biome": "2.5.13",
"ultracite": "7.12.0",
"@biomejs/biome": "^2.5.15",
"ultracite": "^7.12.4",
}) %}{% else %}{% set _ = deps.update({
"@eslint/compat": "^2.1.1",
"@eslint/js": "^10.0.1",
@@ -45,7 +49,7 @@
"prettier-plugin-tailwindcss": "^0.8.1",
"typescript-eslint": "^8.71.1",
}) %}{% endif -%}
{%- if frontend_adapter == 'bun' %}{% set _ = deps.update({"svelte-adapter-bun": "^1.0.1"}) %}{% else %}{% set _ = deps.update({"@sveltejs/adapter-static": "^3.0.10"}) %}{% endif -%}
{%- if frontend_adapter == 'bun' %}{% set _ = deps.update({"@sveltejs/adapter-bun": "^1.0.0"}) %}{% else %}{% set _ = deps.update({"@sveltejs/adapter-static": "^4.0.0"}) %}{% endif -%}
{%- if ui_library == 'shadcn' %}{% set _ = deps.update({
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^1.23.0",
+5
View File
@@ -0,0 +1,5 @@
import { defineEnvVars } from "@sveltejs/kit/env";
export const variables = defineEnvVars({
PUBLIC_API_BASE_URL: { public: true, schema: (input) => input ?? "/api" },
});
@@ -1,6 +1,6 @@
import { env } from "$env/dynamic/public";
import { PUBLIC_API_BASE_URL } from "$app/env/public";
const BASE = env.PUBLIC_API_BASE_URL ?? "/api";
const BASE = PUBLIC_API_BASE_URL;
export class ApiError extends Error {
status: number;
@@ -1,3 +1,3 @@
// place files you want to import through the `$lib` alias in this folder.
// place files you want to import through the `#lib` alias in this folder.
{% if linter == 'biome' %}// biome-ignore-all lint/suspicious/noEmptySource: temporary
{% endif %}
@@ -1,36 +0,0 @@
{% if frontend_adapter == 'bun' -%}
import adapter from "svelte-adapter-bun";
/** @type {import('@sveltejs/kit').Config} */
const config = {
compilerOptions: {
runes: ({ filename }) =>
{% if linter == 'biome' %} // biome-ignore lint/performance/useTopLevelRegex: svelte default behaviour
{% endif %} filename.split(/[/\\]/).includes("node_modules") ? undefined : true,
},
kit: {
adapter: adapter(),
},
};
export default config;
{%- else -%}
import adapter from "@sveltejs/adapter-static";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
assets: "build",
fallback: "index.html",
pages: "build",
precompress: false,
strict: false,
}),
},
preprocess: vitePreprocess(),
};
export default config;
{%- endif %}
+4 -15
View File
@@ -1,20 +1,9 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"extends": "$app/tsconfig",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
"strict": true
},
"include": ["src", "vite.config.ts"]
}
-20
View File
@@ -1,20 +0,0 @@
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
const allowedHosts = process.env.ALLOWED_HOSTS
? process.env.ALLOWED_HOSTS.split(",")
: undefined;
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
server: {
allowedHosts,
proxy: {
"/api": {
changeOrigin: true,
target: process.env.API_PROXY_TARGET ?? "http://localhost:8080",
},
},
},
});
@@ -0,0 +1,51 @@
{% if frontend_adapter == 'bun' -%}
import adapter from "@sveltejs/adapter-bun";
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
const PATH_SEP = /[/\\]/;
{%- else -%}
import adapter from "@sveltejs/adapter-static";
import { sveltekit } from "@sveltejs/kit/vite";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
{%- endif %}
const allowedHosts = process.env.ALLOWED_HOSTS
? process.env.ALLOWED_HOSTS.split(",")
: undefined;
export default defineConfig({
plugins: [
tailwindcss(),
sveltekit({
{%- if frontend_adapter == 'bun' %}
adapter: adapter(),
compilerOptions: {
runes: ({ filename }) =>
filename.split(PATH_SEP).includes("node_modules") ? undefined : true,
},
{%- else %}
adapter: adapter({
assets: "build",
fallback: "index.html",
pages: "build",
precompress: false,
strict: false,
}),
preprocess: [vitePreprocess()],
{%- endif %}
}),
],
server: {
allowedHosts,
proxy: {
"/api": {
changeOrigin: true,
target: process.env.API_PROXY_TARGET ?? "http://localhost:8080",
},
},
},
});
@@ -1,16 +1,8 @@
# {{ project_name }}
Runs in Docker, locally the same way as in production. `COMPOSE_PROFILES` in `.env` selects the services that run on this machine. `docker-compose.override.yml` holds what differs between machines: published ports and the aliases on the external `caddy` network.
```sh
make env
make recreate
make env # .env and docker-compose.override.yml from the examples
make recreate # the services in COMPOSE_PROFILES
make dev # vite on the host
make check
```
`make env` creates `.env` and `docker-compose.override.yml` from their examples. `make recreate` starts the selected profiles, `make rebuild` builds the image first, `make logs`, `make restart` and `make down` do what they say.
To deploy, `git pull && make deploy`. It runs `make pre-deploy`, which rebuilds the image only when something in the repository changed, then `docker compose up -d`. A deploy system with its own `up` step calls `make pre-deploy` before it and `make post-deploy` after. What was built is remembered as `refs/deploy/*` in the clone.
In the fleet, production is deployed from `infra/komodo` instead (git mode of Komodo, see `docs/architecture.md` there): Komodo clones this repo, and a layer plus `deploy.sh` in the stack's directory tag images by the hash of what they bake in and write `.env` from sops. The repo only has to stay a plain recipe: images built from `build:` in compose, `caddy/site.caddy`. `make deploy` and `refs/deploy/*` are for a checkout deployed by hand.
`make dev` runs the Vite dev server on the host, `make fmt` formats, `make check` runs svelte-check and the linter.
@@ -21,6 +21,7 @@
"includes": ["**/*.svelte"],
"linter": {
"rules": {
"a11y": { "useAnchorContent": "off" },
"style": {
"useFilenamingConvention": {
"level": "error",
@@ -6,10 +6,20 @@ import svelte from "eslint-plugin-svelte";
import { defineConfig } from "eslint/config";
import globals from "globals";
import ts from "typescript-eslint";
import svelteConfig from "./svelte.config.js";
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
{% if frontend_adapter == 'bun' -%}
const svelteConfig = {
compilerOptions: {
runes: ({ filename }) =>
filename.split(/[/\\]/).includes("node_modules") ? undefined : true,
},
};
{%- else -%}
const svelteConfig = {};
{%- endif %}
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
@@ -1,5 +1,5 @@
{% if frontend_adapter == 'bun' -%}
FROM oven/bun:alpine AS deps
FROM oven/bun:1-alpine AS deps
ENV TERM=xterm-256color
ENV COLORTERM=truecolor
@@ -22,7 +22,7 @@ RUN bun --bun svelte-kit sync
RUN bun --bun run build
FROM oven/bun:alpine AS runtime
FROM oven/bun:1-alpine AS runtime
ENV NODE_ENV=production
# The cache mount only holds files; without the variable bun caches into
@@ -42,7 +42,7 @@ EXPOSE 3000
ENTRYPOINT ["bun"]
CMD ["build/index.js"]
{%- else -%}
FROM oven/bun:alpine AS deps
FROM oven/bun:1-alpine AS deps
ENV TERM=xterm-256color
ENV COLORTERM=truecolor
@@ -63,7 +63,7 @@ RUN bun --bun svelte-kit sync
RUN bun run build
FROM oven/bun:alpine AS runtime
FROM oven/bun:1-alpine AS runtime
ENV NODE_ENV=production