|
22 | 22 | /** @type {import('$lib/types').FileStub} */ (data.section.a[data.section.focus])
|
23 | 23 | );
|
24 | 24 |
|
25 |
| - /** @type {Map<string, string>} */ |
26 |
| - let expected; |
27 |
| -
|
28 | 25 | /** @type {import('$lib/types').Stub[]}*/
|
29 | 26 | let current_stubs = [];
|
30 | 27 |
|
|
36 | 33 | /** @type {Error | null} */
|
37 | 34 | let error = null;
|
38 | 35 |
|
| 36 | + /** @type {Record<string, string>} */ |
| 37 | + let expected = {}; |
39 | 38 | /** @type {Record<string, boolean>}*/
|
40 | 39 | let complete_states = {};
|
41 |
| - let completed = false; |
42 |
| - let completing = false; |
| 40 | + $: completed = |
| 41 | + Object.keys(complete_states).length > 0 && Object.values(complete_states).every(Boolean); |
| 42 | +
|
43 | 43 | let path = '/';
|
44 | 44 |
|
45 | 45 | /** @type {Record<string, import('$lib/types').Stub>} */
|
|
129 | 129 | return destroy;
|
130 | 130 | });
|
131 | 131 |
|
132 |
| - afterNavigate(async () => { |
133 |
| - complete_states = {}; |
134 |
| - current_stubs = Object.values(data.section.a); |
135 |
| -
|
136 |
| - select( |
137 |
| - /** @type {import('$lib/types').FileStub} */ ( |
138 |
| - current_stubs.find((stub) => stub.name === data.section.focus) |
139 |
| - ) |
140 |
| - ); |
141 |
| -
|
142 |
| - completed = false; |
143 |
| -
|
144 |
| - load_exercise(); |
145 |
| - }); |
| 132 | + afterNavigate(load_exercise); |
146 | 133 |
|
147 | 134 | /**
|
148 | 135 | * Loads the adapter initially or resets it. This method can throw.
|
|
194 | 181 |
|
195 | 182 | async function load_exercise() {
|
196 | 183 | try {
|
| 184 | + current_stubs = Object.values(data.section.a); |
| 185 | + select( |
| 186 | + /** @type {import('$lib/types').FileStub} */ ( |
| 187 | + current_stubs.find((stub) => stub.name === data.section.focus) |
| 188 | + ) |
| 189 | + ); |
| 190 | +
|
197 | 191 | clearTimeout(timeout);
|
198 | 192 | loading = true;
|
199 | 193 |
|
200 |
| - // Load expected output first so we can compare it to the actual output to determine when it's completed |
201 |
| - await reset_adapter(Object.values(b)); |
202 |
| - expected = await get_transformed_modules(data.section.scope.prefix, Object.values(b)); |
| 194 | + expected = {}; |
| 195 | + complete_states = {}; |
| 196 | + for (const stub of Object.values(b)) { |
| 197 | + if (stub.type === 'file') { |
| 198 | + complete_states[stub.name] = false; |
| 199 | + expected[stub.name] = normalise(stub.contents); |
| 200 | + } |
| 201 | + } |
203 | 202 |
|
204 |
| - const stubs = Object.values(data.section.a); |
205 |
| - await load_files(stubs); |
| 203 | + await load_files(current_stubs); |
206 | 204 |
|
207 | 205 | loading = false;
|
208 | 206 | initial = false;
|
|
218 | 216 | */
|
219 | 217 | async function load_files(stubs) {
|
220 | 218 | adapter = await reset_adapter(stubs);
|
221 |
| - const actual = await get_transformed_modules(data.section.scope.prefix, stubs); |
| 219 | + update_complete_states(stubs); |
| 220 | + set_iframe_src(adapter.base); |
| 221 | + } |
222 | 222 |
|
223 |
| - for (const [name, transformed] of expected.entries()) { |
224 |
| - complete_states[name] = transformed === actual.get(name); |
225 |
| - } |
| 223 | + /** |
| 224 | + * @param {CustomEvent<import('$lib/types').FileStub>} event |
| 225 | + */ |
| 226 | + function update_stub(event) { |
| 227 | + const stub = event.detail; |
| 228 | + const index = current_stubs.findIndex((s) => s.name === stub.name); |
| 229 | + current_stubs[index] = stub; |
| 230 | + adapter?.update([stub]); |
| 231 | + update_complete_states([stub]); |
| 232 | + } |
226 | 233 |
|
227 |
| - set_iframe_src(adapter.base); |
| 234 | + /** |
| 235 | + * @param {import('$lib/types').Stub[]} stubs |
| 236 | + */ |
| 237 | + function update_complete_states(stubs) { |
| 238 | + for (const stub of stubs) { |
| 239 | + if (stub.type === 'file' && stub.name in complete_states) { |
| 240 | + complete_states[stub.name] = expected[stub.name] === normalise(stub.contents); |
| 241 | + if (dev) { |
| 242 | + compare(stub.name, normalise(stub.contents), expected[stub.name]); |
| 243 | + } |
| 244 | + } |
| 245 | + } |
228 | 246 | }
|
229 | 247 |
|
230 | 248 | /** @type {NodeJS.Timeout} */
|
|
247 | 265 | set_iframe_src(adapter.base + path);
|
248 | 266 | loading = false;
|
249 | 267 | }, 500);
|
250 |
| - } else if (e.data.type === 'hmr') { |
251 |
| - const transformed = await fetch_from_vite(e.data.data.map(({ path }) => path)); |
252 |
| -
|
253 |
| - for (const { name, code } of transformed) { |
254 |
| - const normalised = normalise(code); |
255 |
| - complete_states[name] = normalised === expected.get(name); |
256 |
| - if (dev) compare(name, normalised, expected.get(name)); |
257 |
| - } |
258 |
| -
|
259 |
| - completed = Object.values(complete_states).every((value) => value); |
260 | 268 | }
|
261 | 269 | }
|
262 | 270 |
|
263 |
| - /** |
264 |
| - * @param {string[]} names |
265 |
| - * @return {Promise<Array<{ name: string, code: string }>>} |
266 |
| - */ |
267 |
| - async function fetch_from_vite(names) { |
268 |
| - /** @type {Window} */ (iframe.contentWindow).postMessage({ type: 'fetch', names }, '*'); |
269 |
| -
|
270 |
| - return new Promise((fulfil, reject) => { |
271 |
| - window.addEventListener('message', function handler(e) { |
272 |
| - if (e.data.type === 'fetch-result') { |
273 |
| - fulfil(e.data.data); |
274 |
| - window.removeEventListener('message', handler); |
275 |
| - } |
276 |
| - }); |
277 |
| -
|
278 |
| - setTimeout(() => { |
279 |
| - reject(new Error('Timed out fetching files from Vite')); |
280 |
| - }, 5000); |
281 |
| - }); |
282 |
| - } |
283 |
| -
|
284 | 271 | /**
|
285 | 272 | * @param {string} name
|
286 | 273 | * @param {string} actual
|
|
296 | 283 | console.groupEnd();
|
297 | 284 | }
|
298 | 285 |
|
299 |
| - /** |
300 |
| - * @param {string} prefix |
301 |
| - * @param {import('$lib/types').Stub[]} stubs |
302 |
| - * @returns {Promise<Map<string, string>>} |
303 |
| - */ |
304 |
| - async function get_transformed_modules(prefix, stubs) { |
305 |
| - const names = stubs |
306 |
| - .filter((stub) => { |
307 |
| - if (stub.name === '/src/__client.js') return; |
308 |
| - if (stub.type !== 'file') return; |
309 |
| - if (!/\.(js|ts|svelte)$/.test(stub.name)) return; |
310 |
| -
|
311 |
| - return stub.name.startsWith(prefix); |
312 |
| - }) |
313 |
| - .map((stub) => stub.name); |
314 |
| -
|
315 |
| - const transformed = await fetch_from_vite(names); |
316 |
| -
|
317 |
| - const map = new Map(); |
318 |
| - transformed.forEach(({ name, code }) => { |
319 |
| - map.set(name, normalise(code)); |
320 |
| - }); |
321 |
| -
|
322 |
| - return map; |
323 |
| - } |
324 |
| -
|
325 | 286 | /** @param {string} code */
|
326 | 287 | function normalise(code) {
|
327 |
| - return code |
328 |
| - .replace(/add_location\([^)]+\)/g, 'add_location(...)') |
329 |
| - .replace(/\?[tv]=[a-zA-Z0-9]+/g, '') |
330 |
| - .replace(/[&?]svelte&type=style&lang\.css/, '') |
331 |
| - .replace(/\/\/# sourceMappingURL=.+/, ''); |
| 288 | + // TODO think about more sophisticated normalisation (e.g. truncate multiple newlines) |
| 289 | + return code.replace(/\s+/g, ' ').trim(); |
332 | 290 | }
|
333 | 291 |
|
334 | 292 | /** @param {string} src */
|
|
381 | 339 | <button
|
382 | 340 | class:completed
|
383 | 341 | disabled={Object.keys(data.section.b).length === 0}
|
384 |
| - on:click={async () => { |
385 |
| - completing = true; |
386 |
| -
|
387 |
| - completed = !completed; |
388 |
| - current_stubs = Object.values(completed ? b : data.section.a); |
389 |
| - adapter?.reset(current_stubs); |
390 |
| -
|
391 |
| - completing = false; |
| 342 | + on:click={() => { |
| 343 | + current_stubs = Object.values(completed ? data.section.a : b); |
| 344 | + load_files(current_stubs); |
392 | 345 | }}
|
393 | 346 | >
|
394 | 347 | {#if completed && Object.keys(data.section.b).length > 0}
|
|
400 | 353 | </section>
|
401 | 354 |
|
402 | 355 | <section class="editor-container" slot="b">
|
403 |
| - <Editor stubs={current_stubs} selected={$selected} {adapter} /> |
| 356 | + <Editor stubs={current_stubs} selected={$selected} on:change={update_stub} /> |
404 | 357 | <ImageViewer selected={$selected} />
|
405 | 358 | </section>
|
406 | 359 | </SplitPane>
|
|
0 commit comments