Skip to content

Commit db2602b

Browse files
authored
[fix] webcontainer tweaks (#114)
- remove steps logic, turns out starting up is very robust if it goes well - do a full page reload if something fails during initial boot - make webcontainer a real singleton internally - use the built-in `rm` method now (works now) - don't reload iframe if not needed (better DX when solving/resetting)
1 parent 2b105ce commit db2602b

File tree

5 files changed

+133
-121
lines changed

5 files changed

+133
-121
lines changed

src/lib/client/adapters/filesystem/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export async function create(stubs) {
3636
});
3737

3838
await new Promise((f) => setTimeout(f, 100)); // wait for chokidar
39+
40+
return true; // always reload page, not worth optimizing for local dev at this point
3941
},
4042

4143
/** @param {import('$lib/types').FileStub[]} stubs */

src/lib/client/adapters/webcontainer/index.js

Lines changed: 112 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,39 @@ import { ready } from '../common/index.js';
44

55
/** @type {import('@webcontainer/api').WebContainer} Web container singleton */
66
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+
1110
/**
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>}
1513
*/
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+
}
1923

2024
/**
2125
* @param {import('$lib/types').Stub[]} stubs
2226
* @returns {Promise<import('$lib/types').Adapter>}
2327
*/
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+
2540
const tree = convert_stubs_to_tree(stubs);
2641

2742
const common = await ready;
@@ -36,21 +51,14 @@ export async function create(stubs) {
3651
throw new Error('WebContainers are not supported by Safari');
3752
}
3853

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) => {
4755
/** @type {any} */
4856
let timeout;
4957
function reset_timeout() {
5058
clearTimeout(timeout);
5159
timeout = setTimeout(() => {
5260
reject(new Error('Timed out starting WebContainer'));
53-
}, 8000);
61+
}, 15000);
5462
}
5563

5664
reset_timeout();
@@ -59,11 +67,9 @@ export async function create(stubs) {
5967
// if there was an error later on or a timeout and the user tries again
6068
if (!vm) {
6169
console.log('loading webcontainer');
62-
6370
const WebContainer = await load();
6471

6572
console.log('booting webcontainer');
66-
6773
vm = await WebContainer.boot();
6874
}
6975

@@ -72,73 +78,63 @@ export async function create(stubs) {
7278
reject(new Error(error.message));
7379
});
7480

75-
const ready_unsub = vm.on('server-ready', (port, _base) => {
76-
base = _base;
81+
const ready_unsub = vm.on('server-ready', (port, base) => {
7782
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
8085
});
8186

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);
10890

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}`)
111100
}
101+
);
102+
const code = await unzip.onExit;
103+
if (code !== 0) {
104+
reject(new Error('Failed to initialize WebContainer'));
112105
}
113106

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();
120111

121-
await vm.run(
112+
async function run_dev() {
113+
const process = await vm.run(
122114
{ command: 'turbo', args: ['run', 'dev'] },
123115
{
124116
stdout: () => {
125117
if (!base) {
126118
reset_timeout();
127119
}
128120
},
129-
stderr: (data) => console.error(`[dev] ${data}`)
121+
stderr: (data) => {
122+
vite_error = true;
123+
console.error(`[dev] ${data}`);
124+
}
130125
}
131126
);
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+
});
132135
}
133136
});
134137

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-
142138
/**
143139
* Deletes old files and adds new ones
144140
* @param {import('$lib/types').Stub[]} stubs
@@ -148,65 +144,61 @@ export async function create(stubs) {
148144
/** @type {Function} */
149145
let resolve = () => {};
150146
running = new Promise((fulfil) => (resolve = fulfil));
147+
vite_error = false;
151148

152149
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);
154154

155155
for (const stub of stubs) {
156156
if (stub.type === 'file') {
157157
old.delete(stub.name);
158158
}
159159
}
160160

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.
162162
// We need to wait for it to finish before we can continue, else we might
163163
// 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 });
201193
}
202194

203-
await vm.loadFiles(convert_stubs_to_tree(stubs));
204-
195+
await vm.loadFiles(convert_stubs_to_tree(new_stubs));
205196
await promise;
206-
207197
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
208198

209199
resolve();
200+
201+
return will_restart || vite_error;
210202
}
211203

212204
/**
@@ -307,9 +299,15 @@ function to_file(stub) {
307299
}
308300

309301
/**
310-
*
311302
* @param {import('$lib/types').Stub[]} stubs
303+
* @returns {Map<string, string>}
312304
*/
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;
315313
}

src/lib/types/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export type Stub = FileStub | DirectoryStub;
2020

2121
export interface Adapter {
2222
base: string;
23-
reset(files: Array<Stub>): Promise<void>;
23+
/** Returns `false` if the reset was in such a way that a reload of the iframe isn't needed */
24+
reset(files: Array<Stub>): Promise<boolean>;
2425
update(file: Array<FileStub>): Promise<void>;
2526
destroy(): Promise<void>;
2627
}

src/routes/tutorial/[slug]/+page.svelte

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,19 +141,18 @@
141141
* @param {import('$lib/types').Stub[]} stubs
142142
*/
143143
async function reset_adapter(stubs) {
144+
let reload_iframe = true;
144145
if (adapter) {
145-
await adapter.reset(stubs);
146-
return adapter;
146+
reload_iframe = await adapter.reset(stubs);
147147
} else {
148148
const module = PUBLIC_USE_FILESYSTEM
149149
? await import('$lib/client/adapters/filesystem/index.js')
150150
: await import('$lib/client/adapters/webcontainer/index.js');
151151
152152
adapter = await module.create(stubs);
153+
set_iframe_src(adapter.base);
153154
}
154155
155-
set_iframe_src(adapter.base);
156-
157156
await new Promise((fulfil, reject) => {
158157
let called = false;
159158
@@ -181,6 +180,11 @@
181180
}, 10000);
182181
});
183182
183+
if (reload_iframe) {
184+
await new Promise((fulfil) => setTimeout(fulfil, 200));
185+
set_iframe_src(adapter.base);
186+
}
187+
184188
return adapter;
185189
}
186190
@@ -222,7 +226,6 @@
222226
async function load_files(stubs) {
223227
adapter = await reset_adapter(stubs);
224228
update_complete_states(stubs);
225-
set_iframe_src(adapter.base);
226229
}
227230
228231
/**

src/routes/tutorial/[slug]/Loading.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,15 @@
4343
<div class="loading" class:error>
4444
{#if error}
4545
{@html get_error_message(error)}
46-
<button on:click={() => dispatch('reload')}>Reload</button>
46+
<button
47+
on:click={() => {
48+
if (initial) {
49+
location.reload();
50+
} else {
51+
dispatch('reload');
52+
}
53+
}}>Reload</button
54+
>
4755
{:else}
4856
{#if initial}
4957
<p>initializing... this may take a few seconds</p>

0 commit comments

Comments
 (0)