Binds the @xcwds kernel to SvelteKit: config loading, plugin entries for the page and the service worker, the pre-paint script, the manifest and icons, and Svelte 5 helpers. Design: RFC 0001, decisions 1 to 4 and 9.
Setup
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { xcwds } from '@xcwds/sveltekit/vite';
import { defineConfig } from 'vite';
export default defineConfig({ plugins: [xcwds(), sveltekit()] });
// svelte.config.js: a Vite plugin can't set the adapter or CSP, so a helper does
import { withXcwds } from '@xcwds/sveltekit/config';
export default await withXcwds({ kit: { paths: { base: '' } } });
// src/hooks.server.ts
export { handle, init } from '@xcwds/sveltekit/hooks';
// src/hooks.client.ts
export { init } from '@xcwds/sveltekit/hooks';
// src/service-worker.ts
import '../.xcwds/worker.js';
// src/routes/+layout.ts
export const prerender = true;
<!-- src/routes/+layout.svelte -->
<script>
import { App } from '@xcwds/sveltekit';
let { children } = $props();
</script>
<App>{@render children()}</App>
src/app.html needs %xcwds.head% right after %sveltekit.head% (see decision 4). Add
.xcwds to .gitignore if your tools don't read the one generated inside it.
What each part does
withXcwds(svelteConfig, { adapter })loadsxcwds.config.ts(or.js) with Vite'srunnerImport, validates it, loads each plugin'sbuildexport and runs its build hooks (onConfig,onManifest,onHead,onWorker,onPrerender), and writes.xcwds/worker.js. It sets adapter-static with a404.htmlfallback (unlesskit.adapteris set), prerenderentriesfor every registered route andonPrerenderpath, and a hash-mode CSP that allows only the app's origin plus the origins plugins declare (networkmetadata) andprivacy.allowOriginslists; yourkit.cspdirectives are added to it. Its adapter wrapper scans the built site and fails the build on any other origin it finds (docs/privacy.md).xcwds()servesvirtual:xcwds/client(each plugin's./cliententry, routes and head data) to the page bundle, and emitsmanifest.webmanifestand the icons rendered frombrand.icon(with@xcwds/core/build, which needs@resvg/resvg-js).handleputs the manifest, icon and iOS tags and one pre-paint script (plugins'onHeadsnippets and settings'prePaintfields) in place of%xcwds.head%, and adds the script's hash to the page's CSP.initloads the plugins before the first render, so pages render and hydrate with their decorators, routes and settings fields..xcwds/worker.jsstarts the worker from@xcwds/sveltekit/worker: plugins'onFetchhooks answer first; the build, static files, prerendered pages, manifest and icons are precached on install and served from this version's cache first; anything else goes to the network, with404.htmlfor offline navigations. The strategy readsapp.worker.policy(extra precache paths, exclusions, the fallback page, runtime caching) once every worker plugin has registered;@xcwds/plugin-offlinesets it. Nothing is cached at runtime unless a plugin turns it on. A new version waits until the old one's tabs have closed or a plugin callsapp.worker.skipWaiting()(@xcwds/plugin-updatedoes when the user taps Update). A static file with the same path as a generated one (manifest.webmanifest,icons/*,favicon.ico) fails the build, since it would replace the generated file.<App>provides the app, boots it after mount (onBoot, thenonReady), then marks<html data-hydrated>(whatgotoHydratedin@xcwds/testingwaits for), and turns navigations intoonNavigateandafterNavigate, and page visibility intoonHidden/onVisible. Vite HMR closes the old app.@xcwds/sveltekit/routesis the framework-free core (no Svelte, SvelteKit or Vite imports) that<App>, the service worker and@xcwds/testingshare, so they can't drift apart: the route registry (decorateRoutes),setupApp(create the app, add the routes, register the plugins),routeOf(the route hooks see for a URL),askGuardsanddecide(what guards' answers mean: cancel, redirect, the 5-redirect limit, the first page) andworkerPath(the requests the worker handles).
Navigation guards
onNavigate(to, from) hooks run before every in-app navigation (links, goto, Back and
Forward, and links to app paths SvelteKit has no route for). Returning false cancels it, a path
(without the base) redirects there, and nothing lets it go ahead. Hooks that answer synchronously
decide on the spot. If one returns a promise, the navigation is held and repeated once the hooks
allow it: Back and Forward with history.go() (after SvelteKit has undone the held one), everything else with goto(url), so a guarded
link loses goto options and data-sveltekit-* link options such as replaceState. Only the
latest held navigation counts. A redirect runs the target's own guards, up to 5 redirects in a
row; after that the navigation stops and the error goes to onError.
The first page is loaded rather than navigated to, so its guards run once the app has booted
(with from null): a redirect replaces it in history, and false does nothing, since the page
is already showing. Both are dropped if the user has navigated away by the time the guards answer.
Everything that builds a URL honours paths.base. Route paths, hook paths and the route
registry never include it.
In components
import { appPath, brand, persist, routeInfo, settings, useApp } from '@xcwds/sveltekit';
const app = useApp(); // the kernel app, with every plugin's decorators
persist(
app.timers.presets,
() => presets,
(v) => (presets = v)
); // loads after mount, saves on change
settings.current.theme; // defaults until `settings.ready`, then the saved values, reactively
routeInfo(page.url.pathname); // { title, emoji, parent, width } of the current page
appPath(page.url.pathname); // '/hello' under any base path (never relative), or null outside
brand.name; // and brand.tagline, from xcwds.config
Plugin packages
A plugin package exports up to three entries, each a kernel plugin:
// index.js: the config factory, and build hooks run in Node
export default descriptor('xcwds-plugin-hello');
export const build = definePlugin((app, options) => {
app.route({ path: '/', title: 'Hello', emoji: '๐', parent: '/' }); // under its `prefix`
app.addHook('onHead', () => 'document.documentElement.dataset.hello="1";');
});
// client.js (the page, and Node at prerender) and worker.js (the service worker)
export default definePlugin((app, options) => { ... });
Its pages are thin route files in the app that render a component the plugin exports
(decision 2). A route is { path, title, emoji?, parent?, width?, private? }; private: true
marks a personal page that plugins keep out of what they share or list. examples/plugin-hello has all three.
Dev, preview and static hosting
vite buildwrites a static site; serve it like GitHub Pages does (unknown paths get404.html, which boots the app and shows+error.svelte).vite devrenders pages on request: the CSP is a header, the manifest and icons come from memory, and there is no404.html, so offline not-found pages only work in a build. SvelteKit registers the service worker in dev too, with empty precache lists.vite previewserves the build output but renders unknown paths on the server instead of serving404.html.