Reference
Packaging
Publish a package of Svelte components that light up as islands in the ogygia app of a consumer. Ship the components, and the consumer marks the strategy. Or declare ogygia.files, and ship your own marks and routes.
On this page
You publish a package of Svelte components, and you want them to operate as islands in the ogygia app of someone else. Good news: there is almost nothing to do.
The one fact
A wake mark is file-local. It is the build of the consuming app that scans its own import graph, and emits the island chunks. Your components become islands the moment that app imports and marks them. Your job as a library author is only to make them importable, exactly like a normal Svelte component library. Nothing ogygia-specific ships in your package.
Ship your components
Export your .svelte files as subpaths, the same shape that @sveltejs/package makes:
{
"name": "my-widgets",
"exports": {
"./Widget.svelte": "./dist/Widget.svelte"
}
}The consumer marks the strategy that fits their page:
<script>
import Widget from 'my-widgets/Widget.svelte' with { wake: 'load' };
</script>
<Widget count={3} />That is the full integration. The consumer picks wake. Only they know whether the widget sits above the fold, must wait for a click, or is gated on a media query. Ship raw .svelte source, and their Svelte compiler handles the rest. lang="ts" in your components is fine (their build preprocesses it).
The boundary laws apply to a library component exactly as to a local one: props cross by value, callbacks do not cross, captured host state is a snapshot.
Barrel exports
When you re-export from an index, the consumer cannot hang with { … } on a named import. That is what the asRegion macro is for, used on their side, in their build:
import { Widget } from 'my-widgets';
const WidgetIsland = import.meta.og.asRegion(Widget, { wake: 'visible' });asRegion is barrel-friendly (region identity keys on the export name), and it runs in the app of the consumer. Thus it is the answer when the mark cannot sit on the import itself. You ship nothing for it.
Marks inside your package: ogygia.files
All above assumes that the consumer marks your components. Sometimes the mark belongs on your side: a Panel.svelte whose search box is an island by design, in whatever app it lands. Or a full route table that you ship, for the consumer to spread into their router. Those marks live in files inside node_modules, where the build of the consumer normally does not look.
Declare the paths that carry them, in your own package.json:
{
"name": "my-widgets",
"ogygia": {
"files": ["./src/widgets", "./src/pages/**/*.svelte", "./src/routes.ts"]
}
}The field operates like the "files" of npm: a bare directory includes all under it, globs are fine, and each path must stay inside the package. That is the full authoring surface. The consumer configures nothing. Their ogygia plugin reads the declarations of their direct dependencies, and gives your paths full app-source treatment. They are scanned and compiled like the own src/ of the app (.svelte and .ts; thus the marked imports of a shipped route table operate). They are kept un-external on the server, and kept out of the dev prebundle.
Only declared packages are scanned, and only their declared paths. There is no blind node_modules walk. A dependency that uses marks without a declaration gets a loud build warning, not silently dead islands.
Rules for declared paths:
- Ship source. Raw
.svelte, and.ts/.jswith thewith { … }attributes intact. The pipeline of the consumer compiles them. (When you transpile, make sure that your tool keeps import attributes.) - Depend on ogygia. A declared surface is real ogygia code. List
ogygiaas a dependency or peer, so that its imports resolve from your package. - The app-side placement rules apply unchanged: no content corpora in components, and server-only modules stay server-only.
A shipped route table is then only an export that the consumer spreads:
import { page } from 'ogygia/router';
import Board from './pages/Board.svelte' with { wake: 'load' };
export const widget_table = {
'/board': page(Board, { load: () => ({ title: 'Live board' }) })
};import { widget_table } from 'my-widgets/routes';
const app = routes({ '/': page(Home), ...widget_table }, { base });If your package imports ogygia
All above is for packages that ship plain components. The moment a package imports from 'ogygia' itself (say, a shared workspace package that uses the drop-in setContext of the context bridge), one rule applies to each build that touches it:
ogygia can never be bundled into a library. It is an external, in the same category as $app/*. The ogygia entry points resolve through virtual modules that exist only inside the Vite pipeline of the final app. And the runtime keeps module-level registries that must be a singleton per app. A lib-mode build must leave the import specifier intact. (An ogygia.files package that ships raw source satisfies this by construction: the specifiers reach the consumer untouched. The rule bites when a build step bundles.)
This propagates further than people expect in a monorepo. When your lib builds bundle workspace dependencies (Vite lib mode does, by default), then a single import { setContext } from 'ogygia' in one shared package surfaces in the bundle graph of each package that depends on it. Each of those builds now needs 'ogygia' in its external list too, although none of them import ogygia themselves. Two ways to hold this:
- Enumerate: add
'ogygia'next to your existing$app/$envexternals, in the config of each affected package. Correct, but repetitive. - Externalize all bare imports (the library-build convention). That covers ogygia,
$app/*,$env/*, and all else at the same time:
build: {
lib: { entry: './index.ts', name: 'my-widgets' },
rollupOptions: {
external: (id) => !id.startsWith('.') && !path.isAbsolute(id)
}
}Islands the consumer never imports
An island that your library renders only from its own server code, one that the consumer never imports, is invisible to the import graph of the consumer. That is exactly what the ogygia.files declaration above is for. Declare the path, and the build of the consumer scans it like its own source. Thus the client chunk of the island emits. (The own profiler UI of ogygia rides the same mechanism internally.)