Skip to content

Reference

Packaging

Publish a package of Svelte components that become islands in a consumer's ogygia app. Either ship the components and let the consumer choose the strategy, or declare ogygia.files and ship your own marks and routes.

On this page

If you publish a package of Svelte components and want them to work as islands in someone else’s ogygia app, there is very little you need to do.

How it works

A wake mark is local to the file it is written in. The consuming app’s build scans its own import graph and emits the island chunks, so your components become islands as soon as that app imports and marks them. As a library author, your only job is to make them importable, exactly as for a normal Svelte component library, and nothing ogygia-specific ships in your package.

Ship your components

Export your .svelte files as subpaths, in the same shape that @sveltejs/package produces:

{
  "name": "my-widgets",
  "exports": {
    "./Widget.svelte": "./dist/Widget.svelte"
  }
}

The consumer then 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 whole integration. The consumer chooses wake, because only they know whether the widget sits above the fold, should wait for a click, or depends on a media query. Ship raw .svelte source and their Svelte compiler handles the rest; lang="ts" in your components is fine, because their build preprocesses it.

The boundary rules apply to a library component exactly as they do to a local one: props cross by value, callbacks do not cross, and captured host state is a snapshot.

Barrel exports

When you re-export from an index, the consumer cannot attach with { … } to a named import. The asRegion macro covers that case, used on their side in their build:

import { Widget } from 'my-widgets';

const WidgetIsland = import.meta.og.asRegion(Widget, { wake: 'visible' });

asRegion works with barrels (region identity is keyed on the export name) and runs in the consumer’s app, so it is the answer whenever the mark cannot sit on the import itself, and you ship nothing for it.

Marks inside your package: ogygia.files

Everything above assumes the consumer marks your components. Sometimes the mark belongs on your side instead, such as a Panel.svelte whose search box is an island by design in whatever app it lands in, or a whole route table that you ship for the consumer to spread into their router. Those marks live in files inside node_modules, where the consumer’s build does not normally 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 works like npm’s "files": a bare directory includes everything under it, globs are allowed, and every path must stay inside the package. That is all you write, and the consumer configures nothing. Their ogygia plugin reads the declarations of their direct dependencies and treats your paths exactly like app source. They are scanned and compiled like the app’s own src/ (both .svelte and .ts, so the marked imports of a shipped route table work), kept non-external on the server, and kept out of the dev prebundle.

Only declared packages are scanned, and only their declared paths, so there is no blind walk through node_modules. A dependency that uses marks without a declaration gets a prominent build warning rather than islands that silently never wake.

Declared paths follow these rules:

  • Ship source. Ship raw .svelte, and .ts/.js with the with { … } attributes intact, for the consumer’s pipeline to compile. If you transpile, make sure your tool keeps import attributes.
  • Depend on ogygia. A declared surface is real ogygia code, so list ogygia as a dependency or peer dependency so that its imports resolve from your package. A route table also lists @ogygia/router, and the consumer lists its router() plugin.
  • The placement rules for app code apply unchanged: no content corpora in components, and server-only modules stay server-only.

A shipped route table is then an ordinary 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

Everything above applies to packages that ship plain components. As soon as a package imports from 'ogygia' itself (for example a shared workspace package that uses the drop-in setContext of the context bridge), one rule applies to every build that touches it:

ogygia must 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 final app’s Vite pipeline, and the runtime keeps module-level registries that must be a single instance per app. A lib-mode build must therefore leave the import specifier intact. An ogygia.files package that ships raw source satisfies this automatically, because its specifiers reach the consumer untouched; the rule only matters when a build step bundles.

In a monorepo this spreads further than people expect. If your library builds bundle their workspace dependencies (Vite lib mode does by default), a single import { setContext } from 'ogygia' in one shared package appears in the bundle graph of every package that depends on it. Each of those builds then needs 'ogygia' in its external list as well, even though none of them import ogygia directly. There are two ways to handle this:

  • List it explicitly by adding 'ogygia' next to your existing $app/$env externals in each affected package’s config. This is correct, but repetitive.
  • Externalize all bare imports, which is the usual convention for library builds and covers ogygia, $app/*, $env/* and everything else at once:
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, and that the consumer never imports, is invisible to the consumer’s import graph. The ogygia.files declaration above exists for exactly this case: declare the path, the consumer’s build scans it like its own source, and the island’s client chunk is emitted. ogygia’s own profiler UI uses the same mechanism internally.