import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig, type Plugin } from 'vitest/config' import { buildStamp } from './scripts/build-stamp.mjs' import { deployedClientMetadata } from './src/lib/client-metadata.js' /** * An atproto OAuth client is identified by a URL that serves its own metadata, so a deployed Radial * UI has to publish one at its own origin — and a static bundle cannot know its origin unless it is * told. `PUBLIC_RADIAL_ORIGIN=https://radial.example pnpm build` writes the document into the build; * without it the bundle is still complete, and still signs in on a loopback host, where the client * id encodes the metadata instead of pointing at it (see `auth.svelte.ts`). * * The origin is read from Vite's resolved env rather than the process environment, so it comes from * the same place `import.meta.env.PUBLIC_*` does — including `.env` files — and the published * document cannot disagree with what the app was built believing. */ function clientMetadata(): Plugin { let origin: string | undefined return { name: 'radial-client-metadata', apply: 'build', configResolved(config) { const configured: unknown = config.env.PUBLIC_RADIAL_ORIGIN origin = typeof configured === 'string' && configured ? configured : undefined }, generateBundle() { // SvelteKit builds twice; only the client build produces the static site the document belongs // beside, and only it has an `index.html` to sit next to. if (!origin || this.environment?.name === 'ssr') return this.emitFile({ type: 'asset', fileName: 'client-metadata.json', source: `${JSON.stringify(deployedClientMetadata(origin), undefined, 2)}\n`, }) }, } } export default defineConfig({ plugins: [sveltekit(), clientMetadata()], resolve: { alias: [ // Workspace package declarations expose `src` for types and `dist` at runtime. That split is // right for Node consumers, but it lets the dev server typecheck a newly added command or fold // projection and then execute an older build that does not contain it. The UI is the browser // implementation of these isomorphic packages, so bundle their current sources directly. { find: /^@radial\/core$/, replacement: new URL('../core/src/index.ts', import.meta.url).pathname }, { find: /^@radial\/atproto$/, replacement: new URL('../atproto/src/index.ts', import.meta.url).pathname }, { find: /^@radial\/ingest$/, replacement: new URL('../ingest/src/index.ts', import.meta.url).pathname }, { find: /^@radial\/sidecar$/, replacement: new URL('../sidecar/src/index.ts', import.meta.url).pathname }, ], }, // Which build this is, for the stamp at the foot of the pane (`src/lib/build.ts`). Read once, here, // rather than by the app: a bundle is a set of hashed files served from a static host and there is // nothing left at runtime that knows where it came from. It is a `define` rather than an env var // because the commit is not a knob an operator sets — it is a fact about the checkout the build ran // in, and `scripts/build-stamp.mjs` is what asks git for it. define: { __RADIAL_BUILD__: JSON.stringify(buildStamp()) }, // SvelteKit exposes `PUBLIC_*` through `$env`, but leaves Vite's own prefix at `VITE_`. Adding it // here makes `import.meta.env.PUBLIC_*` mean what its name says in this project, and — the reason // it is here at all — puts those values in `config.env` where a plugin can read them. envPrefix: ['PUBLIC_', 'VITE_'], server: { // **127.0.0.1, not localhost.** atproto's loopback redirect URI is always an IP — `http://localhost/` // is not a legal one — so an OAuth session established here always lands on `127.0.0.1`. Serving // the app on `localhost` would put the tab on a *different origin* from the one holding the // session's IndexedDB: sign-in would appear to work and then find nothing on return. The library // relocates a `localhost` tab for exactly this reason; binding here means it never has to. host: '127.0.0.1', // Workspace packages resolve to sibling directories outside this one, and the dev server refuses // to serve a file it was not told about. Production is unaffected — a build inlines them. fs: { allow: ['..'] }, }, // The same origin rule applies to a production build served locally, which is what `preview` is. preview: { host: '127.0.0.1' }, // Two suites, because they need two different Sveltes. // // Almost everything here is pure — the fold, the write path, the locators — and runs in Node with // no DOM at all, which is the property the whole app is built for. The exception is the markdown // editor: CodeMirror is a real contenteditable with real key handling, and the contracts it has to // keep (a value the form reads, a reset after submit, Escape leaving the field) cannot be asserted // without mounting it. Those files are `*.svelte.test.ts`, get a DOM, and resolve `svelte` through // its `browser` condition so `mount()` exists — which the Node suite must NOT do, since resolving // the browser build there would be testing something other than what runs. test: { projects: [ { extends: true, test: { name: 'pure', include: ['src/**/*.test.ts'], exclude: ['**/node_modules/**', 'src/**/*.svelte.test.ts'], environment: 'node', }, }, { extends: true, resolve: { conditions: ['browser'] }, test: { name: 'components', include: ['src/**/*.svelte.test.ts'], environment: 'jsdom', setupFiles: ['./src/lib/jsdom-layout.ts'], }, }, ], }, })