chore(tooling): add vitest + svelte-check, drop yarn.lock, CI gates, typed env imports
Toolchain hygiene:
- svelte-check is now a pinned devDep (was npx-fetched per CI run)
- vitest + coverage as devDeps; vitest.config.ts is separate from
vite.config.ts so vite build does not pull vitest
- $env stub for vitest (alias resolves $env/dynamic/{public,private}
to a process.env proxy) — keeps server modules testable without
booting SvelteKit
- npm scripts: check / check:watch / typecheck / test / test:watch
- drop yarn.lock + packageManager field; CI uses npm ci, single source
of truth is package-lock.json
- CI runs generate:icons + svelte-check + tsc --noEmit before deploy
Switch from raw process.env.X to $env/dynamic/{public,private} in
file-cache, image-cache, transform-cache and rusty-image.server. The
typed imports get tree-shaken correctly per server/client and fail
loudly at build time on accidental client-side use of private vars.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -32,10 +32,14 @@ jobs:
|
|||||||
cache: npm
|
cache: npm
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
- name: Generate icons subset
|
||||||
|
run: npm run generate:icons
|
||||||
- name: SvelteKit sync
|
- name: SvelteKit sync
|
||||||
run: npx svelte-kit sync
|
run: npx svelte-kit sync
|
||||||
- name: Typecheck
|
- name: Typecheck (svelte-check)
|
||||||
run: npx svelte-check --threshold error
|
run: npx svelte-check --threshold error
|
||||||
|
- name: Typecheck (tsc)
|
||||||
|
run: npx tsc --noEmit
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: verify
|
needs: verify
|
||||||
|
|||||||
Generated
+2371
-6
File diff suppressed because it is too large
Load Diff
+16
-3
@@ -4,10 +4,19 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
"predev": "node scripts/generate-icons.mjs",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"prebuild": "node scripts/generate-icons.mjs",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "node build",
|
"start": "node build",
|
||||||
"generate:api-types": "node scripts/generate-api-types.mjs",
|
"generate:api-types": "node scripts/generate-api-types.mjs",
|
||||||
|
"generate:icons": "node scripts/generate-icons.mjs",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"typecheck": "svelte-kit sync && tsc --noEmit",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
"prepare": "svelte-kit sync || true"
|
"prepare": "svelte-kit sync || true"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -17,11 +26,15 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
|
"@types/sanitize-html": "^2.13.0",
|
||||||
|
"@vitest/coverage-v8": "^2.1.9",
|
||||||
"openapi-typescript": "^7.4.0",
|
"openapi-typescript": "^7.4.0",
|
||||||
"svelte": "^5.28.2",
|
"svelte": "^5.28.2",
|
||||||
|
"svelte-check": "^4.1.4",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vitest": "^2.1.9"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/lora": "^5.2.8",
|
"@fontsource-variable/lora": "^5.2.8",
|
||||||
@@ -31,8 +44,8 @@
|
|||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"marked": "^17.0.2",
|
"marked": "^17.0.2",
|
||||||
|
"sanitize-html": "^2.13.1",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module"
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Stub für `$env/dynamic/{public,private}`. Wird ausschließlich von Vitest
|
||||||
|
* via Alias aufgelöst (siehe `vitest.config.ts`). Im Produktivbuild verwendet
|
||||||
|
* SvelteKit das echte Modul.
|
||||||
|
*
|
||||||
|
* Tests können `process.env` direkt setzen — Proxy reicht alle Reads
|
||||||
|
* unverändert durch.
|
||||||
|
*/
|
||||||
|
export const env = new Proxy({} as Record<string, string | undefined>, {
|
||||||
|
get(_target, prop) {
|
||||||
|
if (typeof prop !== 'string') return undefined;
|
||||||
|
return process.env[prop];
|
||||||
|
},
|
||||||
|
has(_target, prop) {
|
||||||
|
if (typeof prop !== 'string') return false;
|
||||||
|
return prop in process.env;
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -8,9 +8,10 @@ import { createHash } from 'node:crypto';
|
|||||||
import { existsSync, mkdirSync } from 'node:fs';
|
import { existsSync, mkdirSync } from 'node:fs';
|
||||||
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
||||||
import { extname, join } from 'node:path';
|
import { extname, join } from 'node:path';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
const CACHE_DIR = process.env.FILE_CACHE_DIR?.trim() || '.cache/files';
|
const CACHE_DIR = env.FILE_CACHE_DIR?.trim() || '.cache/files';
|
||||||
const MAX_MEMORY_ENTRIES = Number(process.env.FILE_CACHE_MAX_MEMORY) || 50;
|
const MAX_MEMORY_ENTRIES = Number(env.FILE_CACHE_MAX_MEMORY) || 50;
|
||||||
|
|
||||||
export interface CachedFile {
|
export interface CachedFile {
|
||||||
buffer: Buffer;
|
buffer: Buffer;
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import { createHash } from 'node:crypto';
|
|||||||
import { existsSync, mkdirSync } from 'node:fs';
|
import { existsSync, mkdirSync } from 'node:fs';
|
||||||
import { readFile, readdir, unlink, writeFile } from 'node:fs/promises';
|
import { readFile, readdir, unlink, writeFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
|
||||||
const CACHE_DIR = process.env.IMAGE_CACHE_DIR?.trim() || '.cache/images';
|
const CACHE_DIR = env.IMAGE_CACHE_DIR?.trim() || '.cache/images';
|
||||||
const MAX_MEMORY_ENTRIES = Number(process.env.IMAGE_CACHE_MAX_MEMORY) || 200;
|
const MAX_MEMORY_ENTRIES = Number(env.IMAGE_CACHE_MAX_MEMORY) || 200;
|
||||||
|
|
||||||
export interface CachedImage {
|
export interface CachedImage {
|
||||||
buffer: Buffer;
|
buffer: Buffer;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Server-only Bild-Helfer: Asset-Meta vom CMS, Blurhash-LQIP, Responsive-Srcset.
|
* Server-only Bild-Helfer: Asset-Meta vom CMS, Blurhash-LQIP, Responsive-Srcset.
|
||||||
* Nur in `+page.server.ts` / `+layout.server.ts` importieren — nutzt sharp + blurhash + process.env.
|
* Nur in `+page.server.ts` / `+layout.server.ts` importieren — nutzt sharp + blurhash.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
@@ -11,10 +11,7 @@ import {
|
|||||||
} from './rusty-image';
|
} from './rusty-image';
|
||||||
|
|
||||||
function getCmsBaseUrl(): string {
|
function getCmsBaseUrl(): string {
|
||||||
return (env.PUBLIC_CMS_URL || process.env.PUBLIC_CMS_URL || 'http://localhost:3000').replace(
|
return (env.PUBLIC_CMS_URL || 'http://localhost:3000').replace(/\/$/, '');
|
||||||
/\/$/,
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssetMeta {
|
export interface AssetMeta {
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
* - Optional Datei-Cache: TRANSFORM_CACHE_DIR setzen (z. B. .cache/transform).
|
* - Optional Datei-Cache: TRANSFORM_CACHE_DIR setzen (z. B. .cache/transform).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const MAX_ENTRIES = Number(process.env.TRANSFORM_CACHE_MAX_ENTRIES) || 100;
|
import { env } from '$env/dynamic/private';
|
||||||
const CACHE_DIR = process.env.TRANSFORM_CACHE_DIR?.trim() || '';
|
|
||||||
|
const MAX_ENTRIES = Number(env.TRANSFORM_CACHE_MAX_ENTRIES) || 100;
|
||||||
|
const CACHE_DIR = env.TRANSFORM_CACHE_DIR?.trim() || '';
|
||||||
|
|
||||||
interface CachedImage {
|
interface CachedImage {
|
||||||
buffer: ArrayBuffer;
|
buffer: ArrayBuffer;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vitest läuft ohne SvelteKit-Plugin (zu schwer für Unit-Tests, zu viele
|
||||||
|
* Initialisierungsschritte). `$env/dynamic/{public,private}`-Imports werden
|
||||||
|
* stattdessen auf Stub-Module gemappt, die `process.env` direkt durchreichen.
|
||||||
|
* Damit lassen sich Server-Module isoliert testen, ohne das volle
|
||||||
|
* SvelteKit-Boot durchlaufen zu müssen.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
$lib: path.resolve(__dirname, 'src/lib'),
|
||||||
|
'$env/dynamic/public': path.resolve(__dirname, 'src/lib/_test/env-stub.ts'),
|
||||||
|
'$env/dynamic/private': path.resolve(__dirname, 'src/lib/_test/env-stub.ts'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.{test,spec}.{ts,js}'],
|
||||||
|
environment: 'node',
|
||||||
|
globals: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user