Initial Navigator app: SvelteKit static build + OSM Overpass data pipeline

- SvelteKit project with @sveltejs/adapter-static (runs fully static)
- scripts/osm-data.mjs: pulls OSM data from Overpass API and converts to
  static GeoJSON under static/data/ with per-area manifest
- Leaflet map frontend that loads the static data and renders features
  with popups and area selection dropdown
- npm run build fetches data then produces a self-contained static site
This commit is contained in:
hermes-explorigin 2026-08-08 15:29:51 +00:00
parent 492916e0c5
commit 3413b7053f
20 changed files with 2033 additions and 2 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

102
README.md
View File

@ -1,3 +1,101 @@
# navigator # Navigator
Software project repository A custom web application that serves **OpenStreetMap (OSM) data**. It has a build
process that:
1. **Pulls** OSM data from the [Overpass API](https://overpass-api.de/) for
configurable geographic areas and feature types.
2. **Converts** it to static **GeoJSON** files.
3. **Builds** a **SvelteKit** frontend (with the static adapter) into a plain
static site that renders the data on an interactive **Leaflet** map.
The final output in `build/` is a fully self-contained static site — no server
runtime required. You can serve it with any static HTTP server (nginx, Apache,
`python -m http.server`, S3/Cloudflare Pages, etc.).
## Built With
- [SvelteKit](https://svelte.dev/kit) (Svelte 5, runes mode)
- [@sveltejs/adapter-static](https://kit.svelte.dev/docs/adapter-static) — outputs a static site
- [Leaflet](https://leafletjs.com/) — interactive map on the frontend
- [Overpass API](https://overpass-api.de/) — OSM data source
## Quick Start
```bash
npm install
# Fetch & convert OSM data into static/data/*.geojson
npm run build:data
# Full production build: (fetch data) + (static site) -> build/
npm run build
# Preview the production build locally
npm run preview
```
### Development
```bash
npm run dev # Vite dev server (your data in static/data is served too)
npm run check # Type + Svelte checks
```
## Build Process
| Command | What it does |
| -------------------- | ---------------------------------------------------------------- |
| `npm run build:data` | Runs `scripts/osm-data.mjs` to fetch OSM data → `static/data/` |
| `npm run build` | `build:data` followed by the SvelteKit static build → `build/` |
| `npm run preview` | Serves the `build/` output locally |
### Data layout
```
static/data/
├── _index.json # Manifest of all built areas (drives the UI dropdown)
└── <area>.geojson # One FeatureCollection per configured area
```
The frontend loads `/data/_index.json`, then fetches the GeoJSON file(s) for the
selected area(s) and renders them on the map with popups.
## Configuring Areas & Queries
Edit **`scripts/osm-data.mjs`** — the `AREAS` object describes each area:
- `bbox``[south, west, north, east]` in decimal degrees.
- `queries` — a map of name → Overpass QL snippet. Use `{{bbox}}` as the
placeholder for the area's bounding box.
- The script falls back across multiple public Overpass mirrors if one is
overloaded, and is polite to the API (short delay between queries).
Example:
```js
parks: `
way["leisure"="park"]({{bbox}});
out center tags geom;
`
```
List configured queries with `npm run data -- --queries`, or build a single area
with `npm run data -- --area=tulsa`.
## Deployment
The `build/` directory is entirely static. Serve it directly:
```bash
cd build
python3 -m http.server 8080
# or: nginx -s listen 8080; root /path/to/build; (with a fallback to index.html)
```
Because the site uses a static adapter with prerendering, everything can be
hosted on any static file host or CDN.
## Repository
Hosted on Gitea: https://gitea.thecookiejar.me/hermes-explorigin/navigator

1333
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "navigator",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"data": "node scripts/osm-data.mjs",
"data:area": "node scripts/osm-data.mjs",
"build:data": "node scripts/osm-data.mjs",
"build": "npm run build:data && vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.63.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"svelte": "^5.56.1",
"svelte-check": "^4.6.0",
"typescript": "^6.0.3",
"vite": "^8.0.16"
},
"dependencies": {
"@types/leaflet": "^1.9.22",
"leaflet": "^1.9.4"
}
}

203
scripts/osm-data.mjs Normal file
View File

@ -0,0 +1,203 @@
#!/usr/bin/env node
/**
* OSM Data Build Script
* ---------------------
* Queries the OpenStreetMap Overpass API for one or more configurable areas,
* fetches the requested feature types, and converts the results into static
* GeoJSON files under static/data/. These are bundled into the SvelteKit
* build and served as plain static files by the frontend.
*
* Usage:
* node scripts/osm-data.mjs # build all areas
* node scripts/osm-data.mjs --area=tulsa # build a single area
* node scripts/osm-data.mjs --queries # list available queries
*
* Output layout:
* static/data/<area>.geojson # merged FeatureCollection per area
* static/data/_index.json # manifest of all built areas
*/
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
const OUT_DIR = resolve(ROOT, 'static', 'data');
const OVERPASS_ENDPOINTS = [
'https://overpass-api.de/api/interpreter',
'https://overpass.kumi.systems/api/interpreter',
'https://maps.mail.ru/osm/tools/overpass/api/interpreter'
];
// ---------------------------------------------------------------------------
// Configuration: edit this to describe the areas and features you want.
// Each query is an Overpass QL snippet; the request is wrapped with the
// [out:json] output format. Use {{bbox}} as a placeholder for the area bbox.
// ---------------------------------------------------------------------------
const AREAS = {
tulsa: {
label: 'Tulsa, Oklahoma',
bbox: [35.9, -96.1, 36.3, -95.7], // [south, west, north, east]
queries: {
schools: `
(
node["amenity"="school"]({{bbox}});
way["amenity"="school"]({{bbox}});
relation["amenity"="school"]({{bbox}});
);
out center tags;
`,
firestations: `
(
node["amenity"="fire_station"]({{bbox}});
way["amenity"="fire_station"]({{bbox}});
);
out center tags;
`,
parks: `
way["leisure"="park"]({{bbox}});
out center tags geom;
`
}
}
};
// ---------------------------------------------------------------------------
// Overpass API client with retry/fallback across endpoints.
// ---------------------------------------------------------------------------
async function overpassFetch(query, signal) {
const body = new URLSearchParams({ data: query });
let lastErr;
for (const endpoint of OVERPASS_ENDPOINTS) {
if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
signal
});
if (!res.ok) throw new Error(`HTTP ${res.status} from ${endpoint}`);
return await res.json();
} catch (err) {
lastErr = err;
console.warn(` [warn] endpoint failed: ${endpoint} -> ${err.message}`);
}
}
throw new Error(`All Overpass endpoints failed. Last error: ${lastErr?.message}`);
}
function wait(ms, signal) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
// ---------------------------------------------------------------------------
// Overpass JSON -> GeoJSON conversion helpers.
// ---------------------------------------------------------------------------
function coordsFor(el) {
if (el.type === 'node') return [el.lon, el.lat];
if (el.center) return [el.center.lon, el.center.lat]; // out center
if (el.geometry && el.geometry.length) { // way geom outline
return el.geometry.map((p) => [p.lon, p.lat]);
}
return null;
}
function geometryFor(el) {
const coords = coordsFor(el);
if (!coords) return null;
// Preserve polygons for closed ways when we have full geometry.
if (el.type === 'way' && el.geometry && el.geometry.length > 2 &&
coords[0][0] === coords[coords.length - 1][0] &&
coords[0][1] === coords[coords.length - 1][1]) {
return { type: 'Polygon', coordinates: [coords] };
}
if (coords.length === 1) return { type: 'Point', coordinates: coords[0] };
return { type: 'LineString', coordinates: coords };
}
function toFeature(el) {
const geom = geometryFor(el);
if (!geom) return null;
return {
type: 'Feature',
id: `${el.type}/${el.id}`,
properties: { ...(el.tags || {}), _type: el.type, _id: el.id },
geometry: geom
};
}
// ---------------------------------------------------------------------------
// Build a single area.
// ---------------------------------------------------------------------------
async function buildArea(name, cfg, signal) {
console.log(`\n==> Building area "${name}" (${cfg.label})`);
const [south, west, north, east] = cfg.bbox;
const bboxStr = `${south},${west},${north},${east}`;
const features = [];
for (const [qname, qbody] of Object.entries(cfg.queries)) {
if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
const ql = `[out:json][timeout:180];\n${qbody.replaceAll('{{bbox}}', bboxStr)}`;
console.log(` - query: ${qname} (${bboxStr})`);
try {
const json = await overpassFetch(ql, signal);
const els = json.elements || [];
let count = 0;
for (const el of els) {
const f = toFeature(el);
if (f) { features.push(f); count++; }
}
console.log(` fetched ${els.length} elements, converted ${count} features`);
} catch (err) {
console.error(` [error] query ${qname} failed: ${err.message}`);
}
await wait(1500, signal); // be polite to the Overpass API
}
const fc = { type: 'FeatureCollection', crs: { type: 'name', properties: { name: 'urn:ogc:def:crs:OGC:1.3:CRS84' } }, features };
const outPath = resolve(OUT_DIR, `${name}.geojson`);
await writeFile(outPath, JSON.stringify(fc));
console.log(` wrote ${features.length} features -> ${outPath}`);
return { name, label: cfg.label, bbox: cfg.bbox, featureCount: features.length, path: `/data/${name}.geojson` };
}
// ---------------------------------------------------------------------------
// Main.
// ---------------------------------------------------------------------------
async function main() {
const args = process.argv.slice(2);
if (args.includes('--queries')) {
console.log('Configured areas and queries:\n');
for (const [name, cfg] of Object.entries(AREAS)) {
console.log(` ${name} (${cfg.label})`);
for (const q of Object.keys(cfg.queries)) console.log(` - ${q}`);
}
return;
}
const onlyArg = args.find((a) => a.startsWith('--area='));
const only = onlyArg ? onlyArg.split('=')[1] : null;
const signal = new AbortController().signal;
await mkdir(OUT_DIR, { recursive: true });
const manifest = [];
for (const [name, cfg] of Object.entries(AREAS)) {
if (only && name !== only) continue;
manifest.push(await buildArea(name, cfg, signal));
}
await writeFile(resolve(OUT_DIR, '_index.json'), JSON.stringify(manifest, null, 2));
console.log(`\nWrote manifest -> ${resolve(OUT_DIR, '_index.json')}`);
console.log('Done.');
}
main().catch((err) => {
console.error('Fatal:', err.message);
process.exit(1);
});

13
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,149 @@
<script lang="ts">
import 'leaflet/dist/leaflet.css';
import { onMount } from 'svelte';
import type { Map as LeafletMap } from 'leaflet';
type Feature = {
type: string;
id: string;
properties: Record<string, unknown>;
geometry: { type: string; coordinates: unknown };
};
type FeatureCollection = {
type: string;
features: Feature[];
};
interface Props {
/** URL(s) to one or more static GeoJSON files to load. */
dataUrls: string[];
/** Center [lat, lon]. */
center?: [number, number];
/** Initial zoom. */
zoom?: number;
/** Title shown in the header. */
title?: string;
}
let {
dataUrls = [],
center = [36.1, -95.9],
zoom = 12,
title = 'Navigator'
}: Props = $props();
let container: HTMLDivElement;
let map: LeafletMap = $state() as LeafletMap;
let markers = $state(0);
let loading = $state(true);
let error = $state<string | null>(null);
onMount(async () => {
let L;
try {
L = (await import('leaflet')).default;
} catch (e) {
error = `Failed to load Leaflet: ${(e as Error).message}`;
loading = false;
return;
}
map = L.map(container).setView(center, zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
const allLayers: L.Layer[] = [];
try {
for (const url of dataUrls) {
const res = await fetch(url);
if (!res.ok) throw new Error(`fetch ${url}: HTTP ${res.status}`);
const fc: FeatureCollection = await res.json();
const layer = L.geoJSON(fc as unknown as GeoJSON.GeoJsonObject, {
pointToLayer: (_f, latlng) => L.circleMarker(latlng, {
radius: 6,
fillColor: '#e11d48',
color: '#fff',
weight: 1,
opacity: 1,
fillOpacity: 0.8
}),
style: {
color: '#0284c7',
weight: 2,
fillColor: '#38bdf8',
fillOpacity: 0.25
},
onEachFeature: (f, lay) => {
markers++;
lay.bindPopup(popupHtml(f));
}
}).addTo(map);
allLayers.push(layer);
}
if (allLayers.length) {
const group = L.featureGroup(allLayers);
map.fitBounds(group.getBounds(), { padding: [40, 40] });
}
} catch (e) {
error = (e as Error).message;
}
loading = false;
});
function popupHtml(f: GeoJSON.Feature): string {
const props = (f.properties ?? {}) as Record<string, unknown>;
const name = (props.name as string | undefined) ?? String(f.id ?? '');
const rows = Object.entries(props)
.filter(([k]) => !k.startsWith('_'))
.map(([k, v]) => `<tr><th>${k}</th><td>${String(v)}</td></tr>`)
.join('');
return `
<strong>${name}</strong>
<table class="popup">${rows}</table>`;
}
</script>
<svelte:head>
<title>{title}</title>
</svelte:head>
<div class="wrap">
<header class="topbar">
<h1>{title}</h1>
<span class="meta">
{#if markers > 0}
{markers} features loaded
{:else}OSM data
{/if}
</span>
</header>
{#if loading}<div class="notice">Loading map &amp; data…</div>{/if}
{#if error}<div class="notice error">Error: {error}</div>{/if}
<div class="map" bind:this={container}></div>
</div>
<style>
.wrap { display: grid; grid-template-rows: auto 1fr; height: 100vh; }
.topbar {
display: flex; align-items: center; justify-content: space-between;
padding: 0 1.25rem; height: 56px;
background: #0f172a; color: #fff; font-family: system-ui, sans-serif;
}
.topbar h1 { font-size: 1.15rem; margin: 0; font-weight: 600; }
.meta { font-size: 0.8rem; color: #94a3b8; }
.map { width: 100%; height: 100%; z-index: 0; }
.notice {
position: absolute; top: 66px; left: 50%; transform: translateX(-50%);
z-index: 1000; background: #0f172a; color: #fff;
padding: 6px 14px; border-radius: 999px; font: 0.8rem system-ui, sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,.3);
}
.notice.error { background: #b91c1c; }
:global(.popup) { border-collapse: collapse; margin-top: 4px; font-size: 0.75rem; }
:global(.popup th) { text-align: left; padding-right: 10px; color: #64748b; }
:global(.popup td) { padding: 1px 0; }
</style>

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

11
src/routes/+layout.svelte Normal file
View File

@ -0,0 +1,11 @@
<script lang="ts">
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{@render children()}

2
src/routes/+layout.ts Normal file
View File

@ -0,0 +1,2 @@
export const prerender = true;
export const ssr = false;

95
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,95 @@
<script lang="ts">
import { onMount } from 'svelte';
import MapView from '$lib/components/MapView.svelte';
type AreaManifest = {
name: string;
label: string;
bbox: number[];
featureCount: number;
path: string;
};
let areas = $state<AreaManifest[]>([]);
let selected = $state('all');
let loading = $state(true);
let dataUrls = $state<string[]>([]);
let manifestError = $state<string | null>(null);
onMount(async () => {
try {
const res = await fetch('/data/_index.json');
if (!res.ok) throw new Error(`manifest HTTP ${res.status}`);
areas = await res.json();
// Default view: all areas on the map.
dataUrls = areas.map((a) => a.path);
} catch (e) {
manifestError = (e as Error).message;
}
loading = false;
});
function onSelect() {
if (selected === 'all') {
dataUrls = areas.map((a) => a.path);
} else {
const found = areas.find((a) => a.name === selected);
dataUrls = found ? [found.path] : [];
}
}
const totalFeatures = $derived(areas.reduce((n, a) => n + a.featureCount, 0));
</script>
<svelte:head>
<title>Navigator — OSM Map</title>
</svelte:head>
<div class="app">
<div class="controls">
<label>
Area
<select bind:value={selected} onchange={() => onSelect()}>
<option value="all">All areas</option>
{#each areas as a (a.name)}
<option value={a.name}>{a.label}</option>
{/each}
</select>
</label>
{#if areas.length}
<span class="stats">{totalFeatures} total features across {areas.length} area(s)</span>
{/if}
</div>
{#if manifestError}
<div class="error">
Could not load data manifest (<code>/data/_index.json</code>).<br />
Run <code>npm run build:data</code> to fetch OSM data, then rebuild.
<p class="detail">{manifestError}</p>
</div>
{:else if dataUrls.length}
<MapView {dataUrls} title="Navigator" />
{:else if !loading}
<div class="error">No built data areas found.</div>
{/if}
</div>
<style>
.app { display: grid; grid-template-rows: auto 1fr; height: 100vh; }
.controls {
display: flex; align-items: center; gap: 1.25rem;
padding: 0.75rem 1.25rem;
background: #1e293b; color: #e2e8f0;
font: 0.85rem system-ui, sans-serif; z-index: 500;
}
.controls label { display: flex; align-items: center; gap: 0.5rem; }
select {
background: #0f172a; color: #fff; border: 1px solid #475569;
border-radius: 6px; padding: 4px 8px; font-size: 0.85rem;
}
.stats { color: #94a3b8; margin-left: auto; }
.error {
padding: 3rem 2rem; font: 0.95rem/1.5 system-ui, sans-serif; color: #b91c1c;
}
.detail { color: #64748b; font-size: 0.8rem; }
</style>

14
static/data/_index.json Normal file
View File

@ -0,0 +1,14 @@
[
{
"name": "tulsa",
"label": "Tulsa, Oklahoma",
"bbox": [
35.9,
-96.1,
36.3,
-95.7
],
"featureCount": 512,
"path": "/data/tulsa.geojson"
}
]

File diff suppressed because one or more lines are too long

3
static/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"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
}

18
vite.config.ts Normal file
View File

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-static';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
sveltekit({
compilerOptions: {
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
runes: ({ filename }) =>
filename.split(/[\\/]/).includes('node_modules') ? undefined : true
},
// Static adapter: builds the app into static HTML/JS/CSS that can be
// served by any static HTTP server (nginx, python -m http.server, etc).
adapter: adapter()
})
]
});