Skip to content

Commit c9b51f0

Browse files
committed
feat: use rollup web worker for Svelte tutorial
1 parent 205d791 commit c9b51f0

File tree

13 files changed

+315
-77
lines changed

13 files changed

+315
-77
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { Adapter, FileStub, Stub, Warning } from '$lib/tutorial';
2+
import Bundler from '@sveltejs/repl/bundler';
3+
import type { Writable } from 'svelte/store';
4+
// @ts-ignore package exports don't have types
5+
import * as yootils from 'yootils';
6+
7+
/** Rollup bundler singleton */
8+
let vm: Bundler;
9+
10+
/**
11+
* @param {import('svelte/store').Writable<any>} bundle
12+
* @param {import('svelte/store').Writable<Error | null>} error
13+
* @param {import('svelte/store').Writable<{ value: number, text: string }>} progress
14+
* @param {import('svelte/store').Writable<string[]>} logs
15+
* @param {import('svelte/store').Writable<Record<string, import('$lib/tutorial').Warning[]>>} warnings
16+
* @returns {Promise<import('$lib/tutorial').Adapter>}
17+
*/
18+
export async function create(
19+
bundle: Writable<any>,
20+
error: Writable<Error | null>, // TODO wire this up? Is this the correct place?
21+
progress: Writable<{ value: number; text: string }>,
22+
logs: Writable<string[]>, // TODO write to this somehow instead of the console viewer?
23+
warnings: Writable<Record<string, Warning[]>>
24+
): Promise<Adapter> {
25+
progress.set({ value: 0, text: 'loading files' });
26+
27+
let done = false;
28+
29+
vm = new Bundler({
30+
packages_url: 'https://unpkg.com',
31+
svelte_url: `https://unpkg.com/svelte@next`, // TODO remove @next once 5.0 is released
32+
// svelte_url: `${browser ? location.origin : ''}/svelte`, // TODO think about bringing back main-build for Playground?
33+
onstatus(val) {
34+
if (!done && val === null) {
35+
done = true;
36+
progress.set({ value: 1, text: 'ready' });
37+
}
38+
}
39+
});
40+
41+
progress.set({ value: 0.5, text: 'loading svelte compiler' });
42+
43+
/** Paths and contents of the currently loaded file stubs */
44+
let current_stubs = stubs_to_map([]);
45+
46+
async function compile() {
47+
const result = await vm.bundle(
48+
[...current_stubs.values()]
49+
// TODO we can probably remove all the SvelteKit specific stuff from the tutorial content once this settles down
50+
.filter((f): f is FileStub => f.name.startsWith('/src/lib/') && f.type === 'file')
51+
.map((f) => ({
52+
name: f.name.slice(9).split('.').slice(0, -1).join('.'),
53+
source: f.contents,
54+
type: f.name.split('.').pop() ?? 'svelte'
55+
}))
56+
);
57+
bundle.set(result);
58+
59+
const _warnings: Record<string, any> = {};
60+
for (const warning of result?.warnings ?? []) {
61+
const file = '/src/lib/' + warning.filename;
62+
_warnings[file] = _warnings[file] || [];
63+
_warnings[file].push(warning);
64+
}
65+
warnings.set(_warnings);
66+
}
67+
68+
const q = yootils.queue(1);
69+
70+
return {
71+
reset: (stubs) => {
72+
return q.add(async () => {
73+
current_stubs = stubs_to_map(stubs, current_stubs);
74+
75+
await compile();
76+
77+
return false;
78+
});
79+
},
80+
update: (file) => {
81+
return q.add(async () => {
82+
current_stubs.set(file.name, file);
83+
84+
await compile();
85+
86+
return false;
87+
});
88+
}
89+
};
90+
}
91+
92+
function stubs_to_map(files: Stub[], map = new Map<string, Stub>()) {
93+
for (const file of files) {
94+
map.set(file.name, file);
95+
}
96+
return map;
97+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
solution
2121
} from './state.js';
2222
import { text_files } from './shared';
23+
import OutputRollup from './OutputRollup.svelte';
24+
import { page } from '$app/stores';
2325
2426
export let data;
2527
@@ -315,7 +317,11 @@
315317
</section>
316318
317319
<section slot="b" class="preview">
318-
<Output exercise={data.exercise} {paused} />
320+
{#if /svelte$/.test($page.data.exercise.part.slug)}
321+
<OutputRollup />
322+
{:else}
323+
<Output exercise={data.exercise} {paused} />
324+
{/if}
319325
</section>
320326
</SplitPane>
321327
</section>

apps/svelte.dev/src/routes/tutorial/[slug]/Chrome.svelte

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
1-
<script>
2-
import { createEventDispatcher } from 'svelte';
3-
4-
/** @type {string} */
5-
export let path;
6-
7-
/** @type {boolean} */
8-
export let loading;
9-
10-
/** @type {string | null} */
11-
export let href;
1+
<script lang="ts">
2+
interface Props {
3+
path?: string;
4+
loading?: boolean;
5+
href?: string | null;
6+
change?: (value: { value: string }) => void;
7+
refresh?: () => void;
8+
toggle_terminal?: () => void;
9+
}
1210
13-
const dispatch = createEventDispatcher();
11+
let {
12+
path = '/',
13+
loading = false,
14+
href = null,
15+
change,
16+
refresh,
17+
toggle_terminal
18+
}: Props = $props();
1419
</script>
1520

16-
<div class="chrome" class:loading>
17-
<button
18-
disabled={loading}
19-
class="reload icon"
20-
on:click={() => dispatch('refresh')}
21-
aria-label="reload"
21+
<div class="chrome">
22+
<button disabled={loading || !refresh} class="reload icon" onclick={refresh} aria-label="reload"
2223
></button>
2324

2425
<input
25-
disabled={loading}
26+
disabled={loading || !change}
2627
aria-label="URL"
2728
value={path}
28-
on:change={(e) => {
29-
dispatch('change', { value: e.currentTarget.value });
29+
onchange={(e) => {
30+
change?.({ value: e.currentTarget.value });
3031
}}
31-
on:keydown={(e) => {
32+
onkeydown={(e) => {
3233
if (e.key !== 'Enter') return;
3334

34-
dispatch('change', { value: e.currentTarget.value });
35+
change?.({ value: e.currentTarget.value });
3536
e.currentTarget.blur();
3637
}}
3738
/>
@@ -45,9 +46,9 @@
4546
></a>
4647

4748
<button
48-
disabled={loading}
49+
disabled={loading || !toggle_terminal}
4950
class="terminal icon"
50-
on:click={() => dispatch('toggle_terminal')}
51+
onclick={() => toggle_terminal?.()}
5152
aria-label="toggle terminal"
5253
></button>
5354
</div>
@@ -90,7 +91,7 @@
9091
background-size: 2rem;
9192
}
9293
93-
.loading a {
94+
a:not([href]) {
9495
opacity: 0.5;
9596
}
9697

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,6 @@
191191
// could be false if onMount returned early
192192
select_state($selected_name);
193193
}
194-
195-
// clear warnings
196-
warnings.set({});
197194
});
198195
</script>
199196

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

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
1-
<script>
2-
import { page } from '$app/stores';
3-
import { Icon } from '@sveltejs/site-kit/components';
1+
<script lang="ts">
42
import { load_webcontainer, reset } from './adapter.js';
53
import { files } from './state.js';
64
7-
/** @type {boolean} */
8-
export let initial;
9-
10-
/** @type {Error | null} */
11-
export let error;
12-
13-
/** @type {number} */
14-
export let progress;
15-
16-
/** @type {string} */
17-
export let status;
5+
interface Props {
6+
initial: boolean;
7+
error?: Error | null;
8+
progress: number;
9+
status: string;
10+
}
1811
19-
$: is_svelte = /Part (1|2)/.test($page.data.exercise.part.title);
12+
let { initial, error = null, progress, status }: Props = $props();
2013
</script>
2114

2215
<div class="loading">
@@ -73,9 +66,9 @@
7366
<p>
7467
If this is not the case, you can try loading it by <button
7568
type="button"
76-
on:click={async () => {
69+
onclick={async () => {
7770
error = null;
78-
load_webcontainer();
71+
load_webcontainer(true);
7972
await reset($files);
8073
}}>clicking here</button
8174
>.
@@ -85,12 +78,6 @@
8578
We couldn't start the app. Please ensure third party cookies are enabled for this site.
8679
</p>
8780
{/if}
88-
89-
{#if is_svelte}
90-
<a href="https://svelte.dev/tutorial/{$page.data.exercise.slug}">
91-
Or go to the legacy svelte tutorial instead <Icon name="arrow-right" />
92-
</a>
93-
{/if}
9481
</div>
9582

9683
<small>{error.message}</small>

apps/svelte.dev/src/routes/tutorial/[slug]/Output.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,15 @@
116116
{path}
117117
{loading}
118118
href={$base && $base + path}
119-
on:refresh={() => {
119+
refresh={() => {
120120
set_iframe_src($base + path);
121121
}}
122-
on:toggle_terminal={() => {
122+
toggle_terminal={() => {
123123
terminal_visible = !terminal_visible;
124124
}}
125-
on:change={(e) => {
125+
change={(e) => {
126126
if ($base) {
127-
const url = new URL(e.detail.value, $base);
127+
const url = new URL(e.value, $base);
128128
path = url.pathname + url.search + url.hash;
129129
set_iframe_src($base + path);
130130
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<script lang="ts">
2+
import { browser } from '$app/environment';
3+
// @ts-expect-error TODO types
4+
import Viewer from '@sveltejs/repl/viewer';
5+
import { theme } from '@sveltejs/site-kit/stores';
6+
import Chrome from './Chrome.svelte';
7+
import Loading from './Loading.svelte';
8+
import { bundle, logs, progress } from './adapter.js';
9+
10+
let initial = $state(true);
11+
let terminal_visible = $state(false);
12+
</script>
13+
14+
<!-- TODO: refresh iframe somehow? somehow use terminal instead of console view of REPL viewer? -->
15+
<Chrome />
16+
17+
<div class="content">
18+
{#if browser}
19+
<Viewer {bundle} theme={$theme.current} />
20+
{/if}
21+
22+
{#if $progress.value !== 1}
23+
<!-- TODO is there any startup error we should worry about and forward to the Loading component? -->
24+
<Loading {initial} progress={$progress.value} status={$progress.text} />
25+
{/if}
26+
27+
<div class="terminal" class:visible={terminal_visible}>
28+
{#each $logs as log}
29+
<div>{@html log}</div>
30+
{/each}
31+
</div>
32+
</div>
33+
34+
<style>
35+
.content {
36+
display: flex;
37+
flex-direction: column;
38+
position: relative;
39+
min-height: 0;
40+
height: 100%;
41+
max-height: 100%;
42+
background: var(--sk-back-2);
43+
--menu-width: 5.4rem;
44+
}
45+
46+
.terminal {
47+
position: absolute;
48+
left: 0;
49+
bottom: 0;
50+
width: 100%;
51+
height: 80%;
52+
font-family: var(--font-mono);
53+
font-size: var(--sk-text-xs);
54+
padding: 1rem;
55+
background: rgba(255, 255, 255, 0.5);
56+
transform: translate(0, 100%);
57+
transition: transform 0.3s;
58+
backdrop-filter: blur(3px);
59+
overflow-y: auto;
60+
}
61+
62+
.terminal::after {
63+
--thickness: 6px;
64+
--shadow: transparent;
65+
content: '';
66+
display: block;
67+
position: absolute;
68+
width: 100%;
69+
height: var(--thickness);
70+
left: 0;
71+
top: calc(-1 * var(--thickness));
72+
background-image: linear-gradient(to bottom, transparent, var(--shadow));
73+
pointer-events: none;
74+
}
75+
76+
.terminal.visible {
77+
transform: none;
78+
}
79+
80+
.terminal.visible::after {
81+
--shadow: rgba(0, 0, 0, 0.05);
82+
}
83+
84+
@media (prefers-color-scheme: dark) {
85+
.terminal {
86+
background: rgba(0, 0, 0, 0.5);
87+
}
88+
}
89+
</style>

0 commit comments

Comments
 (0)