@@ -4,68 +4,147 @@ import { getEntrypointName } from '../utils/entrypoints';
44import { parseHTML } from 'linkedom' ;
55import { 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}
0 commit comments