Skip to content

Commit 0aea741

Browse files
Rich-HarrisRich Harris
and
Rich Harris
authored
overhaul adapter code (sveltejs#249)
* tidy up * restart process immediately when it dies * create adapter in onMount * make adapter a singleton * WIP refactor * add an event mechanism for reloading iframe * call adapter methods directly * separate state from events * add some HMR support * unify types, remove destroy method * preserve loading screen until page is ready * expand new folders by default * remove unnecessary pagehide stuff * make update(...) take a single file * fix hella weird bug with process not exiting if vite and svelte configs are both written simultaneously * minor tweak * simplify * move collapsed state to the filetree * remove some more indirection * unused import * use sets for editing constraints * move logic to the server * move editing_constraints out of state * fix * create multiple independent stores so we can e.g. SSR the filetree * tweak * consistency * use correct path, prevent some errors --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 457da8a commit 0aea741

File tree

14 files changed

+417
-564
lines changed

14 files changed

+417
-564
lines changed

content/tutorial/common/src/routes/+page.svelte

Whitespace-only changes.

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @param {import('$lib/types').Stub[]} stubs
33
* @param {(progress: number, status: string) => void} cb
4-
* @returns {Promise<import('$lib/types').AdapterInternal>}
4+
* @returns {Promise<import('$lib/types').Adapter>}
55
*/
66
export async function create(stubs, cb) {
77
const res = await fetch('/backend', {
@@ -56,10 +56,6 @@ export async function create(stubs, cb) {
5656
await new Promise((f) => setTimeout(f, 100)); // wait for chokidar
5757

5858
return will_restart_vite_dev_server(stubs);
59-
},
60-
61-
async destroy() {
62-
navigator.sendBeacon(`/backend/destroy?id=${id}`);
6359
}
6460
};
6561
}

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

Lines changed: 85 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@ function console_stream(label) {
1616
}
1717

1818
/**
19-
* @param {import('$lib/types').Stub[]} stubs
20-
* @param {(progress: number, status: string) => void} callback
21-
* @returns {Promise<import('$lib/types').AdapterInternal>}
19+
* @param {import('svelte/store').Writable<string | null>} base
20+
* @param {import('svelte/store').Writable<Error | null>} error
21+
* @param {import('svelte/store').Writable<{ value: number, text: string }>} progress
22+
* @returns {Promise<import('$lib/types').Adapter>}
2223
*/
23-
export async function create(stubs, callback) {
24+
export async function create(base, error, progress) {
2425
if (/safari/i.test(navigator.userAgent) && !/chrome/i.test(navigator.userAgent)) {
2526
throw new Error('WebContainers are not supported by Safari');
2627
}
2728

28-
callback(0, 'loading files');
29+
progress.set({ value: 0, text: 'loading files' });
2930

3031
/**
3132
* Keeps track of the latest create/reset to ensure things are not processed in parallel.
@@ -35,27 +36,26 @@ export async function create(stubs, callback) {
3536
let running;
3637

3738
/** Paths and contents of the currently loaded file stubs */
38-
let current_stubs = stubs_to_map(stubs);
39+
let current_stubs = stubs_to_map([]);
3940

4041
/** @type {boolean} Track whether there was an error from vite dev server */
4142
let vite_error = false;
4243

43-
callback(1 / 5, 'booting webcontainer');
44+
progress.set({ value: 1 / 5, text: 'booting webcontainer' });
4445
vm = await WebContainer.boot();
4546

46-
callback(2 / 5, 'writing virtual files');
47+
progress.set({ value: 2 / 5, text: 'writing virtual files' });
4748
const common = await ready;
4849
await vm.mount({
4950
'common.zip': {
5051
file: { contents: new Uint8Array(common.zipped) }
5152
},
5253
'unzip.cjs': {
5354
file: { contents: common.unzip }
54-
},
55-
...convert_stubs_to_tree(stubs)
55+
}
5656
});
5757

58-
callback(3 / 5, 'unzipping files');
58+
progress.set({ value: 3 / 5, text: 'unzipping files' });
5959
const unzip = await vm.spawn('node', ['unzip.cjs']);
6060
unzip.output.pipeTo(console_stream('unzip'));
6161
const code = await unzip.exit;
@@ -66,41 +66,51 @@ export async function create(stubs, callback) {
6666

6767
await vm.spawn('chmod', ['a+x', 'node_modules/vite/bin/vite.js']);
6868

69-
callback(4 / 5, 'starting dev server');
70-
const base = await new Promise(async (fulfil, reject) => {
71-
const error_unsub = vm.on('error', (error) => {
72-
error_unsub();
73-
reject(new Error(error.message));
74-
});
69+
vm.on('server-ready', (_port, url) => {
70+
base.set(url);
71+
});
7572

76-
const ready_unsub = vm.on('server-ready', (_port, base) => {
77-
ready_unsub();
78-
callback(5 / 5, 'ready');
79-
fulfil(base); // this will be the last thing that happens if everything goes well
80-
});
73+
vm.on('error', ({ message }) => {
74+
error.set(new Error(message));
75+
});
8176

82-
await run_dev();
77+
let launched = false;
8378

84-
async function run_dev() {
85-
const process = await vm.spawn('turbo', ['run', 'dev']);
79+
async function launch() {
80+
if (launched) return;
81+
launched = true;
8682

87-
// TODO differentiate between stdout and stderr (sets `vite_error` to `true`)
88-
// https://github.com/stackblitz/webcontainer-core/issues/971
89-
process.output.pipeTo(console_stream('dev'));
83+
progress.set({ value: 4 / 5, text: 'starting dev server' });
9084

91-
// keep restarting dev server (can crash in case of illegal +files for example)
92-
process.exit.then((code) => {
93-
if (code !== 0) {
94-
setTimeout(() => {
95-
run_dev();
96-
}, 2000);
97-
}
85+
await new Promise(async (fulfil, reject) => {
86+
const error_unsub = vm.on('error', (error) => {
87+
error_unsub();
88+
reject(new Error(error.message));
9889
});
99-
}
100-
});
90+
91+
const ready_unsub = vm.on('server-ready', (_port, base) => {
92+
ready_unsub();
93+
progress.set({ value: 5 / 5, text: 'ready' });
94+
fulfil(base); // this will be the last thing that happens if everything goes well
95+
});
96+
97+
await run_dev();
98+
99+
async function run_dev() {
100+
const process = await vm.spawn('turbo', ['run', 'dev']);
101+
102+
// TODO differentiate between stdout and stderr (sets `vite_error` to `true`)
103+
// https://github.com/stackblitz/webcontainer-core/issues/971
104+
process.output.pipeTo(console_stream('dev'));
105+
106+
// keep restarting dev server (can crash in case of illegal +files for example)
107+
await process.exit;
108+
run_dev();
109+
}
110+
});
111+
}
101112

102113
return {
103-
base,
104114
reset: async (stubs) => {
105115
await running;
106116
/** @type {Function} */
@@ -146,7 +156,7 @@ export async function create(stubs, callback) {
146156
// For some reason, server-ready is fired again when the vite dev server is restarted.
147157
// We need to wait for it to finish before we can continue, else we might
148158
// request files from Vite before it's ready, leading to a timeout.
149-
const will_restart = will_restart_vite_dev_server(to_write);
159+
const will_restart = launched && to_write.some(will_restart_vite_dev_server);
150160
const promise = will_restart
151161
? new Promise((fulfil, reject) => {
152162
const error_unsub = vm.on('error', (error) => {
@@ -190,60 +200,57 @@ export async function create(stubs, callback) {
190200

191201
// Also trigger a reload of the iframe in case new files were added / old ones deleted,
192202
// because that can result in a broken UI state
193-
return will_restart || vite_error || to_delete.length > 0 || added_new_file;
203+
const should_reload = !launched || will_restart || vite_error || to_delete.length > 0;
204+
// `|| added_new_file`, but I don't actually think that's necessary?
205+
206+
await launch();
207+
208+
return should_reload;
194209
},
195-
update: async (stubs) => {
210+
update: async (file) => {
196211
await running;
197212

198213
/** @type {import('@webcontainer/api').FileSystemTree} */
199214
const root = {};
200215

201-
for (const stub of stubs) {
202-
let tree = root;
203-
204-
const path = stub.name.split('/').slice(1);
205-
const basename = /** @type {string} */ (path.pop());
216+
let tree = root;
206217

207-
for (const part of path) {
208-
if (!tree[part]) {
209-
/** @type {import('@webcontainer/api').FileSystemTree} */
210-
const directory = {};
218+
const path = file.name.split('/').slice(1);
219+
const basename = /** @type {string} */ (path.pop());
211220

212-
tree[part] = {
213-
directory
214-
};
215-
}
221+
for (const part of path) {
222+
if (!tree[part]) {
223+
/** @type {import('@webcontainer/api').FileSystemTree} */
224+
const directory = {};
216225

217-
tree = /** @type {import('@webcontainer/api').DirectoryNode} */ (tree[part]).directory;
226+
tree[part] = {
227+
directory
228+
};
218229
}
219230

220-
tree[basename] = to_file(stub);
231+
tree = /** @type {import('@webcontainer/api').DirectoryNode} */ (tree[part]).directory;
221232
}
222233

234+
tree[basename] = to_file(file);
235+
223236
await vm.mount(root);
224237

225-
stubs_to_map(stubs, current_stubs);
238+
current_stubs.set(file.name, file);
226239

227240
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
228241

229-
return will_restart_vite_dev_server(stubs);
230-
},
231-
destroy: async () => {
232-
vm.teardown();
242+
return will_restart_vite_dev_server(file);
233243
}
234244
};
235245
}
236246

237247
/**
238-
* @param {import('$lib/types').Stub[]} stubs
248+
* @param {import('$lib/types').Stub} file
239249
*/
240-
function will_restart_vite_dev_server(stubs) {
241-
return stubs.some(
242-
(stub) =>
243-
stub.type === 'file' &&
244-
(stub.name === '/vite.config.js' ||
245-
stub.name === '/svelte.config.js' ||
246-
stub.name === '/.env')
250+
function will_restart_vite_dev_server(file) {
251+
return (
252+
file.type === 'file' &&
253+
(file.name === '/vite.config.js' || file.name === '/svelte.config.js' || file.name === '/.env')
247254
);
248255
}
249256

@@ -272,11 +279,11 @@ function convert_stubs_to_tree(stubs, depth = 1) {
272279
return tree;
273280
}
274281

275-
/** @param {import('$lib/types').FileStub} stub */
276-
function to_file(stub) {
282+
/** @param {import('$lib/types').FileStub} file */
283+
function to_file(file) {
277284
// special case
278-
if (stub.name === '/src/app.html') {
279-
const contents = stub.contents.replace(
285+
if (file.name === '/src/app.html') {
286+
const contents = file.contents.replace(
280287
'</head>',
281288
'<script type="module" src="/src/__client.js"></script></head>'
282289
);
@@ -286,20 +293,20 @@ function to_file(stub) {
286293
};
287294
}
288295

289-
const contents = stub.text ? stub.contents : base64.toByteArray(stub.contents);
296+
const contents = file.text ? file.contents : base64.toByteArray(file.contents);
290297

291298
return {
292299
file: { contents }
293300
};
294301
}
295302

296303
/**
297-
* @param {import('$lib/types').Stub[]} stubs
304+
* @param {import('$lib/types').Stub[]} files
298305
* @returns {Map<string, import('$lib/types').Stub>}
299306
*/
300-
function stubs_to_map(stubs, map = new Map()) {
301-
for (const stub of stubs) {
302-
map.set(stub.name, stub);
307+
function stubs_to_map(files, map = new Map()) {
308+
for (const file of files) {
309+
map.set(file.name, file);
303310
}
304311
return map;
305312
}

src/lib/server/content.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export function get_exercise(slug) {
101101
}
102102

103103
const b = walk(`${dir}/app-b`);
104+
const has_solution = Object.keys(b).length > 0;
104105

105106
const part_meta = json(`content/tutorial/${part_dir}/meta.json`);
106107
const chapter_meta = json(`content/tutorial/${part_dir}/${chapter_dir}/meta.json`);
@@ -152,6 +153,36 @@ export function get_exercise(slug) {
152153
};
153154
}
154155

156+
const editing_constraints = {
157+
create: new Set(exercise_meta.editing_constraints?.create ?? []),
158+
remove: new Set(exercise_meta.editing_constraints?.remove ?? [])
159+
};
160+
161+
const solution = { ...a };
162+
163+
for (const stub of Object.values(b)) {
164+
if (stub.type === 'file' && stub.contents.startsWith('__delete')) {
165+
// remove file
166+
editing_constraints.remove.add(stub.name);
167+
delete solution[stub.name];
168+
} else if (stub.name.endsWith('/__delete')) {
169+
// remove directory
170+
const parent = stub.name.slice(0, stub.name.lastIndexOf('/'));
171+
editing_constraints.remove.add(parent);
172+
delete solution[parent];
173+
for (const k in solution) {
174+
if (k.startsWith(parent + '/')) {
175+
delete solution[k];
176+
}
177+
}
178+
} else {
179+
if (!solution[stub.name]) {
180+
editing_constraints.create.add(stub.name);
181+
}
182+
solution[stub.name] = stub;
183+
}
184+
}
185+
155186
return {
156187
part: {
157188
slug: part_dir,
@@ -169,18 +200,16 @@ export function get_exercise(slug) {
169200
prev,
170201
next,
171202
dir,
172-
editing_constraints: {
173-
create: exercise_meta.editing_constraints?.create ?? [],
174-
remove: exercise_meta.editing_constraints?.remove ?? []
175-
},
203+
editing_constraints,
176204
html: transform(markdown, {
177205
codespan: (text) =>
178206
filenames.size > 1 && filenames.has(text)
179207
? `<code data-file="${scope.prefix + text}">${text}</code>`
180208
: `<code>${text}</code>`
181209
}),
182210
a,
183-
b
211+
b: solution,
212+
has_solution
184213
};
185214
}
186215

@@ -218,7 +247,7 @@ function extract_frontmatter(markdown, dir) {
218247
* exclude?: string[]
219248
* }} options
220249
*/
221-
export function walk(cwd, options = {}) {
250+
function walk(cwd, options = {}) {
222251
/** @type {Record<string, import('$lib/types').FileStub | import('$lib/types').DirectoryStub>} */
223252
const result = {};
224253

0 commit comments

Comments
 (0)