Skip to content

Commit e594910

Browse files
authored
Merge pull request #42 from tomoam/update-up-to-20230510
2023/05/10 迄の更新に追従
2 parents 94f9fb2 + a48b733 commit e594910

File tree

7 files changed

+825
-43
lines changed

7 files changed

+825
-43
lines changed

content/tutorial/03-sveltekit/06-forms/04-progressive-enhancement/app-b/src/routes/+page.svelte

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,14 @@
5656
flex: 1;
5757
}
5858
59-
.saving {
59+
button {
60+
border: none;
61+
background: url(./remove.svg) no-repeat 50% 50%;
62+
background-size: 1rem 1rem;
63+
cursor: pointer;
64+
height: 100%;
65+
aspect-ratio: 1;
6066
opacity: 0.5;
67+
transition: opacity 0.2s;
6168
}
6269
</style>

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

Lines changed: 69 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import AnsiToHtml from 'ansi-to-html';
44
import * as yootils from 'yootils';
55
import { escape_html, get_depth } from '../../../utils.js';
66
import { ready } from '../common/index.js';
7+
import { isWebContainerSupported } from './utils.js';
78

89
/**
910
* @typedef {import("../../../../routes/tutorial/[slug]/state.js").CompilerWarning} CompilerWarning
@@ -25,13 +26,15 @@ let vm;
2526
* @returns {Promise<import('$lib/types').Adapter>}
2627
*/
2728
export async function create(base, error, progress, logs, warnings) {
28-
if (/safari/i.test(navigator.userAgent) && !/chrome/i.test(navigator.userAgent)) {
29-
throw new Error('WebContainers are not supported by Safari');
29+
if (!isWebContainerSupported()) {
30+
throw new Error('WebContainers are not supported by Safari 16.3 or earlier');
3031
}
3132

3233
progress.set({ value: 0, text: 'loading files' });
3334

3435
const q = yootils.queue(1);
36+
/** @type {Map<string, Array<import('$lib/types').FileStub>>} */
37+
const q_per_file = new Map();
3538

3639
/** Paths and contents of the currently loaded file stubs */
3740
let current_stubs = stubs_to_map([]);
@@ -203,25 +206,9 @@ export async function create(base, error, progress, logs, warnings) {
203206
// For some reason, server-ready is fired again when the vite dev server is restarted.
204207
// We need to wait for it to finish before we can continue, else we might
205208
// request files from Vite before it's ready, leading to a timeout.
206-
const will_restart = launched && to_write.some(will_restart_vite_dev_server);
207-
const promise = will_restart
208-
? new Promise((fulfil, reject) => {
209-
const error_unsub = vm.on('error', (error) => {
210-
error_unsub();
211-
reject(new Error(error.message));
212-
});
213-
214-
const ready_unsub = vm.on('server-ready', (port, base) => {
215-
ready_unsub();
216-
console.log(`server ready on port ${port} at ${performance.now()}: ${base}`);
217-
fulfil(undefined);
218-
});
219-
220-
setTimeout(() => {
221-
reject(new Error('Timed out resetting WebContainer'));
222-
}, 10000);
223-
})
224-
: Promise.resolve();
209+
const will_restart = launched &&
210+
(to_write.some(is_config) || to_delete.some(is_config_path));
211+
const promise = will_restart ? wait_for_restart_vite() : Promise.resolve();
225212

226213
for (const file of to_delete) {
227214
await vm.fs.rm(file, { force: true, recursive: true });
@@ -241,6 +228,15 @@ export async function create(base, error, progress, logs, warnings) {
241228
});
242229
},
243230
update: (file) => {
231+
232+
let queue = q_per_file.get(file.name);
233+
if (queue) {
234+
queue.push(file);
235+
return Promise.resolve(false);
236+
}
237+
238+
q_per_file.set(file.name, queue = [file]);
239+
244240
return q.add(async () => {
245241
/** @type {import('@webcontainer/api').FileSystemTree} */
246242
const root = {};
@@ -263,22 +259,35 @@ export async function create(base, error, progress, logs, warnings) {
263259
tree = /** @type {import('@webcontainer/api').DirectoryNode} */ (tree[part]).directory;
264260
}
265261

266-
tree[basename] = to_file(file);
262+
const will_restart = is_config(file);
263+
264+
while (queue && queue.length > 0) {
267265

268-
// initialize warnings of this file
269-
$warnings[file.name] = [];
270-
schedule_to_update_warning(100);
266+
// if the file is updated many times rapidly, get the most recently updated one
267+
const file = /** @type {import('$lib/types').FileStub} */ (queue.pop());
268+
queue.length = 0
271269

272-
await vm.mount(root);
270+
tree[basename] = to_file(file);
273271

274-
current_stubs.set(file.name, file);
272+
// initialize warnings of this file
273+
$warnings[file.name] = [];
274+
schedule_to_update_warning(100);
275+
276+
await vm.mount(root);
277+
278+
if (will_restart) await wait_for_restart_vite();
279+
280+
current_stubs.set(file.name, file);
281+
282+
// we need to stagger sequential updates, just enough that the HMR
283+
// wires don't get crossed. 50ms seems to be enough of a delay
284+
// to avoid glitches without noticeably affecting update speed
285+
await new Promise((f) => setTimeout(f, 50));
286+
}
275287

276-
// we need to stagger sequential updates, just enough that the HMR
277-
// wires don't get crossed. 50ms seems to be enough of a delay
278-
// to avoid glitches without noticeably affecting update speed
279-
await new Promise((f) => setTimeout(f, 50));
288+
q_per_file.delete(file.name)
280289

281-
return will_restart_vite_dev_server(file);
290+
return will_restart;
282291
});
283292
}
284293
};
@@ -287,11 +296,34 @@ export async function create(base, error, progress, logs, warnings) {
287296
/**
288297
* @param {import('$lib/types').Stub} file
289298
*/
290-
function will_restart_vite_dev_server(file) {
291-
return (
292-
file.type === 'file' &&
293-
(file.name === '/vite.config.js' || file.name === '/svelte.config.js' || file.name === '/.env')
294-
);
299+
function is_config(file) {
300+
return file.type === 'file' && is_config_path(file.name);
301+
}
302+
303+
/**
304+
* @param {string} path
305+
*/
306+
function is_config_path(path) {
307+
return ['/vite.config.js', '/svelte.config.js', '/.env'].includes(path);
308+
}
309+
310+
function wait_for_restart_vite() {
311+
return new Promise((fulfil, reject) => {
312+
const error_unsub = vm.on('error', (error) => {
313+
error_unsub();
314+
reject(new Error(error.message));
315+
});
316+
317+
const ready_unsub = vm.on('server-ready', (port, base) => {
318+
ready_unsub();
319+
console.log(`server ready on port ${port} at ${performance.now()}: ${base}`);
320+
fulfil(undefined);
321+
});
322+
323+
setTimeout(() => {
324+
reject(new Error('Timed out resetting WebContainer'));
325+
}, 10000);
326+
});
295327
}
296328

297329
/**
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Checks if WebContainer is supported on the current browser.
3+
* This function is borrowed from [stackblitz/webcontainer-docs](https://github.com/stackblitz/webcontainer-docs/blob/369dd58b2749b085ed7642f22108a9bcbcd68fc4/docs/.vitepress/theme/components/Examples/WCEmbed/utils.ts#L4-L29)
4+
*/
5+
export function isWebContainerSupported() {
6+
const hasSharedArrayBuffer = 'SharedArrayBuffer' in window;
7+
const looksLikeChrome = navigator.userAgent.toLowerCase().includes('chrome');
8+
const looksLikeFirefox = navigator.userAgent.includes('Firefox');
9+
const looksLikeSafari = navigator.userAgent.includes('Safari');
10+
11+
if (hasSharedArrayBuffer && (looksLikeChrome || looksLikeFirefox)) {
12+
return true;
13+
}
14+
15+
if (hasSharedArrayBuffer && looksLikeSafari) {
16+
// we only support Safari 16.4 and up so we check for the version here
17+
const match = navigator.userAgent.match(/Version\/(\d+)\.(\d+) (?:Mobile\/.*?)?Safari/);
18+
const majorVersion = match ? Number(match?.[1]) : 0;
19+
const minorVersion = match ? Number(match?.[2]) : 0;
20+
21+
return majorVersion > 16 || (majorVersion === 16 && minorVersion >= 4);
22+
}
23+
24+
// Allow overriding the support check with localStorage.webcontainer_any_ua = 1
25+
try {
26+
return Boolean(localStorage.getItem('webcontainer_any_ua'));
27+
} catch {
28+
return false;
29+
}
30+
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { afterNavigate, beforeNavigate } from '$app/navigation';
1818
import { files, selected_file, selected_name, update_file } from './state.js';
1919
import { warnings } from './adapter.js';
20+
import { autocomplete_for_svelte } from './autocompletion.js';
2021
import './codemirror.css';
2122
2223
/** @type {HTMLDivElement} */
@@ -127,16 +128,16 @@
127128
let lang;
128129
129130
if (file.name.endsWith('.js') || file.name.endsWith('.json')) {
130-
lang = javascript();
131+
lang = [javascript()];
131132
} else if (file.name.endsWith('.html')) {
132-
lang = html();
133+
lang = [html()];
133134
} else if (file.name.endsWith('.svelte')) {
134-
lang = svelte();
135+
lang = [svelte(), ...autocomplete_for_svelte()];
135136
}
136137
137138
state = EditorState.create({
138139
doc: file.contents,
139-
extensions: lang ? [...extensions, lang] : extensions
140+
extensions: lang ? [...extensions, ...lang] : extensions
140141
});
141142
142143
editor_states.set(file.name, state);

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script>
2+
import { isWebContainerSupported } from "$lib/client/adapters/webcontainer/utils.js";
3+
24
/** @type {boolean} */
35
export let initial;
46
@@ -14,7 +16,7 @@
1416

1517
<div class="loading" class:error>
1618
{#if error}
17-
{#if /safari/i.test(navigator.userAgent) && !/chrome/i.test(navigator.userAgent)}
19+
{#if !isWebContainerSupported()}
1820
<p>This app requires modern web platform features. Please use a browser other than Safari.</p>
1921
{:else}
2022
<small>{error.message}</small>

0 commit comments

Comments
 (0)