1
1
import { WebContainer } from '@webcontainer/api' ;
2
2
import base64 from 'base64-js' ;
3
3
import AnsiToHtml from 'ansi-to-html' ;
4
+ import * as yootils from 'yootils' ;
4
5
import { escape_html , get_depth } from '../../../utils.js' ;
5
6
import { ready } from '../common/index.js' ;
6
7
@@ -25,12 +26,7 @@ export async function create(base, error, progress, logs) {
25
26
26
27
progress . set ( { value : 0 , text : 'loading files' } ) ;
27
28
28
- /**
29
- * Keeps track of the latest create/reset to ensure things are not processed in parallel.
30
- * (if this turns out to be insufficient, we can use a queue)
31
- * @type {Promise<any> | undefined }
32
- */
33
- let running ;
29
+ const q = yootils . queue ( 1 ) ;
34
30
35
31
/** Paths and contents of the currently loaded file stubs */
36
32
let current_stubs = stubs_to_map ( [ ] ) ;
@@ -118,116 +114,111 @@ export async function create(base, error, progress, logs) {
118
114
}
119
115
120
116
return {
121
- reset : async ( stubs ) => {
122
- await running ;
123
- /** @type {Function } */
124
- let resolve = ( ) => { } ;
125
- running = new Promise ( ( fulfil ) => ( resolve = fulfil ) ) ;
126
-
127
- /** @ type { import('$lib/types').Stub[] } */
128
- const to_write = [ ] ;
129
-
130
- for ( const stub of stubs ) {
131
- if ( stub . type === 'file' ) {
132
- const current = /** @type { import('$lib/types').FileStub } */ (
133
- current_stubs . get ( stub . name )
134
- ) ;
135
-
136
- if ( current ?. contents !== stub . contents ) {
117
+ reset : ( stubs ) => {
118
+ return q . add ( async ( ) => {
119
+ /** @type {import('$lib/types').Stub[] } */
120
+ const to_write = [ ] ;
121
+
122
+ for ( const stub of stubs ) {
123
+ if ( stub . type === 'file' ) {
124
+ const current = /** @type { import('$lib/types').FileStub } */ (
125
+ current_stubs . get ( stub . name )
126
+ ) ;
127
+
128
+ if ( current ?. contents !== stub . contents ) {
129
+ to_write . push ( stub ) ;
130
+ }
131
+ } else {
132
+ // always add directories, otherwise convert_stubs_to_tree will fail
137
133
to_write . push ( stub ) ;
138
134
}
139
- } else {
140
- // always add directories, otherwise convert_stubs_to_tree will fail
141
- to_write . push ( stub ) ;
142
- }
143
-
144
- current_stubs . delete ( stub . name ) ;
145
- }
146
135
147
- // Don't delete the node_modules folder when switching from one exercise to another
148
- // where, as this crashes the dev server.
149
- [ '/node_modules' , '/node_modules/.bin' ] . forEach ( ( name ) => current_stubs . delete ( name ) ) ;
150
-
151
- const to_delete = Array . from ( current_stubs . keys ( ) ) ;
152
- current_stubs = stubs_to_map ( stubs ) ;
153
-
154
- // For some reason, server-ready is fired again when the vite dev server is restarted.
155
- // We need to wait for it to finish before we can continue, else we might
156
- // request files from Vite before it's ready, leading to a timeout.
157
- const will_restart = launched && to_write . some ( will_restart_vite_dev_server ) ;
158
- const promise = will_restart
159
- ? new Promise ( ( fulfil , reject ) => {
160
- const error_unsub = vm . on ( 'error' , ( error ) => {
161
- error_unsub ( ) ;
162
- resolve ( ) ;
163
- reject ( new Error ( error . message ) ) ;
164
- } ) ;
165
-
166
- const ready_unsub = vm . on ( 'server-ready' , ( port , base ) => {
167
- ready_unsub ( ) ;
168
- console . log ( `server ready on port ${ port } at ${ performance . now ( ) } : ${ base } ` ) ;
169
- resolve ( ) ;
170
- fulfil ( undefined ) ;
171
- } ) ;
172
-
173
- setTimeout ( ( ) => {
174
- resolve ( ) ;
175
- reject ( new Error ( 'Timed out resetting WebContainer' ) ) ;
176
- } , 10000 ) ;
177
- } )
178
- : Promise . resolve ( ) ;
179
-
180
- for ( const file of to_delete ) {
181
- await vm . fs . rm ( file , { force : true , recursive : true } ) ;
182
- }
136
+ current_stubs . delete ( stub . name ) ;
137
+ }
183
138
184
- await vm . mount ( convert_stubs_to_tree ( to_write ) ) ;
185
- await promise ;
186
- await new Promise ( ( f ) => setTimeout ( f , 200 ) ) ; // wait for chokidar
139
+ // Don't delete the node_modules folder when switching from one exercise to another
140
+ // where, as this crashes the dev server.
141
+ [ '/node_modules' , '/node_modules/.bin' ] . forEach ( ( name ) => current_stubs . delete ( name ) ) ;
142
+
143
+ const to_delete = Array . from ( current_stubs . keys ( ) ) ;
144
+ current_stubs = stubs_to_map ( stubs ) ;
145
+
146
+ // For some reason, server-ready is fired again when the vite dev server is restarted.
147
+ // We need to wait for it to finish before we can continue, else we might
148
+ // request files from Vite before it's ready, leading to a timeout.
149
+ const will_restart = launched && to_write . some ( will_restart_vite_dev_server ) ;
150
+ const promise = will_restart
151
+ ? new Promise ( ( fulfil , reject ) => {
152
+ const error_unsub = vm . on ( 'error' , ( error ) => {
153
+ error_unsub ( ) ;
154
+ reject ( new Error ( error . message ) ) ;
155
+ } ) ;
156
+
157
+ const ready_unsub = vm . on ( 'server-ready' , ( port , base ) => {
158
+ ready_unsub ( ) ;
159
+ console . log ( `server ready on port ${ port } at ${ performance . now ( ) } : ${ base } ` ) ;
160
+ fulfil ( undefined ) ;
161
+ } ) ;
162
+
163
+ setTimeout ( ( ) => {
164
+ reject ( new Error ( 'Timed out resetting WebContainer' ) ) ;
165
+ } , 10000 ) ;
166
+ } )
167
+ : Promise . resolve ( ) ;
168
+
169
+ for ( const file of to_delete ) {
170
+ await vm . fs . rm ( file , { force : true , recursive : true } ) ;
171
+ }
187
172
188
- resolve ( ) ;
173
+ await vm . mount ( convert_stubs_to_tree ( to_write ) ) ;
174
+ await promise ;
175
+ await new Promise ( ( f ) => setTimeout ( f , 200 ) ) ; // wait for chokidar
189
176
190
- // Also trigger a reload of the iframe in case new files were added / old ones deleted,
191
- // because that can result in a broken UI state
192
- const should_reload = ! launched || will_restart || to_delete . length > 0 ;
177
+ // Also trigger a reload of the iframe in case new files were added / old ones deleted,
178
+ // because that can result in a broken UI state
179
+ const should_reload = ! launched || will_restart || to_delete . length > 0 ;
193
180
194
- await launch ( ) ;
181
+ await launch ( ) ;
195
182
196
- return should_reload ;
183
+ return should_reload ;
184
+ } ) ;
197
185
} ,
198
- update : async ( file ) => {
199
- await running ;
186
+ update : ( file ) => {
187
+ return q . add ( async ( ) => {
188
+ /** @type {import('@webcontainer/api').FileSystemTree } */
189
+ const root = { } ;
200
190
201
- /** @type {import('@webcontainer/api').FileSystemTree } */
202
- const root = { } ;
191
+ let tree = root ;
203
192
204
- let tree = root ;
193
+ const path = file . name . split ( '/' ) . slice ( 1 ) ;
194
+ const basename = /** @type {string } */ ( path . pop ( ) ) ;
205
195
206
- const path = file . name . split ( '/' ) . slice ( 1 ) ;
207
- const basename = /** @type {string } */ ( path . pop ( ) ) ;
196
+ for ( const part of path ) {
197
+ if ( ! tree [ part ] ) {
198
+ /** @type {import('@webcontainer/api').FileSystemTree } */
199
+ const directory = { } ;
208
200
209
- for ( const part of path ) {
210
- if ( ! tree [ part ] ) {
211
- /** @type { import('@webcontainer/api').FileSystemTree } */
212
- const directory = { } ;
201
+ tree [ part ] = {
202
+ directory
203
+ } ;
204
+ }
213
205
214
- tree [ part ] = {
215
- directory
216
- } ;
206
+ tree = /** @type {import('@webcontainer/api').DirectoryNode } */ ( tree [ part ] ) . directory ;
217
207
}
218
208
219
- tree = /** @type {import('@webcontainer/api').DirectoryNode } */ ( tree [ part ] ) . directory ;
220
- }
221
-
222
- tree [ basename ] = to_file ( file ) ;
209
+ tree [ basename ] = to_file ( file ) ;
223
210
224
- await vm . mount ( root ) ;
211
+ await vm . mount ( root ) ;
225
212
226
- current_stubs . set ( file . name , file ) ;
213
+ current_stubs . set ( file . name , file ) ;
227
214
228
- await new Promise ( ( f ) => setTimeout ( f , 200 ) ) ; // wait for chokidar
215
+ // we need to stagger sequential updates, just enough that the HMR
216
+ // wires don't get crossed. 50ms seems to be enough of a delay
217
+ // to avoid glitches without noticeably affecting update speed
218
+ await new Promise ( ( f ) => setTimeout ( f , 50 ) ) ;
229
219
230
- return will_restart_vite_dev_server ( file ) ;
220
+ return will_restart_vite_dev_server ( file ) ;
221
+ } ) ;
231
222
}
232
223
} ;
233
224
}
0 commit comments