sveltekit-plugin-system
v0.2.0
Published
> A tiny plugin system for SvelteKit — let self-contained "plugins" inject **components**, run **actions**, and transform data through **filters** at named locations across your app, without touching your core code.
Readme
sveltekit-plugin-system
A tiny plugin system for SvelteKit — let self-contained "plugins" inject components, run actions, and transform data through filters at named locations across your app, without touching your core code.
Think of it as WordPress-style hooks for SvelteKit: your app exposes named locations, and plugins (dropped into a plugins/ folder) hook into them. Works on both the server and the client.
- Requirements: Svelte 5 and SvelteKit 2. (For Svelte 4 / SvelteKit 1, use
0.1.x.)
Table of contents
- Why
- Concepts
- Installation
- Quick start
- Writing a plugin
- The three hook types
- Loading plugins
- API reference
- Tailwind compatibility
- Examples
- Contributing
Why
You want a codebase where features can be added or removed as drop-in folders — for a marketplace, a white-label product, optional modules, or simply to keep concerns separate. Instead of editing shared files every time, you expose extension points and let plugins attach to them.
Concepts
- Location — a named extension point (a plain string, e.g.
"after-content"). - Plugin — a folder containing an
index.ts(orindex.js) whose default export is a function that receives the sharedhooksobject and registers hooks on locations. - Hook — what a plugin attaches to a location. There are three kinds:
- a component to render,
- an action to run (side effects),
- a filter to transform a value.
Installation
npm i -D sveltekit-plugin-systemQuick start
src/
├─ routes/
│ ├─ +layout.ts # register plugins (see "Loading plugins")
│ ├─ +layout.svelte
│ └─ +page.svelte
└─ plugins/
└─ hello/
├─ index.ts # the plugin
└─ Banner.svelte1. Register plugins in src/routes/+layout.ts:
import { loadPlugins } from 'sveltekit-plugin-system';
export const load = () => {
loadPlugins({
plugins: import.meta.glob('../plugins/**/index.(ts|js)', { eager: true })
});
};2. Expose a location in a component (here in +page.svelte):
<script lang="ts">
import { Hook } from 'sveltekit-plugin-system';
</script>
<h1>Home</h1>
<Hook location="after-content" />3. Write a plugin in src/plugins/hello/index.ts:
import type { Plugins } from 'sveltekit-plugin-system';
import Banner from './Banner.svelte';
export default (hooks: Plugins.HookCreateStore) => {
hooks.addComponent('after-content', Banner);
};That's it — Banner now renders wherever <Hook location="after-content" /> appears.
Writing a plugin
A plugin is a folder with an index.ts that default-exports a registration function:
import type { Plugins } from 'sveltekit-plugin-system';
export default (hooks: Plugins.HookCreateStore) => {
// register components / actions / filters here
};Every plugins/**/index.(ts|js) matched by your import.meta.glob is loaded automatically.
The three hook types
1. Components
Render a Svelte component at a location. Any extra props passed to <Hook> are forwarded to
the injected components.
// in a plugin
hooks.addComponent('sidebar', MyWidget);<!-- in your app -->
<Hook location="sidebar" title="Widgets" />
<!-- MyWidget receives { title: "Widgets" } -->2. Actions
Run one or more callbacks registered on a location. Extra arguments are forwarded, and
async handlers are awaited.
// in a plugin
hooks.addAction('user:login', (user) => track('login', user));
hooks.addAction('user:login', async (user) => {
await sendWelcomeEmail(user);
});// in your app
await hooks.doAction('user:login', currentUser);3. Filters
Transform a value by passing it through every filter registered on a location, in
registration order. Extra arguments are forwarded, and async filters are awaited.
// in a plugin
hooks.addFilter('page-title', (title) => `${title} • My Site`);// in your app (e.g. a load function)
const title = await hooks.applyFilter('page-title', 'Home');
// → "Home • My Site"Server-side too — a common pattern is enriching event.locals in hooks.server.ts:
import type { Handle } from '@sveltejs/kit';
import { loadPlugins, hooks } from 'sveltekit-plugin-system';
// Load plugins once, before requests are handled.
loadPlugins({ plugins: import.meta.glob('./plugins/**/index.(ts|js)', { eager: true }) });
export const handle: Handle = async ({ resolve, event }) => {
event.locals = await hooks.applyFilter('server-locals', {});
return resolve(event);
};Loading plugins
Call loadPlugins once. import.meta.glob paths are resolved relative to the file that
calls it.
Recommended — +layout.ts:
import { loadPlugins } from 'sveltekit-plugin-system';
export const load = () => {
loadPlugins({ plugins: import.meta.glob('../plugins/**/index.(ts|js)', { eager: true }) });
};Why
+layout.tsand not+layout.svelte?loadfunctions run before components mount and before childloads, on both the server and the client. Registering plugins there guarantees hooks are available everywhere — including inside pageloadfunctions and for SSR-consistent values. If a pageloadneeds a filter,await parent()first so the layoutloadhas run.Registering plugins inside a component
<script>also works, but only after mount, so client-sideloadfunctions won't see them yet.
Load plugins server-side in hooks.server.ts (module scope) if you use server hooks such as
server-locals — see the Filters example.
ℹ️ Adding or removing a server-side hook requires restarting/rebuilding your app.
API reference
Import the shared store and helpers from the package:
import { loadPlugins, hooks, Hook, createHooksStore } from 'sveltekit-plugin-system';
import type { Plugins } from 'sveltekit-plugin-system';loadPlugins(options?)
Runs every plugin's default export and marks the store initialized (after which hooks can no longer be registered).
| Option | Type | Description |
| --------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| plugins | Record<string, { default? }> | Plugin modules, typically import.meta.glob(..., { eager: true }). Defaults to globbing plugins/**/index.(ts\|js). |
hooks
The shared singleton used by plugins and your app.
| Member | Description |
| -------------------------------------- | ---------------------------------------------------------------------------------------- |
| addComponent(location, component) | Register a component at location. |
| addAction(location, callback) | Register an action callback at location. |
| doAction(location, ...args) | Run all actions at location (awaits async ones). Returns Promise<void>. |
| addFilter(location, filter) | Register a filter at location. |
| applyFilter(location, data, ...args) | Pass data through all filters at location (awaits async ones). Returns Promise<T>. |
| components / actions / filters | Reactive read-only accessors (Svelte 5 runes) of the registered hooks. |
| initialized | true once loadPlugins has run. |
<Hook location=... {...props} />
Renders every component registered at location, forwarding any extra props.
createHooksStore()
Creates an isolated store instance (same shape as hooks). Useful for tests or advanced setups.
Deprecated: $hooks store
For backwards compatibility, hooks still implements Svelte's Writable contract
(subscribe / set / update), so $hooks auto-subscription keeps working:
<script>
import { hooks } from 'sveltekit-plugin-system';
// legacy — prefer the reactive `hooks.components` accessor instead
$: locations = Object.keys($hooks.components);
</script>Prefer the runes accessors (hooks.components, hooks.actions, hooks.filters) in new code.
Tailwind compatibility
Tailwind needs to know where your plugin components live to compile their classes:
// tailwind.config.js
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
'./src/plugins/**/*.{html,js,svelte,ts}' // your plugins path
]
};Examples
A complete, runnable SvelteKit app demonstrating every feature (components, actions, sync/async,
filters, server-side filters, the runes read API, and the deprecated $hooks store) lives in
examples/basic.
Contributing
npm install
npm run check # type-check
npm run lint # prettier + eslint
npm test # vitest (unit + regression)
npm run build # build + publint