import type { Adapter } from "@sveltejs/kit";
import { sveltekit } from "@sveltejs/kit/vite";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import tailwindcss from "@tailwindcss/vite";
import Icons from "unplugin-icons/vite";
import { defineConfig } from "vitest/config";
import oauthMetadata, {
callbackPath,
loopbackClientId,
loopbackScope
} from "./src/lib/oauth-client-metadata.ts";
import { testids } from "./scripts/testids.ts";
import { timeAgoTag } from "./src/lib/timeAgoScript.ts";
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
import { playwright } from "@vitest/browser-playwright";
const dirname = import.meta.dirname;
const NOSCRIPT = `<noscript><p style="margin: 2rem; color: var(--y-muted)">this page needs javascript, the knot hasn't rendered it</p></noscript>`;
const spa = (out = "build", server = "build-server"): Adapter => ({
name: "spa",
async adapt(builder) {
builder.rimraf(out);
builder.rimraf(server);
builder.writeClient(out);
builder.writePrerendered(out);
const fallback = path.join(out, "index.html");
await builder.generateFallback(fallback);
// only urls the knot didn't prerender get this shell, which is blank without js
const shell = await readFile(fallback, "utf8");
await writeFile(fallback, shell.replace("</body>", `${NOSCRIPT}</body>`));
await builder.compress(out);
builder.writeServer(server);
builder.generateServerInstance(path.join(server, "instance.js"), {
serverDirectory: server
});
}
});
const chromiumBrowser = {
enabled: true,
headless: true,
provider: playwright({
launchOptions: process.env.E2E_CHROMIUM
? { executablePath: process.env.E2E_CHROMIUM }
: undefined
}),
instances: [{ browser: "chromium" } as const]
};
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
const devHost = process.env.VITE_DEV_HOST ?? "127.0.0.1";
const devPort = Number(process.env.VITE_DEV_PORT ?? 5173);
// docker desktop's macos file sharing never forwards inotify to the container,
// so an edit on the host lands on disk but the watcher never fires and hmr goes
// stale. polling is the only way to see those writes, and only the container
// needs it
const watch =
process.env.VITE_WATCH_POLL === "true" ? { usePolling: true, interval: 300 } : undefined;
const allowedHosts = [
".ts.net",
...(process.env.VITE_ALLOWED_HOSTS?.split(",")
.map((h) => h.trim())
.filter(Boolean) ?? [])
];
const devRedirectUri = `http://127.0.0.1:${devPort}${callbackPath}`;
const devScope =
process.env.VITE_OAUTH_SCOPE ??
(process.env.VITE_OAUTH_CLIENT_ID ? oauthMetadata.scope : loopbackScope);
const devClientId = loopbackClientId(devRedirectUri, devScope);
const buildClientId = process.env.VITE_OAUTH_CLIENT_ID ?? oauthMetadata.client_id;
const injectTestIds = process.env.E2E_TESTIDS === "true";
export default defineConfig({
plugins: [
{
name: "oauth-env",
config(_config, { command }) {
const serve = command === "serve";
return {
define: {
"import.meta.env.VITE_OAUTH_CLIENT_ID": JSON.stringify(
process.env.VITE_OAUTH_CLIENT_ID ??
(serve ? devClientId : buildClientId)
),
"import.meta.env.VITE_OAUTH_REDIRECT_URI": JSON.stringify(
process.env.VITE_OAUTH_REDIRECT_URI ??
(serve ? devRedirectUri : oauthMetadata.redirect_uris[0])
),
"import.meta.env.VITE_OAUTH_SCOPE": JSON.stringify(
serve ? devScope : (process.env.VITE_OAUTH_SCOPE ?? oauthMetadata.scope)
)
}
};
}
},
tailwindcss(),
Icons({
compiler: "svelte",
// every icon carries this, which svfe.css aligns to the text beside it
iconCustomizer: (_collection, _icon, props) => {
props["data-icon"] = "";
}
}),
sveltekit({
preprocess: [vitePreprocess(), ...(injectTestIds ? [testids()] : [])],
compilerOptions: {
runes: ({ filename }) =>
filename.split(/[/\\]/).includes("node_modules") ? undefined : true,
// `await` in templates, so a page can hand a streamed promise straight to
// <svelte:boundary pending> instead of threading {#await} through every view
experimental: { async: true }
},
adapter: spa(),
// defaults to the build's clock, which makes every build of the same source
// differ. the deploys pass the web tree's git hash, so it only changes with the app
version: { name: process.env.APP_VERSION || undefined },
paths: {
// GFE is evil and rewrites // to /
// so we do this to have /_app/ be at root always which means it doesnt break
// bcz // -> / removes a depth level which makes browser try to fetch app
// bundle that doesnt exist at that path
relative: false
}
})
],
define: { __TIME_AGO_TAG__: JSON.stringify(timeAgoTag) },
resolve: {
alias: {
"$icon/": "~icons/lucide/"
}
},
worker: { format: "es" },
server: {
host: devHost,
port: devPort,
strictPort: true,
allowedHosts,
watch
},
// pierre's worker entry pulls all of shiki (300+ lang/theme chunks). left to
// runtime discovery it triggers a re-optimize mid-page and in-flight dep
// requests 504 silently. dev-only, the production build ignores it
optimizeDeps: {
include: [
"@pierre/diffs",
"@pierre/diffs/worker",
"@pierre/diffs/ssr",
"@pierre/diffs/edit"
],
exclude: ["@pierre/diffs/react"]
},
test: {
expect: {
requireAssertions: true
},
projects: [
{
extends: "./vite.config.ts",
test: {
name: "server",
environment: "node",
include: ["src/**/*.{test,spec}.{js,ts}", "scripts/**/*.test.ts"],
exclude: ["src/**/*.svelte.{test,spec}.{js,ts}"]
}
},
{
extends: "./vite.config.ts",
test: {
name: "client",
include: ["src/**/*.svelte.{test,spec}.{js,ts}"],
browser: chromiumBrowser
}
},
{
extends: true,
plugins: [
// The plugin will run tests for the stories defined in your Storybook config
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest
storybookTest({
configDir: path.join(dirname, ".storybook")
})
],
// vite only crawls kit routes here, so undeclared story deps would
// optimize mid-run and wedge chromium; sanitize-html is cjs
optimizeDeps: {
noDiscovery: true,
include: ["storybook/test", "sanitize-html", "textarea-caret"]
},
test: {
name: "storybook",
browser: chromiumBrowser
}
}
]
}
});