Skip to content

API

ogygia/vite

The Vite plugin and its config surface.

On this page

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. See OgygiaOptions.

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 }).

  • render attribute → the MODE: static (inline HTML) | deferred (a hole, fetched) | live (a hole that revalidates). Defaults to static.
  • wake attribute → when the region comes alive: for static that is HYDRATION; for deferred/live that 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 an x-ogygia-known header) 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 to if (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 (outside node_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);