Skip to content

Commit cf0a73f

Browse files
authored
Merge pull request #7 from gadget-inc/clearer-module-ids
Use clearer module IDs to disambiguate between JS / HTML entrypoints
2 parents 907a432 + d0910e5 commit cf0a73f

File tree

6 files changed

+54
-54
lines changed

6 files changed

+54
-54
lines changed

spec/chatgpt-widgets.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ describe("vite-plugin-chatgpt-widgets", () => {
1616
expect(html).toContain('<meta charset="UTF-8" />');
1717
expect(html).toContain("<title>TestWidget Widget</title>");
1818
expect(html).toContain('<div id="root"></div>');
19-
expect(html).toContain('<script type="module" src="virtual:chatgpt-widget-TestWidget.js"></script>');
19+
expect(html).toContain('<script type="module" src="virtual:chatgpt-widget-entrypoint-TestWidget.js"></script>');
2020
});
2121

2222
it("should use virtual: protocol for the script src", () => {
2323
const html = generateWidgetEntrypointHTML("MyWidget");
24-
expect(html).toContain('src="virtual:chatgpt-widget-MyWidget.js"');
24+
expect(html).toContain('src="virtual:chatgpt-widget-entrypoint-MyWidget.js"');
2525
});
2626

2727
it("should properly escape widget names in titles", () => {
@@ -34,15 +34,15 @@ describe("vite-plugin-chatgpt-widgets", () => {
3434
it("should resolve virtual HTML entrypoints", () => {
3535
const plugin = chatGPTWidgetPlugin();
3636

37-
const result = (plugin as any).resolveId("virtual:chatgpt-widget-Test.html");
38-
expect(result).toBe("virtual:chatgpt-widget-Test.html");
37+
const result = (plugin as any).resolveId("virtual:chatgpt-widget-html-Test.html");
38+
expect(result).toBe("virtual:chatgpt-widget-html-Test.html");
3939
});
4040

4141
it("should resolve virtual JS entrypoints with null byte prefix", () => {
4242
const plugin = chatGPTWidgetPlugin();
4343

44-
const result = (plugin as any).resolveId("virtual:chatgpt-widget-Test.js");
45-
expect(result).toBe("\0virtual:chatgpt-widget-Test.js");
44+
const result = (plugin as any).resolveId("virtual:chatgpt-widget-entrypoint-Test.js");
45+
expect(result).toBe("\0virtual:chatgpt-widget-entrypoint-Test.js");
4646
});
4747

4848
it("should return null for non-virtual modules", () => {
@@ -56,16 +56,16 @@ describe("vite-plugin-chatgpt-widgets", () => {
5656
const plugin = chatGPTWidgetPlugin();
5757

5858
expect((plugin as any).resolveId("virtual:chatgpt-widget")).toBeNull();
59-
expect((plugin as any).resolveId("chatgpt-widget-Test.html")).toBeNull();
60-
expect((plugin as any).resolveId("virtual:chatgpt-widget-Test.css")).toBeNull();
59+
expect((plugin as any).resolveId("chatgpt-widget-html-Test.html")).toBeNull();
60+
expect((plugin as any).resolveId("virtual:chatgpt-widget-entrypoint-Test.css")).toBeNull();
6161
});
6262
});
6363

6464
describe("chatGPTWidgetPlugin - load hook", () => {
6565
it("should load virtual HTML files using generateWidgetEntrypointHTML", async () => {
6666
const plugin = chatGPTWidgetPlugin();
6767

68-
const result = await (plugin as any).load("virtual:chatgpt-widget-TestWidget.html");
68+
const result = await (plugin as any).load("virtual:chatgpt-widget-html-TestWidget.html");
6969

7070
// Should use the same HTML generation function
7171
const expected = generateWidgetEntrypointHTML("TestWidget");
@@ -83,7 +83,7 @@ describe("vite-plugin-chatgpt-widgets", () => {
8383
const plugin = chatGPTWidgetPlugin();
8484

8585
expect(await (plugin as any).load("virtual:other-module")).toBeNull();
86-
expect(await (plugin as any).load("virtual:chatgpt-widget-Test.css")).toBeNull();
86+
expect(await (plugin as any).load("virtual:chatgpt-widget-entrypoint-Test.js")).toBeNull();
8787
});
8888
});
8989

spec/integration/basic-project.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe("Basic Project Integration", () => {
4646

4747
// Check that the script tag is present and uses absolute URL
4848
expect(html).toContain('<script type="module"');
49-
expect(html).toContain("https://example.com/@id/virtual:chatgpt-widget-TestWidget.js");
49+
expect(html).toContain("https://example.com/@id/virtual:chatgpt-widget-entrypoint-TestWidget.js");
5050
});
5151

5252
it("should generate different HTML for different widgets in dev mode", async () => {
@@ -56,16 +56,16 @@ describe("Basic Project Integration", () => {
5656
expect(testWidgetHtml).toContain("TestWidget Widget");
5757
expect(anotherWidgetHtml).toContain("AnotherWidget Widget");
5858

59-
expect(testWidgetHtml).toContain("/@id/virtual:chatgpt-widget-TestWidget.js");
60-
expect(anotherWidgetHtml).toContain("/@id/virtual:chatgpt-widget-AnotherWidget.js");
59+
expect(testWidgetHtml).toContain("/@id/virtual:chatgpt-widget-entrypoint-TestWidget.js");
60+
expect(anotherWidgetHtml).toContain("/@id/virtual:chatgpt-widget-entrypoint-AnotherWidget.js");
6161
});
6262

6363
it("should generate HTML even for non-existent widgets in dev mode", async () => {
6464
// In dev mode, the HTML is always generated
6565
// The error only occurs when the JS module is loaded by the browser
6666
const { content: html } = await getWidgetHTML("NonExistentWidget", { devServer });
6767
expect(html).toContain("NonExistentWidget Widget");
68-
expect(html).toContain("https://example.com/@id/virtual:chatgpt-widget-NonExistentWidget.js");
68+
expect(html).toContain("https://example.com/@id/virtual:chatgpt-widget-entrypoint-NonExistentWidget.js");
6969
});
7070

7171
it("should include all widgets in getWidgets result with content", async () => {
@@ -76,15 +76,15 @@ describe("Basic Project Integration", () => {
7676
expect(widget.filePath).toBeTruthy();
7777
expect(widget.content).toContain("<!DOCTYPE html>");
7878
expect(widget.content).toContain(`<title>${widget.name} Widget</title>`);
79-
expect(widget.content).toContain("https://example.com/@id/virtual:chatgpt-widget-");
79+
expect(widget.content).toContain("https://example.com/@id/virtual:chatgpt-widget-entrypoint-");
8080
}
8181
});
8282

8383
it("should include Tailwind CSS in widget JavaScript module in dev mode", async () => {
8484
const { content: html } = await getWidgetHTML("TestWidget", { devServer });
8585

8686
// Extract the JavaScript module URL from the HTML
87-
const scriptMatch = html.match(/src="([^"]+virtual:chatgpt-widget-TestWidget\.js[^"]*)"/);
87+
const scriptMatch = html.match(/src="([^"]+virtual:chatgpt-widget-entrypoint-TestWidget\.js[^"]*)"/);
8888
expect(scriptMatch).toBeTruthy();
8989

9090
const scriptUrl = scriptMatch![1].replace("https://example.com/@id/", "").replace("https://example.com/", "");
@@ -162,12 +162,12 @@ describe("Basic Project Integration", () => {
162162
const manifestContent = await fs.readFile(MANIFEST_PATH, "utf-8");
163163
const manifest = JSON.parse(manifestContent);
164164

165-
expect(manifest).toHaveProperty("virtual:chatgpt-widget-TestWidget.html");
166-
expect(manifest).toHaveProperty("virtual:chatgpt-widget-AnotherWidget.html");
165+
expect(manifest).toHaveProperty("virtual:chatgpt-widget-html-TestWidget.html");
166+
expect(manifest).toHaveProperty("virtual:chatgpt-widget-html-AnotherWidget.html");
167167

168168
// Verify the manifest entries point to actual files
169-
expect(manifest["virtual:chatgpt-widget-TestWidget.html"].file).toBeTruthy();
170-
expect(manifest["virtual:chatgpt-widget-AnotherWidget.html"].file).toBeTruthy();
169+
expect(manifest["virtual:chatgpt-widget-html-TestWidget.html"].file).toBeTruthy();
170+
expect(manifest["virtual:chatgpt-widget-html-AnotherWidget.html"].file).toBeTruthy();
171171
});
172172

173173
it("should discover widgets in production mode", async () => {

spec/integration/plain-react.spec.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe("Plain React (without React Router)", () => {
4040
expect(html).toContain("<title>CounterWidget Widget</title>");
4141
expect(html).toContain('<div id="root"></div>');
4242
expect(html).toContain('<script type="module"');
43-
expect(html).toContain("https://example.com/@id/virtual:chatgpt-widget-CounterWidget.js");
43+
expect(html).toContain("https://example.com/@id/virtual:chatgpt-widget-entrypoint-CounterWidget.js");
4444
});
4545

4646
it("should generate HTML for both plain React widgets", async () => {
@@ -50,8 +50,8 @@ describe("Plain React (without React Router)", () => {
5050
expect(counterHtml).toContain("CounterWidget Widget");
5151
expect(greetingHtml).toContain("GreetingWidget Widget");
5252

53-
expect(counterHtml).toContain("/@id/virtual:chatgpt-widget-CounterWidget.js");
54-
expect(greetingHtml).toContain("/@id/virtual:chatgpt-widget-GreetingWidget.js");
53+
expect(counterHtml).toContain("/@id/virtual:chatgpt-widget-entrypoint-CounterWidget.js");
54+
expect(greetingHtml).toContain("/@id/virtual:chatgpt-widget-entrypoint-GreetingWidget.js");
5555
});
5656

5757
it("should include all plain React widgets in getWidgets result", async () => {
@@ -62,7 +62,7 @@ describe("Plain React (without React Router)", () => {
6262
expect(widget.filePath).toBeTruthy();
6363
expect(widget.content).toContain("<!DOCTYPE html>");
6464
expect(widget.content).toContain(`<title>${widget.name} Widget</title>`);
65-
expect(widget.content).toContain("https://example.com/@id/virtual:chatgpt-widget-");
65+
expect(widget.content).toContain("https://example.com/@id/virtual:chatgpt-widget-entrypoint-");
6666
}
6767
});
6868

@@ -120,11 +120,11 @@ describe("Plain React (without React Router)", () => {
120120
const manifestContent = await fs.readFile(MANIFEST_PLAIN_REACT_PATH, "utf-8");
121121
const manifest = JSON.parse(manifestContent);
122122

123-
expect(manifest).toHaveProperty("virtual:chatgpt-widget-CounterWidget.html");
124-
expect(manifest).toHaveProperty("virtual:chatgpt-widget-GreetingWidget.html");
123+
expect(manifest).toHaveProperty("virtual:chatgpt-widget-html-CounterWidget.html");
124+
expect(manifest).toHaveProperty("virtual:chatgpt-widget-html-GreetingWidget.html");
125125

126-
expect(manifest["virtual:chatgpt-widget-CounterWidget.html"].file).toBeTruthy();
127-
expect(manifest["virtual:chatgpt-widget-GreetingWidget.html"].file).toBeTruthy();
126+
expect(manifest["virtual:chatgpt-widget-html-CounterWidget.html"].file).toBeTruthy();
127+
expect(manifest["virtual:chatgpt-widget-html-GreetingWidget.html"].file).toBeTruthy();
128128
});
129129

130130
it("should discover plain React widgets in production mode", async () => {
@@ -152,7 +152,7 @@ describe("Plain React (without React Router)", () => {
152152
const manifestContent = await fs.readFile(MANIFEST_PLAIN_REACT_PATH, "utf-8");
153153
const manifest = JSON.parse(manifestContent);
154154

155-
const widgetEntries = Object.entries(manifest).filter(([key]) => key.startsWith("virtual:chatgpt-widget-"));
155+
const widgetEntries = Object.entries(manifest).filter(([key]) => key.startsWith("virtual:chatgpt-widget-html-"));
156156

157157
for (const [, entry] of widgetEntries) {
158158
const filePath = path.join(BUILD_PLAIN_REACT_DIR, (entry as any).file);

spec/integration/react-router.spec.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe("React Router v7 Integration", () => {
4141
expect(html).toContain("<title>NavigationWidget Widget</title>");
4242
expect(html).toContain('<div id="root"></div>');
4343
expect(html).toContain('<script type="module"');
44-
expect(html).toContain("https://example.com/@id/virtual:chatgpt-widget-NavigationWidget.js");
44+
expect(html).toContain("https://example.com/@id/virtual:chatgpt-widget-entrypoint-NavigationWidget.js");
4545
});
4646

4747
it("should generate HTML for both React Router widgets", async () => {
@@ -51,8 +51,8 @@ describe("React Router v7 Integration", () => {
5151
expect(navWidgetHtml).toContain("NavigationWidget Widget");
5252
expect(dataWidgetHtml).toContain("DataWidget Widget");
5353

54-
expect(navWidgetHtml).toContain("/@id/virtual:chatgpt-widget-NavigationWidget.js");
55-
expect(dataWidgetHtml).toContain("/@id/virtual:chatgpt-widget-DataWidget.js");
54+
expect(navWidgetHtml).toContain("/@id/virtual:chatgpt-widget-entrypoint-NavigationWidget.js");
55+
expect(dataWidgetHtml).toContain("/@id/virtual:chatgpt-widget-entrypoint-DataWidget.js");
5656
});
5757

5858
it("should include all React Router widgets in getWidgets result", async () => {
@@ -63,7 +63,7 @@ describe("React Router v7 Integration", () => {
6363
expect(widget.filePath).toBeTruthy();
6464
expect(widget.content).toContain("<!DOCTYPE html>");
6565
expect(widget.content).toContain(`<title>${widget.name} Widget</title>`);
66-
expect(widget.content).toContain("https://example.com/@id/virtual:chatgpt-widget-");
66+
expect(widget.content).toContain("https://example.com/@id/virtual:chatgpt-widget-entrypoint-");
6767
}
6868
});
6969

@@ -128,11 +128,11 @@ describe("React Router v7 Integration", () => {
128128
const manifestContent = await fs.readFile(MANIFEST_REACT_ROUTER_PATH, "utf-8");
129129
const manifest = JSON.parse(manifestContent);
130130

131-
expect(manifest).toHaveProperty("virtual:chatgpt-widget-NavigationWidget.html");
132-
expect(manifest).toHaveProperty("virtual:chatgpt-widget-DataWidget.html");
131+
expect(manifest).toHaveProperty("virtual:chatgpt-widget-html-NavigationWidget.html");
132+
expect(manifest).toHaveProperty("virtual:chatgpt-widget-html-DataWidget.html");
133133

134-
expect(manifest["virtual:chatgpt-widget-NavigationWidget.html"].file).toBeTruthy();
135-
expect(manifest["virtual:chatgpt-widget-DataWidget.html"].file).toBeTruthy();
134+
expect(manifest["virtual:chatgpt-widget-html-NavigationWidget.html"].file).toBeTruthy();
135+
expect(manifest["virtual:chatgpt-widget-html-DataWidget.html"].file).toBeTruthy();
136136
});
137137

138138
it("should discover React Router widgets in production mode", async () => {
@@ -320,7 +320,7 @@ describe("React Router v7 Integration", () => {
320320
const { content: devHtml } = await getWidgetHTML("SimpleWidget", { devServer });
321321

322322
// Extract the script src
323-
const scriptMatch = devHtml.match(/src="([^"]+virtual:chatgpt-widget-SimpleWidget\.js[^"]*)"/);
323+
const scriptMatch = devHtml.match(/src="([^"]+virtual:chatgpt-widget-entrypoint-SimpleWidget\.js[^"]*)"/);
324324
expect(scriptMatch).toBeTruthy();
325325

326326
// Fetch the JavaScript module from the dev server to verify it contains the React Router HMR runtime

spec/integration/root-layout.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,10 @@ describe("Root Layout Component", () => {
7171
const manifestContent = await fs.readFile(MANIFEST_WITH_ROOT_PATH, "utf-8");
7272
const manifest = JSON.parse(manifestContent);
7373

74-
expect(manifest).toHaveProperty("virtual:chatgpt-widget-WidgetA.html");
75-
expect(manifest).toHaveProperty("virtual:chatgpt-widget-WidgetB.html");
76-
expect(manifest).not.toHaveProperty("virtual:chatgpt-widget-root.html");
77-
expect(manifest).not.toHaveProperty("virtual:chatgpt-widget-Root.html");
74+
expect(manifest).toHaveProperty("virtual:chatgpt-widget-html-WidgetA.html");
75+
expect(manifest).toHaveProperty("virtual:chatgpt-widget-html-WidgetB.html");
76+
expect(manifest).not.toHaveProperty("virtual:chatgpt-widget-html-root.html");
77+
expect(manifest).not.toHaveProperty("virtual:chatgpt-widget-html-Root.html");
7878
});
7979

8080
it("should discover widgets excluding root in production mode", async () => {

src/index.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export async function getWidgetHTML(
131131

132132
if (isViteDevServer) {
133133
const vite = viteHandle.devServer;
134-
const virtualModuleId = `virtual:chatgpt-widget-${widgetName}.html`;
134+
const virtualModuleId = `virtual:chatgpt-widget-html-${widgetName}.html`;
135135

136136
// Step 1: Use plugin container to resolve and load the raw HTML from our plugin
137137
const resolved = await vite.pluginContainer.resolveId(virtualModuleId);
@@ -152,8 +152,8 @@ export async function getWidgetHTML(
152152
// Pass the virtual module ID as the URL so Vite knows the context
153153
const transformedHtml = await vite.transformIndexHtml(virtualModuleId, rawHtml);
154154

155-
// rewrite src="virtual:chatgpt-widget-${widgetName}.js" to src="/@id/virtual:chatgpt-widget-${widgetName}.js"
156-
html = transformedHtml.replace(/src="virtual:chatgpt-widget-/g, `src="/@id/virtual:chatgpt-widget-`);
155+
// rewrite src="virtual:chatgpt-widget-entrypoint-${widgetName}.js" to src="/@id/virtual:chatgpt-widget-entrypoint-${widgetName}.js"
156+
html = transformedHtml.replace(/src="virtual:chatgpt-widget-entrypoint-/g, `src="/@id/virtual:chatgpt-widget-entrypoint-`);
157157

158158
const plugin = vite.config.plugins.find((plugin) => plugin.name === PLUGIN_NAME) as ChatGPTWidgetPlugin;
159159
// Get explicit baseUrl from the plugin in the plugin options
@@ -210,7 +210,7 @@ export async function getWidgetHTML(
210210
const manifest = JSON.parse(manifestContent) as Record<string, { file: string }>;
211211

212212
// Look for the widget HTML file in the manifest
213-
const virtualModuleId = `virtual:chatgpt-widget-${widgetName}.html`;
213+
const virtualModuleId = `virtual:chatgpt-widget-html-${widgetName}.html`;
214214
const manifestEntry = manifest[virtualModuleId];
215215

216216
if (!manifestEntry) {
@@ -271,7 +271,7 @@ export function generateWidgetEntrypointHTML(widgetName: string): string {
271271
// Always use the virtual: protocol here
272272
// In dev mode, the MCP helper will rewrite this to /@id/virtual: after HTML transformation
273273
// In build mode, Vite will resolve and bundle this appropriately
274-
const jsEntrypoint = `virtual:chatgpt-widget-${widgetName}.js`;
274+
const jsEntrypoint = `virtual:chatgpt-widget-entrypoint-${widgetName}.js`;
275275

276276
return `
277277
<!DOCTYPE html>
@@ -339,7 +339,7 @@ export function chatGPTWidgetPlugin(options: ChatGPTWidgetPluginOptions = {}): C
339339
const widgetEntries: Record<string, string> = {};
340340

341341
for (const file of files) {
342-
widgetEntries[`chatgpt-widget-${file.name}`] = `virtual:chatgpt-widget-${file.name}.html`;
342+
widgetEntries[`chatgpt-widget-${file.name}`] = `virtual:chatgpt-widget-html-${file.name}.html`;
343343
}
344344

345345
// Add widget entries to existing input
@@ -362,26 +362,26 @@ export function chatGPTWidgetPlugin(options: ChatGPTWidgetPluginOptions = {}): C
362362

363363
resolveId(id) {
364364
// Handle virtual HTML entrypoint resolution
365-
if (id.startsWith("virtual:chatgpt-widget-") && id.endsWith(".html")) {
365+
if (id.startsWith("virtual:chatgpt-widget-html-") && id.endsWith(".html")) {
366366
return id;
367367
}
368368
// Handle virtual JS entrypoint resolution
369-
if (id.startsWith("virtual:chatgpt-widget-") && id.endsWith(".js")) {
369+
if (id.startsWith("virtual:chatgpt-widget-entrypoint-") && id.endsWith(".js")) {
370370
return "\0" + id;
371371
}
372372
return null;
373373
},
374374

375375
async load(id) {
376376
// Handle virtual HTML files
377-
if (id.startsWith("virtual:chatgpt-widget-") && id.endsWith(".html")) {
378-
const widgetName = id.replace("virtual:chatgpt-widget-", "").replace(".html", "");
377+
if (id.startsWith("virtual:chatgpt-widget-html-") && id.endsWith(".html")) {
378+
const widgetName = id.replace("virtual:chatgpt-widget-html-", "").replace(".html", "");
379379
return generateWidgetEntrypointHTML(widgetName);
380380
}
381381

382382
// Handle virtual JS entrypoints
383-
if (id.startsWith("\0virtual:chatgpt-widget-") && id.endsWith(".js")) {
384-
const widgetName = id.replace("\0virtual:chatgpt-widget-", "").replace(".js", "");
383+
if (id.startsWith("\0virtual:chatgpt-widget-entrypoint-") && id.endsWith(".js")) {
384+
const widgetName = id.replace("\0virtual:chatgpt-widget-entrypoint-", "").replace(".js", "");
385385

386386
// Find the actual widget file
387387
const widgetsDirPath = path.resolve(config.root, widgetsDir);

0 commit comments

Comments
 (0)