API
ogygia/vite
The Vite plugin and its config surface.
On this page
- debarrel
- import_keys_hint
- islandChunkFileName
- islandId
- islandPublicUrl
- normalize_import_keys
- ogygia
- regionId
- regionIdentity
- rewrite_lake_import_to_placeholder
- strategyKey
- wrapperVirtualId
- CLIENT_BINDING_STUB
- DEFAULT_IMPORT_KEYS
- ContentPreset
- ContentPreset.markdown
- DebarrelOptions
- DebarrelOptions.packages
- DebarrelOptions.force
- DebarrelOptions.keep
- DebarrelOptions.importers
- DebarrelOptions.followPackages
- DebarrelOptions.report
- DebarrelOptions.debug
- OgygiaOptions
- OgygiaOptions.regions
- OgygiaOptions.router
- OgygiaOptions.content
- OgygiaOptions.importKeys
- OgygiaOptions.devtools
- OgygiaOptions.rateLimit
- OgygiaOptions.sessionCookie
- OgygiaOptions.profiler
- OgygiaOptions.freeze
- OgygiaOptions.barrels
- OgygiaOptions.standalone
- OgygiaPreset
- OgygiaPreset.render
- OgygiaPreset.wake
- OgygiaPreset.margin
- OgygiaPreset.maxAge
- OgygiaPreset.onExpire
- OgygiaPreset.prefetch
- OgygiaPreset.revalidate
- OgygiaPreset.keep
- OgygiaRateLimit
- OgygiaRateLimit.max
- OgygiaRateLimit.windowMs
- RegionsOptions
- RegionsOptions.visible
- RegionsOptions.presets
- RegionsOptions.ttl
- ImportKeys
- Matcher
Auto-generated from the package’s own .d.ts on every build — signatures, JSDoc, and examples straight from the source.
import { … } from 'ogygia/vite';Exports debarrel · import_keys_hint · islandChunkFileName · islandId · islandPublicUrl · normalize_import_keys · ogygia · regionId · regionIdentity · rewrite_lake_import_to_placeholder · strategyKey · wrapperVirtualId · CLIENT_BINDING_STUB · DEFAULT_IMPORT_KEYS · ContentPreset · DebarrelOptions · OgygiaOptions · OgygiaPreset · OgygiaRateLimit · RegionsOptions · ImportKeys · Matcher
debarrel
function
function debarrel(options?: DebarrelOptions | true, internal?: DebarrelInternal): Plugin;import_keys_hint
function
function import_keys_hint(import_keys: ImportKeys): RegExp;islandChunkFileName
function
Deterministic client chunk path for a hydrate island (mirrors og-runtime.<hash>.js).
SSR bakes this into <ogygia-region entry> so the sticky runtime can import(entry) with no
app-wide regions map — Kit builds server before client, so content-hashed Vite names can't hand off.
function islandChunkFileName(iid: string, appDir?: string): string;islandId
function
Deterministic short id for a region (stable across dev + build).
When salt is set (production OGYGIA_SECRET), ids are not offline-computable (P1-ID).
Paths are always posix so SSR/client builds agree across OS path separators.
function islandId(relHostPath: string, index: string | number, salt?: string): string;islandPublicUrl
function
App-internal URL for islandChunkFileName: /<appDir>/immutable/… (leading slash, appDir,
NO base). It is what SSR bakes onto <ogygia-region entry>; Region.svelte runs it through Kit's asset() at render time, which is the sole authority on base / the assets CDN / relative paths —
so a base is never baked here (that would double-apply it). NOT the on-disk output filename.
function islandPublicUrl(iid: string, appDir?: string): string;normalize_import_keys
function
function normalize_import_keys(overrides?: Partial<ImportKeys> | null): ImportKeys;ogygia
function
Vite plugin: transforms with { hydrate | defer | preset } imports into islands,
serves virtual island modules, and wires signed region endpoints for deferred HTML.
Place before sveltekit() in vite.config.
function ogygia(options?: OgygiaOptions): Plugin[];options— Plugin configuration. SeeOgygiaOptions.
Returns Vite plugins (ogygia pre + island sourcemap fix post). Vite flattens the array.
regionId
function
Hash an identity string (optional production salt) → 12-char region id.
function regionId(identityKey: string, salt?: string): string;regionIdentity
function
Cross-host stable identity: posix component path + strategyKey.
Drives region ids so multiple hosts / import sites / <A /> usages share one module.
function regionIdentity(componentRelPath: string, mark: {
strategy: string;
options?: Record<string, unknown> | null;
}): string;rewrite_lake_import_to_placeholder
function
Rewrite a lake binding's import to the render-nothing placeholder (client island modules only). Default imports are repointed; named imports drop that specifier (and keep siblings) then add a default import of the placeholder under the same local name.
function rewrite_lake_import_to_placeholder(src: string, local: string, placeholder: string): string;strategyKey
function
Fingerprint of a region mark for dedupe. Same component path + same key → one wrapper/entry.
function strategyKey(mark: {
strategy: string;
options?: Record<string, unknown> | null;
}): string;wrapperVirtualId
function
Portable wrapper module id — SSR / csr=true host binding (Island/ServerIsland/Lake shell).
function wrapperVirtualId(iid: string): string;CLIENT_BINDING_STUB
const
csr=false CLIENT host binding target. Kit still emits those page nodes; pointing marked
imports here (instead of wrappers) keeps N island wrappers/entries out of the page graph so emitFile owns hydrate modules and Rolldown does not thin-facade them. Hydration uses import(entry) only — this stub is never loaded for island JS.
Beside the stub the transform emits import 'virtual:ogygia/fouc-css/<entry>' so Kit still
links stylesheets (FOUC) via CSS-only side effects. Importing the full .svelte JS module
dual-owns it with the emitFile island entry and Rolldown thin-facades og-region.*.
Omitting CSS entirely orphans rules (scoped class hashes, no stylesheet).
const CLIENT_BINDING_STUB = "virtual:ogygia/client-binding-stub"DEFAULT_IMPORT_KEYS
const
Default import-attribute keys — the two-dial grammar. Internal role names stay hydrate/defer (the wire format + runtime keep those anchors); the user-facing ATTRIBUTE names are the values.
Override via ogygia({ importKeys }).
renderattribute → the MODE:static(inline HTML) |deferred(a hole, fetched) |live(a hole that revalidates). Defaults tostatic.wakeattribute → when the region comes alive: forstaticthat is HYDRATION; fordeferred/livethat is the FETCH schedule.
const DEFAULT_IMPORT_KEYS: {
readonly wake: "wake";
readonly render: "render";
readonly preset: "preset";
readonly region: "region";
}ContentPreset
interface
A named CONTENT preset — a partial content-config referenced by a literal preset: 'name' on a
loader macro (import.meta.og.loader.folder('../docs', { preset: 'name' })). The referencing
collection's files compile as their own module VARIANTS (?og_preset=name), so the same file
used by another collection under another preset renders independently — never a conflict.
interface ContentPreset {/*…*/}ContentPreset.markdown
Markdown options merged over content.markdown (per setting key — depth-2 replace).
markdown?: MarkdownOptions;DebarrelOptions
interface
interface DebarrelOptions {/*…*/}DebarrelOptions.packages
DEPENDENCIES whose barrels may be bypassed too. The project's own modules need no listing:
every file outside node_modules is a candidate, and purity decides (a barrel is PURE when
every top-level statement is an import, a re-export, a type, or export {}). Packages are
opt-in because Vite pre-bundles bare imports in dev: a leaf import that skips the optimizer
while another importer gets the pre-bundled copy is two instances of one module. List a
package here when you know it is safe (an ESM component library, excluded from optimizeDeps). Names, globs or regexes: ['@acme/ui', '@acme/*', /\/components\/index\.js$/].
A plain string matches the specifier prefix ('@acme/ui' covers @acme/ui/components).
packages?: Matcher[];DebarrelOptions.force
Modules that are IMPURE barrels (own top-level code next to the re-exports) but should still
be rewritten: the re-exported names go to their leaves, the barrel itself stays imported for
its side effects (import 'barrel'). Without this, an impure barrel is left alone.
force?: Matcher[];DebarrelOptions.keep
Never rewrite imports of these modules, whatever else says (a barrel whose own module-level code the app depends on — a store singleton created in the barrel itself).
keep?: Matcher[];DebarrelOptions.importers
Which IMPORTERS are rewritten. Default: project files (not node_modules) with a JS / TS /
Svelte extension.
importers?: {
include?: Matcher[];
exclude?: Matcher[];
};DebarrelOptions.followPackages
Follow export * from into node_modules packages when building a barrel's export map
(default true). A star that reaches something unparsable (CJS, JSON) makes the barrel
OPAQUE for the names it cannot see: those stay on the barrel import.
followPackages?: boolean;DebarrelOptions.report
A report at the end of each build leg — files rewritten, imports and names moved, barrels
bypassed, time spent in the pass, then the barrels by names moved. Default true: the eight
biggest and a count of the rest; 'all': every barrel; false: nothing.
report?: boolean | 'all';DebarrelOptions.debug
Log every rewrite as it happens (dev + build).
debug?: boolean;OgygiaOptions
interface
Options for the {@link ogygia } Vite plugin — one top-level key per subsystem, each subsystem defaults + its own presets.
interface OgygiaOptions {/*…*/}import { ogygia } from 'ogygia/vite';
export default defineConfig({
plugins: [
ogygia({
regions: {
visible: { margin: '200px' },
presets: { chart: { wake: 'visible', margin: '200px' } },
ttl: 3600
}
}),
sveltekit()
]
});OgygiaOptions.regions
The regions subsystem: island defaults (visible.margin), named island presets, and the
capability lifetime of deferred/live holes (ttl).
regions?: RegionsOptions;OgygiaOptions.router
Client-side SPA router — app-wide, on by default. It intercepts same-origin links, swaps <body>, merges <head>, and keeps data-ogygia-keep chrome across navigations. No component
to place: the server handle injects the runtime + the ogygia-router meta into every page.
true(default) — router on, View Transitions on, form continuity on.false— router off (the whole feature is tree-shaken out; same-origin links do full MPA loads, and form continuity — which rides SPA navigation — goes with it).{ viewTransitions: false }— router on, but no View Transitions API on navigation.{ forms: false }— router on, but an island's half-filled form fields are NOT carried across SPA navigation (continuity off).{ serverDelta: true }— opt IN to server-delta nav: an SPA nav tells the server (via anx-ogygia-knownheader) which islands it already has live, so the server SKIPS re-rendering the unchanged ones and the client keeps them running. OFF by default (it is a new client↔server protocol); when off the client never sends the header and the server always full-renders.
Per-page escape hatch (no second config): a page opts itself out of View Transitions by
emitting <svelte:head><meta name="ogygia-router" content="plain" /></svelte:head> — the handle
injects the app default but a page that sets its own meta wins.
router?: boolean | {
viewTransitions?: boolean;
forms?: boolean;
serverDelta?: boolean;
};OgygiaOptions.content
The content subsystem. markdown configures the mdsvex preprocessor (themes, remark plugins,
heading ids…) — the defaults for every markdown file. presets are named variants a loader
macro opts a whole collection into ({ preset: 'name' }); requires markdown to be set (the
defaults are the base every preset merges over). The rest are dev HMR options.
content?: ContentPluginOptions & {
markdown?: MarkdownOptions;
presets?: Record<string, ContentPreset>;
};OgygiaOptions.importKeys
Rename the import-attribute keys claimed by the transform.
Defaults stay hydrate / defer / preset. Escape hatch if another tool already
uses those names on the same imports.
Preset definitions (RegionsOptions.presets) still use canonical hydrate / defer / margin / remount — only the with { … } spellings in source change.
importKeys?: Partial<ImportKeys>;OgygiaOptions.devtools
The devtools EVENT LAYER (internal/notes/devtools.md). Off by default — user apps never ship it.
Turn it on and the framework emits typed lifecycle events (island discovery, server render,
wire crossings, client wake, hub mint/resolve, nav reconcile) keyed on the identities it already
mints, which any sink (ogygia/devtools) can read: the REPL, a boundary-lens overlay, a bug-
report trace, or our own event-driven e2e.
false(default) — the gate is off; every emit folds toif (false)and the bus tree-shakes out of the runtime chunk. Zero cost.true— on, wherever you turn it on. In a build (a preview deploy) the pages carry only a small launcher: the dock's code loads when someone opens it, and the page is measured from the next load on. You decide where:ogygia({ devtools: mode !== 'production' }).
devtools?: boolean;OgygiaOptions.rateLimit
Per-IP budget for the signed island endpoint served by ogygiaHandle().
Default { max: 60, windowMs: 60_000 }. Pass false to disable.
rateLimit?: false | OgygiaRateLimit;OgygiaOptions.sessionCookie
Cookie name to seal into the region MAC (opt-in). Empty/prerender stays unbound.
Harvested capability URLs then fail verification without that cookie.
Default false (unbound).
sessionCookie?: false | string;OgygiaOptions.profiler
The drop-in SSR profiler — configured ENTIRELY here, nowhere else. true (or an options object)
turns it on: the plugin builds its UI (real Svelte islands, marked inside ogygia's own source and
rendered only from its handle, so the client build can't otherwise see them) AND transports this
config to ogygia.handle() via a virtual module. The handle dynamically imports and mounts the
profiler itself — no profiler() in hooks, no handler wiring, and the profiler's weight loads only
when it's on. The secret defaults to the OGYGIA_PROFILER_SECRET env var at runtime; pass { secret } to override (baked into the server build). Off → no profiler at all.
profiler?: boolean | ProfilerOptions;OgygiaOptions.freeze
Frozen pages (render-on-write) — the SWITCH + serializable policy, nothing else. true turns the handle's freeze read/write path on with the tier-1 in-process store: a
page whose render observed no personalization is stored whole and served as bytes until freeze.invalidate(url) / freeze.invalidateWhere({ prefix }) or the TTL backstop.
Live adapters (a valkey client, CDN purge creds) can never ride a virtual module — they are
configured in hooks.server.ts via freeze.configure({ store, edge }) from 'ogygia/freeze'. { ttl } overrides the backstop seconds (clamped to [1, 86400]).
{ default } sets the app-wide opt-in: true (the default, and what a bare true gives)
keeps the auto behaviour — every eligible page is stored, observed-purity refuses the rest,
and a route opts OUT with export const freeze = false. false flips the whole app to
OPT-IN: nothing is stored unless a page or a layout it sits under declares export const freeze = true (it cascades like csr, deepest declaration wins). The export
is read and stripped by ogygia before Kit sees it, so Kit never rejects the non-Kit option.
freeze?: boolean | {
ttl?: number;
default?: boolean;
};OgygiaOptions.barrels
Barrel imports become leaf imports at transform time — import { Button } from '$lib' (an index.ts that re-exports forty files) becomes import Button from '…/Button.svelte', so the
module graph (dev) and the chunk graph (build) hold only the leaves a file uses.
true— every PURE barrel in the project (outsidenode_modules) is bypassed.{ packages, force, keep, … }— opt dependencies in, force impure barrels, keep some.
Independent of islands: an import that carries region marks (with { wake } …) is never
rewritten — a barrel binding is marked through asRegion or a mark in the barrel, as before.
The same plugin is debarrel() from 'ogygia/vite' for any Vite app.
barrels?: boolean | DebarrelOptions;OgygiaOptions.standalone
standalone?: boolean;OgygiaPreset
interface
Named strategy bundle referenced from source via with { preset: 'name' } (or the renamed importKeys.preset key).
A preset speaks the SAME two-dial grammar as an inline import: render (the delivery mode) + wake (the schedule), plus the tuning options that aren't allowed inline (margin, maxAge, …).
interface OgygiaPreset {/*…*/}OgygiaPreset.render
Delivery mode (the render import attribute): 'static' (default — an island that hydrates)
| 'deferred' (a hole whose HTML is fetched) | 'live' (a hole that revalidates).
render?: 'static' | 'deferred' | 'live';OgygiaPreset.wake
Schedule (the wake import attribute): hydration when render is 'static', the FETCH
schedule when 'deferred'/'live'. 'load' | 'idle' | 'visible' | 'interaction' |
a CSS media query | 'none' (a frozen lake).
wake?: string;OgygiaPreset.margin
IntersectionObserver rootMargin when the schedule is 'visible'.
margin?: string;OgygiaPreset.maxAge
render: 'deferred' — response cache max-age for the hole's HTML: seconds (number) or a
duration string ('30s' | '5m' | '1h'). Absent or 0 → no-store: the hole is dynamic,
re-rendered on every request. A positive value opts into a private, max-age browser cache.
Signed into the hole's endpoint so a harvested URL can't be re-pointed at a longer cache.
With render: 'live' this is instead the client revalidate staleness — use a duration string
to stay unit-explicit across both.
maxAge?: number | string;OgygiaPreset.onExpire
render: 'live' — past maxAge, whether to clear the hole ('empty') or refetch ('fetch').
onExpire?: 'empty' | 'fetch';OgygiaPreset.prefetch
render: 'deferred' — warm the hole's HTML into the frame store on this schedule ('load' | 'idle' | 'visible' | a media query) while wake still decides when it swaps in. An
on-demand hole (wake: 'interaction') then has its bytes before the first hover: the gesture
joins the warm frame instead of paying the origin round trip. Never 'interaction' — that is
the swap schedule, wake's job.
prefetch?: string;OgygiaPreset.revalidate
render: 'live' — the revalidate schedule (false disables). Defaults to wake.
revalidate?: false | string;OgygiaPreset.keep
Continuity name (the keep import attribute): the live island relocates across SPA
navigation instead of remounting when the next page carries the same name.
keep?: string;OgygiaRateLimit
interface
Per-IP budget for the signed deferred-region / lake-remount endpoint.
interface OgygiaRateLimit {/*…*/}OgygiaRateLimit.max
Max requests per window (default 60). 0 disables allowing any.
max?: number;OgygiaRateLimit.windowMs
Sliding window length in milliseconds (default 60_000).
windowMs?: number;RegionsOptions
interface
The regions subsystem — island defaults + the named preset dictionary. One grammar shared by
every subsystem: defaults at the root, variance in presets, referenced from a use site by a
LITERAL preset: 'name' (here: import X from '…' with { preset: 'name' }).
interface RegionsOptions {/*…*/}RegionsOptions.visible
Global defaults for islands that use wake: 'visible' / render+wake: 'visible' without their own margin (via a preset).
visible?: {
margin?: string;
};RegionsOptions.presets
Named strategy bundles. Reference one from an import: import Chart from '$lib/Chart.svelte' with { preset: 'chart' };
presets?: Record<string, OgygiaPreset>;RegionsOptions.ttl
How long a deferred/live hole's signed capability URL stays valid, in seconds (default 3600).
Clamped to [60, 86400]. Keep short for harvested-URL risk. Holes outlive this automatically
where they must: on prerendered and frozen pages, and on a page a load marks shared-cacheable
(setHeaders({ 'cache-control': 'public, s-maxage=…' })), where an anonymous hole is signed for
the page's cache life; a hole on an older cached copy renews itself when it wakes.
ttl?: number;ImportKeys
type
Import-attribute key names claimed by the transform
(with { wake | render | preset | region }). Override via ogygia({ importKeys }) when
another tool already uses the default names.
type ImportKeys = {
wake: string;
render: string;
preset: string;
region: string;
};Matcher
type
ogygia({ barrels }) — what counts as a barrel, which importers get rewritten, what to keep.
A BARREL is a module whose exports are re-exports of other modules: index.ts files, a package's components.ts, export * from fan-outs. Importing one name from it drags the whole graph in: in
dev every module behind it is transformed and served, in build the chunk graph follows the import
graph before tree-shaking, and a package that is not sideEffects: false keeps all of it. The
debarrel pass replaces import { A } from 'barrel' with import A from '<leaf of A>' at
transform time, so the graph is the leaves the file actually uses.
This pass is independent of islands: it does not read or write region marks. An import that
carries ogygia attributes (with { wake } …) is left exactly as written — a barrel binding is
marked through asRegion / a mark in the barrel, as before.
type Matcher = string | RegExp | ((id: string) => boolean);