@xcwds/sveltekit
v0.5.1
Published
Binds the @xcwds kernel to SvelteKit: a Vite plugin that sets up SvelteKit, server hooks, the <App> component, Svelte 5 persist() and reactive settings, and a route registry.
Downloads
250
Maintainers
Readme
@xcwds/sveltekit
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
SvelteKit 3 only (no svelte.config.js): xcwds() takes SvelteKit's config and adds SvelteKit's
own Vite plugin, so don't add sveltekit() as well.
// vite.config.ts
import { xcwds } from '@xcwds/sveltekit/vite';
import { defineConfig } from 'vite';
export default defineConfig({ plugins: [xcwds({ paths: { base: '' } })] });// tsconfig.json: .xcwds/types.d.ts tells TypeScript what the plugins add
{
"extends": "$app/tsconfig",
"include": ["src", "*", ".xcwds/types.d.ts"],
"exclude": ["src/service-worker.ts"]
}// 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.
From SvelteKit 2 (0.4 and earlier)
Move the kit options from svelte.config.js into xcwds() and delete the file:
withXcwds({ kit: { paths } }, { adapter }) becomes xcwds({ paths }, { adapter }), and
plugins: [xcwds(), sveltekit()] becomes plugins: [xcwds({ paths })]. Then follow SvelteKit 3's
own migration: the tsconfig extends $app/tsconfig (add .xcwds/types.d.ts to its include,
which withXcwds() used to add for you), $app/environment is $app/env, and $lib is gone
(add "imports": { "#lib/*": "./src/lib/*" } to package.json and import
#lib/name.js, with the extension).
What each part does
xcwds(config, { adapter, root })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.jsand.xcwds/types.d.ts(a type-only import of each plugin's./cliententry). Then it creates SvelteKit's plugin with yourconfig, adding adapter-static with a404.htmlfallback (unlessconfig.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; yourcspdirectives are added to it. Its adapter wrapper scans the built site and fails the build on any other origin it finds (docs/privacy.md).rootdefaults to the working directory.- The
xcwdsVite plugin (the first onexcwds()returns) 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). It adds the page entries of plugins installed innode_modules(and@xcwds/core, when the app depends on it) tooptimizeDeps.include: only the generated module imports them, so Vite's dependency scan can't find them up front. 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.jsfirst imports.xcwds/worker-guard.js, which limits the worker'sfetch,WebSocket,EventSource,WebTransport,Cache#add/addAlland background fetches to the origins the page's CSP allows (@xcwds/sveltekit/guard), since that<meta>policy doesn't reach the worker. It then starts 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.configPlugin 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.
