Skip to content

Commit 110c094

Browse files
committed
Update package version to 0.0.12, add support for referencing global components in tests, and refactor global component handling in the inline Svelte plugin for improved stability and clarity.
1 parent 4912430 commit 110c094

File tree

3 files changed

+139
-46
lines changed

3 files changed

+139
-46
lines changed

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.11",
3+
"version": "0.0.12",
44
"license": "MIT",
55
"author": "Haniel Ubogu <https://github.com/HanielU>",
66
"repository": {

src/lib/plugin/index.ts

Lines changed: 114 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ function applyImports(markup: string, imports: string): string {
3434
return `<script>\n${imports}\n</script>\n` + markup;
3535
}
3636

37+
function getTransitiveDependencies(
38+
entryPoints: Set<string>,
39+
dependencyGraph: Map<string, Set<string>>
40+
): Set<string> {
41+
const resolved = new Set<string>();
42+
const queue = [...entryPoints];
43+
44+
while (queue.length > 0) {
45+
const current = queue.shift()!;
46+
if (resolved.has(current)) continue;
47+
48+
resolved.add(current);
49+
const dependencies = dependencyGraph.get(current);
50+
if (dependencies) {
51+
for (const dep of dependencies) {
52+
queue.push(dep);
53+
}
54+
}
55+
}
56+
return resolved;
57+
}
58+
3759
/* ───────── plugin ───────── */
3860

3961
export default function inlineSveltePlugin({
@@ -47,16 +69,15 @@ export default function inlineSveltePlugin({
4769
const tplRE = new RegExp(`(?:${tagGroup})\\s*\`([\\s\\S]*?)\``, "g");
4870
const fenceRE = new RegExp(`${esc(fenceStart)}([\\s\\S]*?)${esc(fenceEnd)}`, "m");
4971
const globalsRE = new RegExp(`${esc(globalsStart)}([\\s\\S]*?)${esc(globalsEnd)}`, "m");
50-
// FIX: Simplified the regex to remove a problematic negative lookahead that was preventing multiple matches.
51-
const globalDefWithTplRE = new RegExp(
52-
`(const\\s+([a-zA-Z0-9_$]+)\\s*=\\s*)((?:${tagGroup})\\s*\`[\\s\\S]*?\`)`,
53-
"g"
72+
// FIX: New, safer regex anchored to the start of a line.
73+
const globalDefRE = new RegExp(
74+
`^const\\s+([a-zA-Z0-9_$]+)\\s*=\\s*(?:${tagGroup})\\s*(\`[\\s\\S]*?\`)`,
75+
"gm"
5476
);
5577

5678
const VIRT = "virtual:inline-svelte/";
5779
const RSLV = "\0" + VIRT;
5880

59-
/** virtualId → full markup (with injected imports) */
6081
const cache = new Map<string, string>();
6182

6283
return {
@@ -74,8 +95,8 @@ export default function inlineSveltePlugin({
7495
let importsToAdd = "";
7596
let globalImportsForTpl = "";
7697
let hoistedCode = "";
77-
const hashToLocal = new Map<string, string>();
7898
const globalVarDefs = new Map<string, string>();
99+
const hashToLocal = new Map<string, string>();
79100
const globalComponentNames = new Set<string>();
80101

81102
/* 1. Process globals */
@@ -84,42 +105,86 @@ export default function inlineSveltePlugin({
84105
edited = true;
85106
const globalsContent = globalsMatch[1] ?? "";
86107
ms.overwrite(globalsMatch.index, globalsMatch.index + globalsMatch[0].length, "");
108+
hoistedCode = globalsContent;
87109

88-
globalDefWithTplRE.lastIndex = 0;
89-
90-
hoistedCode = globalsContent.replace(
91-
globalDefWithTplRE,
92-
(match, declaration: string, compName: string, templateLiteral: string) => {
93-
globalComponentNames.add(compName);
94-
const rawMarkup = templateLiteral.match(/`([\s\S]*?)`/)[1];
95-
const markup = applyImports(rawMarkup, imports);
96-
const hash = createHash("sha1").update(markup).digest("hex").slice(0, 8);
97-
98-
let local = hashToLocal.get(hash);
99-
if (!local) {
100-
local = `Inline_${hash}`;
101-
hashToLocal.set(hash, local);
102-
const virt = `${VIRT}${hash}.js`;
103-
if (!cache.has(virt)) cache.set(virt, markup);
104-
const ns = `__InlineNS_${hash}`;
105-
importsToAdd += `import * as ${ns} from '${virt}';\nconst ${local}=Object.assign(${ns}.default, ${ns});\n`;
106-
globalImportsForTpl += `import ${compName} from '${virt}';\n`;
107-
}
108-
return `${declaration}${local}`;
110+
// FIX: Refactored component processing to use a more stable while/exec loop.
111+
const replacements = [];
112+
let match;
113+
globalDefRE.lastIndex = 0;
114+
while ((match = globalDefRE.exec(globalsContent)) !== null) {
115+
const [fullMatch, compName, templateLiteralWithTicks] = match;
116+
globalComponentNames.add(compName);
117+
118+
const rawMarkup = templateLiteralWithTicks.slice(1, -1);
119+
const markup = applyImports(rawMarkup, imports);
120+
const hash = createHash("sha1").update(markup).digest("hex").slice(0, 8);
121+
122+
let local = hashToLocal.get(hash);
123+
if (!local) {
124+
local = `Inline_${hash}`;
125+
hashToLocal.set(hash, local);
126+
const virt = `${VIRT}${hash}.js`;
127+
if (!cache.has(virt)) cache.set(virt, markup);
128+
const ns = `__InlineNS_${hash}`;
129+
importsToAdd += `import * as ${ns} from '${virt}';\nconst ${local}=Object.assign(${ns}.default, ${ns});\n`;
130+
globalImportsForTpl += `import ${compName} from '${virt}';\n`;
109131
}
110-
);
111132

112-
const varLines = hoistedCode
113-
.split("\n")
114-
.filter(line => line.trim().match(/^(const|let|var)\s/));
115-
for (const line of varLines) {
116-
const nameMatch = line.match(/(?:const|let|var)\s+([a-zA-Z0-9_$]+)/);
117-
if (nameMatch) {
118-
globalVarDefs.set(nameMatch[1], line);
133+
replacements.push({
134+
start: match.index,
135+
end: match.index + fullMatch.length,
136+
newText: `const ${compName} = ${local}`,
137+
});
138+
}
139+
140+
// Apply replacements from end to start to avoid index shifting
141+
for (const rep of replacements.reverse()) {
142+
hoistedCode = hoistedCode.slice(0, rep.start) + rep.newText + hoistedCode.slice(rep.end);
143+
}
144+
145+
const startOfDeclRegex = /^(?:const|let|var)\s+([a-zA-Z0-9_$]+)/gm;
146+
let declMatch;
147+
while ((declMatch = startOfDeclRegex.exec(hoistedCode)) !== null) {
148+
const varName = declMatch[1];
149+
const startIndex = declMatch.index;
150+
let braceDepth = 0,
151+
bracketDepth = 0,
152+
parenDepth = 0,
153+
endIndex = -1;
154+
for (let i = startIndex; i < hoistedCode.length; i++) {
155+
const char = hoistedCode[i];
156+
if (char === "{") braceDepth++;
157+
else if (char === "}") braceDepth--;
158+
else if (char === "[") bracketDepth++;
159+
else if (char === "]") bracketDepth--;
160+
else if (char === "(") parenDepth++;
161+
else if (char === ")") parenDepth--;
162+
else if (char === ";" && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) {
163+
endIndex = i + 1;
164+
break;
165+
}
119166
}
167+
if (endIndex === -1) {
168+
const nextNewline = hoistedCode.indexOf("\n", startIndex);
169+
endIndex = nextNewline !== -1 ? nextNewline : hoistedCode.length;
170+
}
171+
const definition = hoistedCode.substring(startIndex, endIndex).trim();
172+
if (definition) globalVarDefs.set(varName, definition);
173+
startOfDeclRegex.lastIndex = endIndex;
120174
}
121175
}
122176

177+
const depGraph = new Map<string, Set<string>>();
178+
const allGlobalNames = [...globalVarDefs.keys()];
179+
for (const [name, definition] of globalVarDefs.entries()) {
180+
const dependencies = new Set<string>();
181+
for (const depName of allGlobalNames) {
182+
if (name === depName) continue;
183+
if (new RegExp(`\\b${depName}\\b`).test(definition)) dependencies.add(depName);
184+
}
185+
depGraph.set(name, dependencies);
186+
}
187+
123188
/* 2. Process all regular templates */
124189
let m: RegExpExecArray | null;
125190
tplRE.lastIndex = 0;
@@ -128,22 +193,26 @@ export default function inlineSveltePlugin({
128193
globalsMatch &&
129194
m.index >= globalsMatch.index &&
130195
m.index < globalsMatch.index + globalsMatch[0].length
131-
) {
196+
)
132197
continue;
133-
}
134198

135199
const rawMarkup = m[1];
136-
const scriptContentRE = /<script.*?>([\s\S]*?)<\/script>/;
137-
const existingScriptContent = rawMarkup.match(scriptContentRE)?.[1] ?? "";
200+
const directDeps = new Set<string>();
201+
for (const name of allGlobalNames) {
202+
if (new RegExp(`\\b${name}\\b`).test(rawMarkup)) directDeps.add(name);
203+
}
204+
const allDeps = getTransitiveDependencies(directDeps, depGraph);
138205
let scriptToInject = globalImportsForTpl;
206+
const existingScriptContent = rawMarkup.match(/<script.*?>([\s\S]*?)<\/script>/)?.[1] ?? "";
207+
const sortedDeps = [...allDeps].sort(
208+
(a, b) => allGlobalNames.indexOf(a) - allGlobalNames.indexOf(b)
209+
);
139210

140-
for (const [name, definition] of globalVarDefs.entries()) {
211+
for (const name of sortedDeps) {
141212
if (globalComponentNames.has(name)) continue;
142-
143-
const isUsedInTemplate = new RegExp(`\\b${name}\\b`).test(rawMarkup);
144-
if (isUsedInTemplate && !existingScriptContent.includes(name)) {
145-
scriptToInject += `\n${definition}`;
146-
}
213+
if (existingScriptContent.includes(name)) continue;
214+
const definition = globalVarDefs.get(name);
215+
if (definition) scriptToInject += `\n${definition}`;
147216
}
148217

149218
const markupWithGlobals = applyImports(rawMarkup, scriptToInject);

src/plugin.svelte.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ import { MemoryRouter, Routes, Route } from "@hvniel/svelte-router";
1212
const Frank = html`<h1>Frank</h1>`;
1313
const James = html`<h1>James</h1>`;
1414
const count = $state(0);
15+
const dupes = [
16+
{
17+
name: "John",
18+
comp: James,
19+
},
20+
{
21+
name: "Jane",
22+
comp: James,
23+
},
24+
];
25+
const dupe = dupes[0];
1526
// sg
1627

1728
describe("Inline Svelte Components with sv", () => {
@@ -246,4 +257,17 @@ describe("Inline Svelte Components with sv", () => {
246257
</p>
247258
`);
248259
});
260+
261+
it("allows global vars to reference global components", () => {
262+
const renderer = render(html`<div><dupe.comp /></div>`);
263+
264+
expect(renderer.container.firstElementChild).toMatchInlineSnapshot(`
265+
<div>
266+
<h1>
267+
James
268+
</h1>
269+
<!---->
270+
</div>
271+
`);
272+
});
249273
});

0 commit comments

Comments
 (0)