@@ -4,24 +4,39 @@ import { ready } from '../common/index.js';
4
4
5
5
/** @type {import('@webcontainer/api').WebContainer } Web container singleton */
6
6
let vm ;
7
- /** Keep track of startup progress, so we don't repeat previous steps in case of a timeout */
8
- let step = 0 ;
9
- /** @type {string } path to the web container server instance */
10
- let base ;
7
+ /** @type {Promise<import('$lib/types').Adapter> | undefined } */
8
+ let instance ;
9
+
11
10
/**
12
- * Keeps track of the latest create/reset to ensure things are not processed in parallel.
13
- * (if this turns out to be insufficient, we can use a queue)
14
- * @type {Promise<any> | undefined }
11
+ * @param {import('$lib/types').Stub[] } stubs
12
+ * @returns {Promise<import('$lib/types').Adapter> }
15
13
*/
16
- let running ;
17
- /** @type {Set<string> } Paths of the currently loaded file stubs */
18
- let current = new Set ( ) ;
14
+ export async function create ( stubs ) {
15
+ if ( ! instance ) {
16
+ instance = _create ( stubs ) ;
17
+ } else {
18
+ const adapter = await instance ;
19
+ await adapter . reset ( stubs ) ;
20
+ }
21
+ return instance ;
22
+ }
19
23
20
24
/**
21
25
* @param {import('$lib/types').Stub[] } stubs
22
26
* @returns {Promise<import('$lib/types').Adapter> }
23
27
*/
24
- export async function create ( stubs ) {
28
+ async function _create ( stubs ) {
29
+ /**
30
+ * Keeps track of the latest create/reset to ensure things are not processed in parallel.
31
+ * (if this turns out to be insufficient, we can use a queue)
32
+ * @type {Promise<any> | undefined }
33
+ */
34
+ let running ;
35
+ /** @type {Map<string, string> } Paths and contents of the currently loaded file stubs */
36
+ let current = stubs_to_map ( stubs ) ;
37
+ /** @type {boolean } Track whether there was an error from vite dev server */
38
+ let vite_error = false ;
39
+
25
40
const tree = convert_stubs_to_tree ( stubs ) ;
26
41
27
42
const common = await ready ;
@@ -36,21 +51,14 @@ export async function create(stubs) {
36
51
throw new Error ( 'WebContainers are not supported by Safari' ) ;
37
52
}
38
53
39
- await running ; // wait for any previous create to finish
40
-
41
- const init = new Promise ( async ( fulfil , reject ) => {
42
- if ( base ) {
43
- // startup was successful in the meantime
44
- fulfil ( base ) ;
45
- }
46
-
54
+ const base = await new Promise ( async ( fulfil , reject ) => {
47
55
/** @type {any } */
48
56
let timeout ;
49
57
function reset_timeout ( ) {
50
58
clearTimeout ( timeout ) ;
51
59
timeout = setTimeout ( ( ) => {
52
60
reject ( new Error ( 'Timed out starting WebContainer' ) ) ;
53
- } , 8000 ) ;
61
+ } , 15000 ) ;
54
62
}
55
63
56
64
reset_timeout ( ) ;
@@ -59,11 +67,9 @@ export async function create(stubs) {
59
67
// if there was an error later on or a timeout and the user tries again
60
68
if ( ! vm ) {
61
69
console . log ( 'loading webcontainer' ) ;
62
-
63
70
const WebContainer = await load ( ) ;
64
71
65
72
console . log ( 'booting webcontainer' ) ;
66
-
67
73
vm = await WebContainer . boot ( ) ;
68
74
}
69
75
@@ -72,73 +78,63 @@ export async function create(stubs) {
72
78
reject ( new Error ( error . message ) ) ;
73
79
} ) ;
74
80
75
- const ready_unsub = vm . on ( 'server-ready' , ( port , _base ) => {
76
- base = _base ;
81
+ const ready_unsub = vm . on ( 'server-ready' , ( port , base ) => {
77
82
ready_unsub ( ) ;
78
- console . log ( `server ready on port ${ port } at ${ performance . now ( ) } : ${ _base } ` ) ;
79
- fulfil ( _base ) ;
83
+ console . log ( `server ready on port ${ port } at ${ performance . now ( ) } : ${ base } ` ) ;
84
+ fulfil ( base ) ; // this will be the last thing that happens if everything goes well
80
85
} ) ;
81
86
82
- if ( step < 1 ) {
83
- reset_timeout ( ) ;
84
- step = 1 ;
85
- console . log ( 'loading files' ) ;
86
-
87
- await vm . loadFiles ( tree ) ;
88
-
89
- current = new Set ( stub_filenames ( stubs ) ) ;
90
- }
91
-
92
- if ( step < 2 ) {
93
- reset_timeout ( ) ;
94
- step = 2 ;
95
- console . log ( 'unpacking modules' ) ;
96
-
97
- const unzip = await vm . run (
98
- {
99
- command : 'node' ,
100
- args : [ 'unzip.cjs' ]
101
- } ,
102
- {
103
- stderr : ( data ) => console . error ( `[unzip] ${ data } ` )
104
- }
105
- ) ;
106
-
107
- const code = await unzip . onExit ;
87
+ reset_timeout ( ) ;
88
+ console . log ( 'loading files' ) ;
89
+ await vm . loadFiles ( tree ) ;
108
90
109
- if ( code !== 0 ) {
110
- reject ( new Error ( 'Failed to initialize WebContainer' ) ) ;
91
+ reset_timeout ( ) ;
92
+ console . log ( 'unpacking modules' ) ;
93
+ const unzip = await vm . run (
94
+ {
95
+ command : 'node' ,
96
+ args : [ 'unzip.cjs' ]
97
+ } ,
98
+ {
99
+ stderr : ( data ) => console . error ( `[unzip] ${ data } ` )
111
100
}
101
+ ) ;
102
+ const code = await unzip . onExit ;
103
+ if ( code !== 0 ) {
104
+ reject ( new Error ( 'Failed to initialize WebContainer' ) ) ;
112
105
}
113
106
114
- if ( step < 3 ) {
115
- reset_timeout ( ) ;
116
- step = 3 ;
117
- console . log ( 'starting dev server' ) ;
118
-
119
- await vm . run ( { command : 'chmod' , args : [ 'a+x' , 'node_modules/vite/bin/vite.js' ] } ) ;
107
+ reset_timeout ( ) ;
108
+ console . log ( 'starting dev server' ) ;
109
+ await vm . run ( { command : 'chmod' , args : [ 'a+x' , 'node_modules/vite/bin/vite.js' ] } ) ;
110
+ await run_dev ( ) ;
120
111
121
- await vm . run (
112
+ async function run_dev ( ) {
113
+ const process = await vm . run (
122
114
{ command : 'turbo' , args : [ 'run' , 'dev' ] } ,
123
115
{
124
116
stdout : ( ) => {
125
117
if ( ! base ) {
126
118
reset_timeout ( ) ;
127
119
}
128
120
} ,
129
- stderr : ( data ) => console . error ( `[dev] ${ data } ` )
121
+ stderr : ( data ) => {
122
+ vite_error = true ;
123
+ console . error ( `[dev] ${ data } ` ) ;
124
+ }
130
125
}
131
126
) ;
127
+ // keep restarting dev server (can crash in case of illegal +files for example)
128
+ process . onExit . then ( ( code ) => {
129
+ if ( code !== 0 ) {
130
+ setTimeout ( ( ) => {
131
+ run_dev ( ) ;
132
+ } , 2000 ) ;
133
+ }
134
+ } ) ;
132
135
}
133
136
} ) ;
134
137
135
- running = init . catch ( ( ) => { } ) ;
136
- base = await init ;
137
-
138
- if ( stub_filenames ( stubs ) . some ( ( name ) => ! current . has ( name ) ) ) {
139
- await reset ( stubs ) ;
140
- }
141
-
142
138
/**
143
139
* Deletes old files and adds new ones
144
140
* @param {import('$lib/types').Stub[] } stubs
@@ -148,65 +144,61 @@ export async function create(stubs) {
148
144
/** @type {Function } */
149
145
let resolve = ( ) => { } ;
150
146
running = new Promise ( ( fulfil ) => ( resolve = fulfil ) ) ;
147
+ vite_error = false ;
151
148
152
149
const old = current ;
153
- current = new Set ( stub_filenames ( stubs ) ) ;
150
+ const new_stubs = stubs . filter (
151
+ ( stub ) => stub . type !== 'file' || old . get ( stub . name ) !== stub . contents
152
+ ) ;
153
+ current = stubs_to_map ( stubs ) ;
154
154
155
155
for ( const stub of stubs ) {
156
156
if ( stub . type === 'file' ) {
157
157
old . delete ( stub . name ) ;
158
158
}
159
159
}
160
160
161
- // For some reason, server-ready is fired again on resetting the files here .
161
+ // For some reason, server-ready is fired again when the vite dev server is restarted .
162
162
// We need to wait for it to finish before we can continue, else we might
163
163
// request files from Vite before it's ready, leading to a timeout.
164
- const promise = new Promise ( ( fulfil , reject ) => {
165
- const error_unsub = vm . on ( 'error' , ( error ) => {
166
- error_unsub ( ) ;
167
- resolve ( ) ;
168
- reject ( new Error ( error . message ) ) ;
169
- } ) ;
170
-
171
- const ready_unsub = vm . on ( 'server-ready' , ( port , base ) => {
172
- ready_unsub ( ) ;
173
- console . log ( `server ready on port ${ port } at ${ performance . now ( ) } : ${ base } ` ) ;
174
- resolve ( ) ;
175
- fulfil ( undefined ) ;
176
- } ) ;
177
-
178
- setTimeout ( ( ) => {
179
- resolve ( ) ;
180
- reject ( new Error ( 'Timed out resetting WebContainer' ) ) ;
181
- } , 10000 ) ;
182
- } ) ;
183
-
184
- for ( const file of old ) {
185
- // TODO this fails with a cryptic error
186
- // index.svelte:155 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'rmSync')
187
- // at Object.rm (webcontainer.e2e246a845f9e80283581d6b944116e399af6950.js:6:121171)
188
- // at MessagePort._0x4ec3f4 (webcontainer.e2e246a845f9e80283581d6b944116e399af6950.js:6:110957)
189
- // at MessagePort.nrWrapper (headless:5:29785)
190
- // await vm.fs.rm(file);
191
-
192
- // temporary workaround
193
- try {
194
- await vm . run ( {
195
- command : 'node' ,
196
- args : [ '-e' , `fs.rmSync('${ file . slice ( 1 ) } ')` ]
197
- } ) ;
198
- } catch ( e ) {
199
- console . error ( e ) ;
200
- }
164
+ const will_restart = new_stubs . some (
165
+ ( stub ) =>
166
+ stub . type === 'file' &&
167
+ ( stub . name === '/vite.config.js' || stub . name === '/svelte.config.js' )
168
+ ) ;
169
+ const promise = will_restart
170
+ ? new Promise ( ( fulfil , reject ) => {
171
+ const error_unsub = vm . on ( 'error' , ( error ) => {
172
+ error_unsub ( ) ;
173
+ resolve ( ) ;
174
+ reject ( new Error ( error . message ) ) ;
175
+ } ) ;
176
+
177
+ const ready_unsub = vm . on ( 'server-ready' , ( port , base ) => {
178
+ ready_unsub ( ) ;
179
+ console . log ( `server ready on port ${ port } at ${ performance . now ( ) } : ${ base } ` ) ;
180
+ resolve ( ) ;
181
+ fulfil ( undefined ) ;
182
+ } ) ;
183
+
184
+ setTimeout ( ( ) => {
185
+ resolve ( ) ;
186
+ reject ( new Error ( 'Timed out resetting WebContainer' ) ) ;
187
+ } , 10000 ) ;
188
+ } )
189
+ : Promise . resolve ( ) ;
190
+
191
+ for ( const file of old . keys ( ) ) {
192
+ await vm . fs . rm ( file , { force : true , recursive : true } ) ;
201
193
}
202
194
203
- await vm . loadFiles ( convert_stubs_to_tree ( stubs ) ) ;
204
-
195
+ await vm . loadFiles ( convert_stubs_to_tree ( new_stubs ) ) ;
205
196
await promise ;
206
-
207
197
await new Promise ( ( f ) => setTimeout ( f , 200 ) ) ; // wait for chokidar
208
198
209
199
resolve ( ) ;
200
+
201
+ return will_restart || vite_error ;
210
202
}
211
203
212
204
/**
@@ -307,9 +299,15 @@ function to_file(stub) {
307
299
}
308
300
309
301
/**
310
- *
311
302
* @param {import('$lib/types').Stub[] } stubs
303
+ * @returns {Map<string, string> }
312
304
*/
313
- function stub_filenames ( stubs ) {
314
- return stubs . filter ( ( stub ) => stub . type === 'file' ) . map ( ( stub ) => stub . name ) ;
305
+ function stubs_to_map ( stubs ) {
306
+ const map = new Map ( ) ;
307
+ for ( const stub of stubs ) {
308
+ if ( stub . type === 'file' ) {
309
+ map . set ( stub . name , stub . contents ) ;
310
+ }
311
+ }
312
+ return map ;
315
313
}
0 commit comments