@hvniel/vite-plugin-svelte-inline-component
v0.0.17
Published
> Write tiny Svelte components straight inside your JavaScript / TypeScript tests using tagged‑template literals.
Downloads
432
Readme
@hvniel/vite-plugin-svelte-inline-component
Write tiny Svelte components straight inside your JavaScript / TypeScript tests using tagged‑template literals.
📖 Table of Contents
- ✨ What it does
- 🔧 Installation
- 🚀 Usage
- 🧱 Definitions Fence
- 🧩 Named Exports & Snippets
- 🧪 Testing Inline & Reactive Components
- 🛠️ API
- 🧐 How it works (nutshell)
- ⚠️ Caveats
- 📝 License
✨ What it does
The plugin lets you write Svelte components directly in your .ts or .js test files. It finds every template literal whose tag matches a list you choose (defaults to html and svelte), compiles the markup with the Svelte compiler, and replaces it with an import of a virtual module that exports the compiled component. No extra files.
🔧 Installation
pnpm add -D @hvniel/vite-plugin-svelte-inline-component
# or
npm i -D @hvniel/vite-plugin-svelte-inline-component
# or
yarn add -D @hvniel/vite-plugin-svelte-inline-component🚀 Usage
vite.config.ts / vite.config.js
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite";
import inlineSveltePlugin from "@hvniel/vite-plugin-svelte-inline-component/plugin";
export default defineConfig(({ mode }) => ({
plugins: [mode === "test" && inlineSveltePlugin(), sveltekit()],
// ⬑ only enable during vitest runs in this example – remove the condition to run always
}));Why conditionally enable? In a typical SvelteKit project you already compile
.sveltefiles. Turning the plugin on just for unit tests keeps production builds untouched while giving Vitest access to inline components.
🧱 Definitions Fence
To share code across multiple inline components in the same file, wrap your ES imports, variables, and "global" component definitions in a special comment block. The plugin will make everything inside this fence available to each inline component's script block.
/* svelte:definitions
import { tick } from "svelte";
// Available to all inline components in this file
const sharedClass = "text-red-500";
// This component is also globally available
const GlobalButton = html`<button>Click Me!</button>`;
*/
const Thing1 = html`
<script>
let n = $state(0);
</script>
<button class="{sharedClass}" onclick={() => n++}>Button: {n}</button>
<GlobalButton />
`;🔗 Local Scoping with scope(...)
Inline components defined with const X = html\...`are not automatically visible to other inline components. To use one inside another, pass it toscope(...)` right before the tag:
import { html, scope } from "@hvniel/vite-plugin-svelte-inline-component";
const ComponentA = html`<h1>A</h1>`;
// ComponentA is now in scope for ComponentB
const ComponentB = scope(ComponentA).html`<div><ComponentA /></div>`;Notes:
- The tag (
html) must come immediately before the backtick — this keeps syntax highlighting from extensions likelit-htmlworking. - Components passed to
scope(...)take precedence over fence globals with the same name. - A component can only be scoped after it has been defined earlier in the file.
🧩 Named Exports & Snippets
Just like regular Svelte files, you can use <script context="module"> (or <script module>) to export values from an inline component. This is especially useful for Svelte 5 snippets.
The plugin makes any named exports available as properties on the component itself.
import { html, type InlineSnippet } from "@hvniel/vite-plugin-svelte-inline-component";
const ComponentWithSnippets = html`
<script lang="ts" module>
// These snippets will be attached to the component export
export { header, footer };
</script>
{#snippet header(text: string)}
<header>
<h1>{text}</h1>
</header>
{/snippet} {#snippet footer()}
<footer>
<p>© 2025</p>
</footer>
{/snippet}
`;
// Now you can render the component and pass snippets to it
const { header, footer } = ComponentWithSnippets as unknown as {
header: InlineSnippet;
footer: InlineSnippet;
};
const renderer = render(anchor => {
header(anchor, () => "Welcome!");
});Typing Named Exports
To make TypeScript aware of your named exports, you'll need to use a type assertion.
import { html, type InlineSnippet } from "@hvniel/vite-plugin-svelte-inline-component";
const defaultExport = html`
<script module>
export { element };
</script>
{#snippet element(content)}
<strong>{content}</strong>
{/snippet}
`;
// Use `as` to tell TypeScript about the named export
const { element } = defaultExport as unknown as {
element: InlineSnippet<string>;
};
// `element` is now fully typed!🧪 Testing Inline & Reactive Components
The plugin is perfect for writing component tests without creating separate .svelte files. It works great with Vitest and testing libraries like @testing-library/svelte or vitest-browser-svelte.
Here’s a sample test that proves reactivity with Svelte 5 runes works out of the box:
import { render } from "vitest-browser-svelte";
import { html, type InlineSnippet } from "@hvniel/vite-plugin-svelte-inline-component";
it("supports reactive components", async () => {
const ReactiveComponent = html`
<script>
let count = $state(0);
function increment() {
count++;
}
</script>
<button onclick="{increment}">Count: {count}</button>
`;
const { getByRole } = render(ReactiveComponent);
const button = getByRole("button");
expect(button).toHaveTextContent("Count: 0");
await user.click(button);
expect(button).toHaveTextContent("Count: 1");
});🛠️ API
inlineSveltePlugin(options?: InlineSvelteOptions): Plugin;
InlineSvelteOptions
| option | type | default | description |
| :----------- | :--------- | :---------------------- | :----------------------------------------------- |
| tags | string[] | ["html", "svelte"] | Tag names to be treated as inline Svelte markup. |
| fenceStart | string | /* svelte:definitions | The comment that starts a standard import fence. |
| fenceEnd | string | */ | The comment that ends a standard import fence. |
🧐 How it works (nutshell)
The plugin uses a multi-stage process to transform your code:
- Scan for a Definitions Fence: The plugin first looks for a
/* svelte:definitions */fence to identify any shared code (imports, variables, and components). - Process Templates: It then compiles all "local" and "global" inline components, injecting any shared code from the definitions fence into each component's script scope.
- Replace Literals: Finally, it replaces all the original
html\...`` literals in your code with variables that point to the newly created virtual components.
The result behaves just like a normal Svelte component import.
⚠️ Caveats
- The plugin only transforms your application's source code, ignoring anything inside
node_modules. - The template content must be valid Svelte component markup. Syntax errors will surface during compilation.
- Because each inline component gets a unique hash, HMR will re-render the whole component tree containing it. Keep inline components small for best performance.
📝 License
MIT © 2025 Haniel Ubogu
