Skip to content

Commit 9d594d1

Browse files
committed
Update package version to 0.0.8, enhance README with a new section on global components, and improve inline Svelte plugin to support global component declarations and better script content injection.
1 parent 6320d88 commit 9d594d1

File tree

4 files changed

+193
-70
lines changed

4 files changed

+193
-70
lines changed

README.md

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,19 @@
66

77
## 📖 Table of Contents
88

9-
- [✨ What it does](https://www.google.com/search?q=%23-what-it-does)
10-
- [🔧 Installation](https://www.google.com/search?q=%23-installation)
11-
- [🚀 Usage](https://www.google.com/search?q=%23-usage)
12-
- [vite.config.ts / vite.config.js](https://www.google.com/search?q=%23viteconfigts--viteconfigjs)
13-
- [Declaring the global tag helper](https://www.google.com/search?q=%23declaring-the-global-tag-helper)
14-
- [🧩 Named Exports & Snippets](https://www.google.com/search?q=%23-named-exports--snippets)
15-
- [Typing Named Exports](https://www.google.com/search?q=%23typing-named-exports)
16-
- [🧪 Testing Inline & Reactive Components](https://www.google.com/search?q=%23-testing-inline--reactive-components)
17-
- [🚦 Import Fences](https://www.google.com/search?q=%23-import-fences)
18-
- [🛠️ API](https://www.google.com/search?q=%23-api)
19-
- [`InlineSvelteOptions`](https://www.google.com/search?q=%23inlinesvelteoptions)
20-
- [🧐 How it works (nutshell)](https://www.google.com/search?q=%23-how-it-works-nutshell)
21-
- [⚠️ Caveats](https://www.google.com/search?q=%23-caveats)
22-
- [📝 License](https://www.google.com/search?q=%23-license)
9+
- [✨ What it does](#-what-it-does)
10+
- [🔧 Installation](#-installation)
11+
- [🚀 Usage](#-usage)
12+
- [vite.config.ts / vite.config.js](#viteconfigts--viteconfigjs)
13+
- [🌍 Global Components](#-global-components)
14+
- [🚦 Import Fences](#-import-fences)
15+
- [🧩 Named Exports & Snippets](#-named-exports--snippets)
16+
- [🧪 Testing Inline & Reactive Components](#-testing-inline--reactive-components)
17+
- [🛠️ API](#️-api)
18+
- [`InlineSvelteOptions`](#inlinesvelteoptions)
19+
- [🧐 How it works (nutshell)](#-how-it-works-nutshell)
20+
- [⚠️ Caveats](#️-caveats)
21+
- [📝 License](#-license)
2322

2423
---
2524

@@ -59,6 +58,27 @@ export default defineConfig(({ mode }) => ({
5958
6059
---
6160

61+
## 🌍 Global Components
62+
63+
You can define components inside a special `/* svelte:globals */` fence to make them automatically available to all other inline components in the same file. This is perfect for defining shared UI elements or mocks without manual imports.
64+
65+
```typescript
66+
/* svelte:globals
67+
// Any component defined here is "global" to this file.
68+
const GlobalButton = html`<button on:click>Click Me!</button>`;
69+
*/
70+
71+
// ✅ GlobalButton is now available here automatically.
72+
const Page = html`
73+
<section>
74+
<p>Welcome to the page.</p>
75+
<GlobalButton />
76+
</section>
77+
`;
78+
```
79+
80+
---
81+
6282
## 🧩 Named Exports & Snippets
6383

6484
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**.
@@ -181,37 +201,37 @@ const Thing1 = html`
181201

182202
## 🛠️ API
183203

184-
```ts
185-
inlineSveltePlugin(options?: InlineSvelteOptions): Plugin;
186-
```
204+
`inlineSveltePlugin(options?: InlineSvelteOptions): Plugin;`
187205

188206
### `InlineSvelteOptions`
189207

190-
| option | type | default | description |
191-
| :----------- | :--------- | :------------------- | :--------------------------------------------------------------------- |
192-
| `tags` | `string[]` | `["html", "svelte"]` | Names of template‑tags that should be treated as inline Svelte markup. |
193-
| `fenceStart` | `string` | `/* svelte:imports` | The comment that _starts_ an import fence. |
194-
| `fenceEnd` | `string` | `*/` | The comment that _ends_ an import fence. |
208+
| option | type | default | description |
209+
| :------------------ | :--------- | :------------------- | :------------------------------------------------- |
210+
| `tags` | `string[]` | `["html", "svelte"]` | Tag names to be treated as inline Svelte markup. |
211+
| `fenceStart` | `string` | `/* svelte:imports` | The comment that starts a standard import fence. |
212+
| `globalsFenceStart` | `string` | `/* svelte:globals` | The comment that starts a global components fence. |
213+
| `fenceEnd` | `string` | `*/` | The comment that ends any fence. |
195214

196215
---
197216

198217
## 🧐 How it works (nutshell)
199218

200-
1. **Scan** each user source file (`.js`, `.ts`, `.jsx`, `.tsx`).
201-
2. **Match** template literals whose tag name is in `options.tags`.
202-
3. **Hash** the template content (including any fenced imports) to create a stable virtual module ID.
203-
4. **Replace** the literal with a variable that imports the virtual module. Named exports (like snippets) are merged onto the default component export using `Object.assign`.
204-
5. **Compile** the markup with Svelte when Vite requests the virtual module ID.
219+
The plugin uses a multi-stage process to transform your code:
220+
221+
1. **Scan for Fences:** The plugin first looks for `/* svelte:globals */` and `/* svelte:imports */` fences to identify shared components and imports.
222+
2. **Process Globals:** It compiles any components found inside the `globals` fence.
223+
3. **Process Locals:** It then compiles the remaining "local" components, injecting the standard imports and the compiled global components into each one's script scope.
224+
4. **Replace Literals:** Finally, it replaces all the original `html\`...\`\` literals in your code with variables that point to the newly created virtual components.
205225

206-
The result behaves just like a normal Svelte component import, so SSR, hydration, and Hot Module Replacement work as expected.
226+
The result behaves just like a normal Svelte component import.
207227

208228
---
209229

210230
## ⚠️ Caveats
211231

212232
- The plugin only transforms your application's source code, ignoring anything inside `node_modules`.
213233
- The template content must be valid Svelte component markup. Syntax errors will surface during compilation.
214-
- Because each inline component gets a unique hash, HMR will rerender the whole component tree containing it. Keep inline components small for best performance.
234+
- 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.
215235

216236
---
217237

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hvniel/vite-plugin-svelte-inline-component",
3-
"version": "0.0.7",
3+
"version": "0.0.8",
44
"license": "MIT",
55
"author": "Haniel Ubogu <https://github.com/HanielU>",
66
"repository": {

src/lib/plugin/index.ts

Lines changed: 125 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,34 @@ import MagicString from "magic-string";
44
import type { Plugin } from "vite";
55

66
export interface InlineSvelteOptions {
7-
/** Templatetag names treated as Svelte markup – default `["html", "svelte"]` */
7+
/** Template-tag names treated as Svelte markup. Default: `["html", "svelte"]` */
88
tags?: string[];
9-
/** Comment that *starts* an import fence – default `/* svelte:imports` */
9+
/** Comment that *starts* an import fence. Default: `/* svelte:imports` */
1010
fenceStart?: string;
11-
/** Comment that *ends* an import fence – default `*\/` */
11+
/** Comment that *ends* an import fence. Default: `*\/` */
1212
fenceEnd?: string;
13+
/** Comment that *starts* a global components fence. Default: `/* svelte:globals` */
14+
globalsFenceStart?: string;
1315
}
1416

1517
/* ───────── helpers ───────── */
1618

1719
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1820
const isUserSource = (id: string) => !id.includes("/node_modules/") && /\.(c?[tj]sx?)$/.test(id);
1921

20-
/** Inject shared imports without duplicating instance `<script>` blocks */
21-
function applyImports(markup: string, imports: string): string {
22-
if (!imports) return markup;
22+
/** Injects a string of script content into a Svelte component's markup. */
23+
function applyScriptContent(markup: string, scriptContent: string): string {
24+
if (!scriptContent) return markup;
2325

2426
const scriptRE = /<script(?![^>]*context=["']module["'])[^>]*>/i;
25-
const m = scriptRE.exec(markup);
26-
if (m) {
27-
const idx = m.index + m[0].length;
28-
return markup.slice(0, idx) + "\n" + imports + "\n" + markup.slice(idx);
27+
const match = scriptRE.exec(markup);
28+
29+
if (match) {
30+
const idx = match.index + match[0].length;
31+
return `${markup.slice(0, idx)}\n${scriptContent}\n${markup.slice(idx)}`;
32+
} else {
33+
return `<script>\n${scriptContent}\n</script>\n${markup}`;
2934
}
30-
return `<script>\n${imports}\n</script>\n` + markup;
3135
}
3236

3337
/* ───────── plugin ───────── */
@@ -36,68 +40,149 @@ export default function inlineSveltePlugin({
3640
tags = ["html", "svelte"],
3741
fenceStart = "/* svelte:imports",
3842
fenceEnd = "*/",
43+
globalsFenceStart = "/* svelte:globals",
3944
}: InlineSvelteOptions = {}): Plugin {
4045
const tagGroup = tags.map(esc).join("|");
41-
const tplRE = new RegExp(`(?:${tagGroup})\\s*\`([\\s\\S]*?)\``, "g");
42-
const fenceRE = new RegExp(`${esc(fenceStart)}([\\s\\S]*?)${esc(fenceEnd)}`, "m");
46+
const tplRE = new RegExp(
47+
`(?:const|let|var)\\s+([a-zA-Z0-9_$]+)\\s*=\\s*(?:${tagGroup})\\s*\`([\\s\\S]*?)\``,
48+
"g"
49+
);
50+
const importsFenceRE = new RegExp(`${esc(fenceStart)}([\\s\\S]*?)${esc(fenceEnd)}`, "m");
51+
const globalsFenceRE = new RegExp(`${esc(globalsFenceStart)}([\\s\\S]*?)${esc(fenceEnd)}`, "m");
4352

44-
const VIRT = "virtual:inline-svelte/";
45-
const RSLV = "\0" + VIRT;
53+
const VIRT_PREFIX = "virtual:inline-svelte/";
54+
const RSLV_PREFIX = `\0${VIRT_PREFIX}`;
4655

47-
/** virtualId → full markup (with injected imports) */
4856
const cache = new Map<string, string>();
4957

5058
return {
51-
name: "@hvniel/vite-plugin-svelte-inline-component",
59+
name: "@hvniel/vite-plugin-svelte-inline-component-v3",
5260
enforce: "pre",
5361

5462
transform(code, id) {
55-
if (!isUserSource(id)) return;
56-
57-
/* file‑level shared imports (may be empty) */
58-
const imports = (fenceRE.exec(code)?.[1] ?? "").trim();
63+
if (!isUserSource(id) || !tags.some(tag => code.includes(tag))) {
64+
return;
65+
}
5966

6067
const ms = new MagicString(code);
61-
const hashToLocal = new Map<string, string>(); // dedupe within THIS pass
62-
let edited = false,
63-
m: RegExpExecArray | null;
68+
let edited = false;
69+
70+
// === STAGE 1: Process `svelte:imports` fence ===
71+
const importsMatch = importsFenceRE.exec(code);
72+
const standardImports = (importsMatch?.[1] ?? "").trim();
73+
74+
// === STAGE 2: Process `svelte:globals` fence ===
75+
const globalsMatch = globalsFenceRE.exec(code);
76+
// This will hold the full script block (imports + consts) for global components
77+
let globalScriptBlock = "";
78+
const hashToLocal = new Map<string, string>();
79+
80+
if (globalsMatch) {
81+
const globalsContent = globalsMatch[1];
82+
const globalsStartIndex = globalsMatch.index;
83+
let match: RegExpExecArray | null;
84+
85+
tplRE.lastIndex = 0; // Reset regex before scanning fence content
86+
while ((match = tplRE.exec(globalsContent))) {
87+
const [fullMatch, componentName, rawMarkup] = match;
88+
89+
const finalMarkup = applyScriptContent(rawMarkup, standardImports);
90+
const hash = createHash("sha1").update(finalMarkup).digest("hex").slice(0, 8);
91+
92+
let local = hashToLocal.get(hash);
93+
if (!local) {
94+
local = `_Inline_${hash}`;
95+
hashToLocal.set(hash, local);
96+
97+
const virtId = `${VIRT_PREFIX}${hash}.js`;
98+
if (!cache.has(virtId)) cache.set(virtId, finalMarkup);
99+
100+
const ns = `_InlineNS_${hash}`;
101+
const importStatement = `import * as ${ns} from '${virtId}';`;
102+
const localDefStatement = `const ${local} = Object.assign(${ns}.default, ${ns});`;
103+
104+
// Prepend the import to the top of the file for resolution
105+
ms.prepend(`${importStatement}\n${localDefStatement}\n`);
106+
107+
// **FIX**: Build a full script block to inject into local components
108+
// This includes the necessary import and the const declarations.
109+
const aliasStatement = `const ${componentName} = ${local};`;
110+
globalScriptBlock += `${importStatement}\n${localDefStatement}\n${aliasStatement}\n`;
111+
} else {
112+
// If the component was already processed, just add its alias declaration
113+
globalScriptBlock += `const ${componentName} = ${local};\n`;
114+
}
115+
116+
ms.overwrite(
117+
globalsStartIndex + match.index,
118+
globalsStartIndex + match.index + fullMatch.length,
119+
`const ${componentName} = ${local};`
120+
);
121+
edited = true;
122+
}
123+
ms.remove(globalsStartIndex, globalsStartIndex + globalsMatch[0].length);
124+
}
64125

65-
while ((m = tplRE.exec(code))) {
66-
const rawMarkup = m[1];
67-
const markup = applyImports(rawMarkup, imports);
68-
const hash = createHash("sha1").update(markup).digest("hex").slice(0, 8);
126+
// === STAGE 3: Process remaining "local" inline components ===
127+
let match: RegExpExecArray | null;
128+
tplRE.lastIndex = 0;
129+
130+
while ((match = tplRE.exec(code))) {
131+
if (
132+
globalsMatch &&
133+
match.index >= globalsMatch.index &&
134+
match.index < globalsMatch.index + globalsMatch[0].length
135+
) {
136+
continue;
137+
}
138+
139+
const [fullMatch, componentName, rawMarkup] = match;
140+
141+
// Inject standard imports AND the full script block for global components
142+
const scriptToInject = `${standardImports}\n${globalScriptBlock}`;
143+
const finalMarkup = applyScriptContent(rawMarkup, scriptToInject);
144+
145+
const hash = createHash("sha1").update(finalMarkup).digest("hex").slice(0, 8);
69146

70-
/* reuse the same local var if this hash appears again in the file */
71147
let local = hashToLocal.get(hash);
72148
if (!local) {
73-
local = `Inline_${hash}`;
149+
local = `_Inline_${hash}`;
74150
hashToLocal.set(hash, local);
75151

76-
const virt = `${VIRT}${hash}.js`;
77-
if (!cache.has(virt)) cache.set(virt, markup);
152+
const virtId = `${VIRT_PREFIX}${hash}.js`;
153+
if (!cache.has(virtId)) cache.set(virtId, finalMarkup);
78154

79-
const ns = `__InlineNS_${hash}`;
155+
const ns = `_InlineNS_${hash}`;
80156
ms.prepend(
81-
`import * as ${ns} from '${virt}';\n` +
82-
`const ${local}=Object.assign(${ns}.default, ${ns});\n`
157+
`import * as ${ns} from '${virtId}';\n` +
158+
`const ${local} = Object.assign(${ns}.default, ${ns});\n`
83159
);
84160
}
85161

86-
ms.overwrite(m.index, tplRE.lastIndex, local);
162+
ms.overwrite(
163+
match.index,
164+
match.index + fullMatch.length,
165+
`const ${componentName} = ${local};`
166+
);
87167
edited = true;
88168
}
89169

90-
return edited ? { code: ms.toString(), map: ms.generateMap({ hires: true }) } : null;
170+
if (!edited) return null;
171+
172+
return {
173+
code: ms.toString(),
174+
map: ms.generateMap({ hires: true }),
175+
};
91176
},
92177

93178
resolveId(id) {
94-
return id.startsWith(VIRT) ? RSLV + id.slice(VIRT.length) : undefined;
179+
return id.startsWith(VIRT_PREFIX) ? RSLV_PREFIX + id.slice(VIRT_PREFIX.length) : undefined;
95180
},
96181

97182
load(id) {
98-
if (!id.startsWith(RSLV)) return;
183+
if (!id.startsWith(RSLV_PREFIX)) return;
99184

100-
const markup = cache.get(VIRT + id.slice(RSLV.length))!;
185+
const markup = cache.get(VIRT_PREFIX + id.slice(RSLV_PREFIX.length))!;
101186
return compile(markup, {
102187
generate: "client",
103188
css: "injected",

src/plugin.svelte.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { page } from "@vitest/browser/context";
88
import { MemoryRouter, Routes, Route } from "@hvniel/svelte-router";
99
*/
1010

11+
/* svelte:globals
12+
const Frank = html`<h1>Frank</h1>`;
13+
*/
14+
1115
describe("Inline Svelte Components with sv", () => {
1216
it("renders a simple component", () => {
1317
const SimpleComponent = html`<h1>Hello World</h1>`;
@@ -193,4 +197,18 @@ describe("Inline Svelte Components with sv", () => {
193197
</header>
194198
`);
195199
});
200+
201+
it("supports global components", () => {
202+
const Page = html`<div><Frank /></div>`;
203+
const renderer = render(Page);
204+
205+
expect(renderer.container.firstElementChild).toMatchInlineSnapshot(`
206+
<div>
207+
<h1>
208+
Frank
209+
</h1>
210+
<!---->
211+
</div>
212+
`);
213+
});
196214
});

0 commit comments

Comments
 (0)