Skip to content

Commit d200c37

Browse files
authored
fix: Support react-refresh when pre-rendering HTML pages in dev mode (wxt-dev#158)
1 parent b58fb02 commit d200c37

File tree

4 files changed

+139
-57
lines changed

4 files changed

+139
-57
lines changed

.codecov.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@ coverage:
22
status:
33
project:
44
default:
5-
target: 80%
6-
threshold: 1%
5+
informational: true
6+
patch:
7+
default:
8+
informational: true

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ CHANGELOG.md
1313
*.txt
1414
_gitignore
1515
_redirects
16+
*.svelte

src/core/vite-plugins/devHtmlPrerender.ts

Lines changed: 133 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,68 +4,147 @@ import { getEntrypointName } from '../utils/entrypoints';
44
import { parseHTML } from 'linkedom';
55
import { dirname, isAbsolute, relative, resolve } from 'path';
66

7+
// Cache the preamble script for all devHtmlPrerender plugins, not just one
8+
let reactRefreshPreamble = '';
9+
710
/**
811
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
912
*/
10-
export function devHtmlPrerender(config: InternalConfig): vite.Plugin {
11-
return {
12-
apply: 'build',
13-
name: 'wxt:dev-html-prerender',
14-
config() {
15-
return {
16-
resolve: {
17-
alias: {
18-
'@wxt/reload-html': resolve(
19-
config.root,
20-
'node_modules/wxt/dist/virtual-modules/reload-html.js',
21-
),
13+
export function devHtmlPrerender(config: InternalConfig): vite.PluginOption {
14+
const htmlReloadId = '@wxt/reload-html';
15+
const resolvedHtmlReloadId = resolve(
16+
config.root,
17+
'node_modules/wxt/dist/virtual-modules/reload-html.js',
18+
);
19+
const virtualReactRefreshId = '@wxt/virtual-react-refresh';
20+
const resolvedVirtualReactRefreshId = '\0' + virtualReactRefreshId;
21+
22+
return [
23+
{
24+
apply: 'build',
25+
name: 'wxt:dev-html-prerender',
26+
config() {
27+
return {
28+
resolve: {
29+
alias: {
30+
[htmlReloadId]: resolvedHtmlReloadId,
31+
},
2232
},
23-
},
24-
};
25-
},
26-
async transform(html, id) {
27-
const server = config.server;
28-
if (config.command !== 'serve' || server == null || !id.endsWith('.html'))
29-
return;
33+
};
34+
},
35+
// Convert scripts like src="./main.tsx" -> src="http://localhost:3000/entrypoints/popup/main.tsx"
36+
// before the paths are replaced with their bundled path
37+
transform(code, id) {
38+
const server = config.server;
39+
if (
40+
config.command !== 'serve' ||
41+
server == null ||
42+
!id.endsWith('.html')
43+
)
44+
return;
45+
46+
const { document } = parseHTML(code);
47+
48+
const pointToDevServer = (
49+
querySelector: string,
50+
attr: string,
51+
): void => {
52+
document.querySelectorAll(querySelector).forEach((element) => {
53+
const src = element.getAttribute(attr);
54+
if (!src) return;
3055

31-
const originalUrl = `${server.origin}${id}`;
32-
const name = getEntrypointName(config.entrypointsDir, id);
33-
const url = `${server.origin}/${name}.html`;
34-
const serverHtml = await server.transformIndexHtml(
35-
url,
36-
html,
37-
originalUrl,
38-
);
39-
const { document } = parseHTML(serverHtml);
56+
if (isAbsolute(src)) {
57+
element.setAttribute(attr, server.origin + src);
58+
} else if (src.startsWith('.')) {
59+
const abs = resolve(dirname(id), src);
60+
const pathname = relative(config.root, abs);
61+
element.setAttribute(attr, `${server.origin}/${pathname}`);
62+
}
63+
});
64+
};
65+
pointToDevServer('script[type=module]', 'src');
66+
pointToDevServer('link[rel=stylesheet]', 'href');
4067

41-
const pointToDevServer = (querySelector: string, attr: string): void => {
42-
document.querySelectorAll(querySelector).forEach((element) => {
43-
const src = element.getAttribute(attr);
44-
if (!src) return;
68+
// Add a script to add page reloading
69+
const reloader = document.createElement('script');
70+
reloader.src = htmlReloadId;
71+
reloader.type = 'module';
72+
document.head.appendChild(reloader);
4573

46-
if (isAbsolute(src)) {
47-
element.setAttribute(attr, server.origin + src);
48-
} else if (src.startsWith('.')) {
49-
const abs = resolve(dirname(id), src);
50-
const pathname = relative(config.root, abs);
51-
element.setAttribute(attr, `${server.origin}/${pathname}`);
52-
}
53-
});
54-
};
55-
pointToDevServer('script[type=module]', 'src');
56-
pointToDevServer('link[rel=stylesheet]', 'href');
74+
const newHtml = document.toString();
75+
config.logger.debug('transform ' + id);
76+
config.logger.debug('Old HTML:\n' + code);
77+
config.logger.debug('New HTML:\n' + newHtml);
78+
return newHtml;
79+
},
5780

58-
// Add a script to add page reloading
59-
const reloader = document.createElement('script');
60-
reloader.src = '@wxt/reload-html';
61-
reloader.type = 'module';
62-
document.head.appendChild(reloader);
81+
// Pass the HTML through the dev server to add dev-mode specific code
82+
async transformIndexHtml(html, ctx) {
83+
const server = config.server;
84+
if (config.command !== 'serve' || server == null) return;
6385

64-
const newHtml = document.toString();
65-
config.logger.debug('Transformed ' + id);
66-
config.logger.debug('Old HTML:\n' + html);
67-
config.logger.debug('New HTML:\n' + newHtml);
68-
return newHtml;
86+
const originalUrl = `${server.origin}${ctx.path}`;
87+
const name = getEntrypointName(config.entrypointsDir, ctx.filename);
88+
const url = `${server.origin}/${name}.html`;
89+
const serverHtml = await server.transformIndexHtml(
90+
url,
91+
html,
92+
originalUrl,
93+
);
94+
const { document } = parseHTML(serverHtml);
95+
96+
// React pages include a preamble as an unsafe-inline type="module" script to enable fast refresh, as shown here:
97+
// https://github.com/wxt-dev/wxt/issues/157#issuecomment-1756497616
98+
// Since unsafe-inline scripts are blocked by MV3 CSPs, we need to virtualize it.
99+
const reactRefreshScript = Array.from(
100+
document.querySelectorAll('script[type=module]'),
101+
).find((script) => script.innerHTML.includes('@react-refresh'));
102+
if (reactRefreshScript) {
103+
// Save preamble to serve from server
104+
reactRefreshPreamble = reactRefreshScript.innerHTML;
105+
106+
// Replace unsafe inline script
107+
const virtualScript = document.createElement('script');
108+
virtualScript.type = 'module';
109+
virtualScript.src = `${server.origin}/${virtualReactRefreshId}`;
110+
reactRefreshScript.replaceWith(virtualScript);
111+
}
112+
113+
// Change /@vite/client -> http://localhost:3000/@vite/client
114+
const viteClientScript = document.querySelector<HTMLScriptElement>(
115+
"script[src='/@vite/client']",
116+
);
117+
if (viteClientScript) {
118+
viteClientScript.src = `${server.origin}${viteClientScript.src}`;
119+
}
120+
121+
const newHtml = document.toString();
122+
config.logger.debug('transformIndexHtml ' + ctx.filename);
123+
config.logger.debug('Old HTML:\n' + html);
124+
config.logger.debug('New HTML:\n' + newHtml);
125+
return newHtml;
126+
},
127+
},
128+
{
129+
name: 'wxt:virtualize-react-refresh',
130+
apply: 'serve',
131+
resolveId(id) {
132+
if (id === `/${virtualReactRefreshId}`) {
133+
return resolvedVirtualReactRefreshId;
134+
}
135+
// Ignore chunk contents when pre-rendering
136+
if (id.startsWith('/chunks/')) {
137+
return '\0noop';
138+
}
139+
},
140+
load(id) {
141+
if (id === resolvedVirtualReactRefreshId) {
142+
return reactRefreshPreamble;
143+
}
144+
if (id === '\0noop') {
145+
return '';
146+
}
147+
},
69148
},
70-
};
149+
];
71150
}

templates/svelte/src/entrypoints/popup/App.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
1313
</a>
1414
</div>
15-
<h1>WXT 2 + Svelte</h1>
15+
<h1>WXT + Svelte</h1>
1616

1717
<div class="card">
1818
<Counter />

0 commit comments

Comments
 (0)