diff --git a/packages/editor/src/lib/Workspace.svelte.ts b/packages/editor/src/lib/Workspace.svelte.ts index 9054c08e72..ffa3c915a4 100644 --- a/packages/editor/src/lib/Workspace.svelte.ts +++ b/packages/editor/src/lib/Workspace.svelte.ts @@ -32,10 +32,10 @@ export type Item = File | Directory; export interface Compiled { error: CompileError | null; - result: CompileResult; + result: CompileResult | null; migration: { code: string; - }; + } | null; } function is_file(item: Item): item is File { @@ -85,6 +85,7 @@ export class Workspace { }); compiled = $state>({}); + #svelte_version: string; #readonly = false; // TODO do we need workspaces for readonly stuff? #files = $state.raw([]); #current = $state.raw() as File; @@ -99,17 +100,20 @@ export class Workspace { constructor( files: Item[], { + svelte_version = 'latest', initial, readonly = false, onupdate, onreset }: { + svelte_version?: string; initial?: string; readonly?: boolean; onupdate?: (file: File) => void; onreset?: (items: Item[]) => void; } = {} ) { + this.#svelte_version = svelte_version; this.#readonly = readonly; this.set(files, initial); @@ -315,7 +319,7 @@ export class Workspace { this.modified[file.name] = true; if (BROWSER && is_svelte_file(file)) { - compile_file(file, this.compiler_options).then((compiled) => { + compile_file(file, this.#svelte_version, this.compiler_options).then((compiled) => { this.compiled[file.name] = compiled; }); } @@ -430,7 +434,7 @@ export class Workspace { seen.push(file.name); - compile_file(file, this.compiler_options).then((compiled) => { + compile_file(file, this.#svelte_version, this.compiler_options).then((compiled) => { this.compiled[file.name] = compiled; }); } diff --git a/packages/editor/src/lib/compile-worker/index.ts b/packages/editor/src/lib/compile-worker/index.ts index 07591969dd..a6b1eada70 100644 --- a/packages/editor/src/lib/compile-worker/index.ts +++ b/packages/editor/src/lib/compile-worker/index.ts @@ -1,9 +1,8 @@ import { BROWSER } from 'esm-env'; import CompileWorker from './worker?worker'; import type { Compiled, File } from '../Workspace.svelte'; -import type { CompileOptions } from 'svelte/compiler'; -const callbacks = new Map void>(); +const callbacks = new Map void>>(); let worker: Worker; @@ -13,27 +12,50 @@ if (BROWSER) { worker = new CompileWorker(); worker.addEventListener('message', (event) => { - const callback = callbacks.get(event.data.id); - - if (callback) { - callback(event.data.payload); - callbacks.delete(event.data.id); + const { filename, id, payload } = event.data; + const file_callbacks = callbacks.get(filename); + + if (file_callbacks) { + const callback = file_callbacks.get(id); + if (callback) { + callback(payload); + file_callbacks.delete(id); + + for (const [other_id, callback] of file_callbacks) { + if (id > other_id) { + callback(payload); + file_callbacks.delete(other_id); + } + } + + if (file_callbacks.size === 0) { + callbacks.delete(filename); + } + } } }); } export function compile_file( file: File, + version: string, options: { generate: 'client' | 'server'; dev: boolean } ): Promise { // @ts-ignore if (!BROWSER) return; let id = uid++; + const filename = file.name; + + if (!callbacks.has(filename)) { + callbacks.set(filename, new Map()); + } + + const file_callbacks = callbacks.get(filename)!; - worker.postMessage({ id, file, options }); + worker.postMessage({ id, file, version, options }); return new Promise((fulfil) => { - callbacks.set(id, fulfil); + file_callbacks.set(id, fulfil); }); } diff --git a/packages/editor/src/lib/compile-worker/worker.ts b/packages/editor/src/lib/compile-worker/worker.ts index 5d91fc8000..57eb4fbad8 100644 --- a/packages/editor/src/lib/compile-worker/worker.ts +++ b/packages/editor/src/lib/compile-worker/worker.ts @@ -1,23 +1,71 @@ -import { compile, compileModule, migrate } from 'svelte/compiler'; import type { File } from '../Workspace.svelte'; -// TODO need to handle Svelte 3/4 for playground +// hack for magic-string and Svelte 4 compiler +// do not put this into a separate module and import it, would be treeshaken in prod +self.window = self; + +declare var self: Window & typeof globalThis & { svelte: typeof import('svelte/compiler') }; + +let inited = false; +let fulfil_ready: (arg?: never) => void; +const ready = new Promise((f) => { + fulfil_ready = f; +}); + +addEventListener('message', async (event) => { + if (!inited) { + inited = true; + const svelte_url = `https://unpkg.com/svelte@${event.data.version}`; + const { version } = await fetch(`${svelte_url}/package.json`).then((r) => r.json()); + + if (version.startsWith('4.')) { + // unpkg doesn't set the correct MIME type for .cjs files + // https://github.com/mjackson/unpkg/issues/355 + const compiler = await fetch(`${svelte_url}/compiler.cjs`).then((r) => r.text()); + (0, eval)(compiler + '\n//# sourceURL=compiler.cjs@' + version); + } else if (version.startsWith('3.')) { + const compiler = await fetch(`${svelte_url}/compiler.js`).then((r) => r.text()); + (0, eval)(compiler + '\n//# sourceURL=compiler.js@' + version); + } else { + const compiler = await fetch(`${svelte_url}/compiler/index.js`).then((r) => r.text()); + (0, eval)(compiler + '\n//# sourceURL=compiler/index.js@' + version); + } + + fulfil_ready(); + } + + await ready; -addEventListener('message', (event) => { const { id, file, options } = event.data as { id: number; file: File; options: { generate: 'client' | 'server'; dev: boolean }; }; - const fn = file.name.endsWith('.svelte') ? compile : compileModule; + const fn = file.name.endsWith('.svelte') ? self.svelte.compile : self.svelte.compileModule; + + if (!fn) { + // .svelte.js file compiled with Svelte 3/4 compiler + postMessage({ + id, + filename: file.name, + payload: { + error: null, + result: null, + migration: null + } + }); + return; + } let migration = null; - try { - migration = migrate(file.contents, { filename: file.name }); - } catch (e) { - // can this happen? + if (self.svelte.migrate) { + try { + migration = self.svelte.migrate(file.contents, { filename: file.name }); + } catch (e) { + // can this happen? + } } try { @@ -25,12 +73,19 @@ addEventListener('message', (event) => { postMessage({ id, + filename: file.name, payload: { error: null, result: { + // @ts-expect-error Svelte 3/4 doesn't contain this field + metadata: { runes: false }, ...result, - // @ts-expect-error https://github.com/sveltejs/svelte/issues/13628 - warnings: result.warnings.map((w) => ({ message: w.message, ...w })) + warnings: result.warnings.map((w) => { + // @ts-expect-error This exists on Svelte 3/4 and is required to be deleted, otherwise postMessage won't work + delete w.toString; + // @ts-expect-error https://github.com/sveltejs/svelte/issues/13628 (fixed in 5.0, but was like that for most of the preview phase) + return { message: w.message, ...w }; + }) }, migration } @@ -38,6 +93,7 @@ addEventListener('message', (event) => { } catch (e) { postMessage({ id, + filename: file.name, payload: { // @ts-expect-error error: { message: e.message, ...e }, diff --git a/packages/repl/src/lib/Output/Output.svelte b/packages/repl/src/lib/Output/Output.svelte index e49e78ff5a..ec5fc47913 100644 --- a/packages/repl/src/lib/Output/Output.svelte +++ b/packages/repl/src/lib/Output/Output.svelte @@ -65,14 +65,12 @@ // TODO this effect is a bit of a code smell $effect(() => { - if (current) { - if (current.error) { - js.contents = css.contents = `/* ${current.error.message} */`; - } else { - js.contents = current.result.js.code; - css.contents = - current.result.css?.code ?? `/* Add a tag to see the CSS output */`; - } + if (current?.error) { + js.contents = css.contents = `/* ${current.error.message} */`; + } else if (current?.result) { + js.contents = current.result.js.code; + css.contents = + current.result.css?.code ?? `/* Add a tag to see the CSS output */`; } else { js.contents = css.contents = `/* Select a component to see its compiled code */`; } diff --git a/packages/repl/src/lib/Repl.svelte b/packages/repl/src/lib/Repl.svelte index c8de6e9e62..ff5f9d651c 100644 --- a/packages/repl/src/lib/Repl.svelte +++ b/packages/repl/src/lib/Repl.svelte @@ -49,18 +49,17 @@ text: true }; - const workspace = $state( - new Workspace([dummy], { - initial: 'App.svelte', - onupdate() { - rebundle(); - onchange?.(); - }, - onreset() { - rebundle(); - } - }) - ); + const workspace = new Workspace([dummy], { + initial: 'App.svelte', + svelte_version: svelteUrl.split('@')[1], + onupdate() { + rebundle(); + onchange?.(); + }, + onreset() { + rebundle(); + } + }); // TODO get rid export function toJSON() { @@ -105,7 +104,7 @@ workspace.update_file({ ...workspace.current!, - contents: migration.code + contents: migration!.code }); rebundle(); diff --git a/packages/repl/src/lib/workers/bundler/index.ts b/packages/repl/src/lib/workers/bundler/index.ts index 54d244b9ab..4b2eb6cccc 100644 --- a/packages/repl/src/lib/workers/bundler/index.ts +++ b/packages/repl/src/lib/workers/bundler/index.ts @@ -17,6 +17,10 @@ import type { Warning } from '../../types'; import type { CompileError, CompileOptions, CompileResult } from 'svelte/compiler'; import type { File } from 'editor'; +// hack for magic-string and rollup inline sourcemaps +// do not put this into a separate module and import it, would be treeshaken in prod +self.window = self; + let packages_url: string; let svelte_url: string; let version: string; @@ -40,6 +44,9 @@ self.addEventListener('message', async (event: MessageEvent) // https://github.com/mjackson/unpkg/issues/355 const compiler = await fetch(`${svelte_url}/compiler.cjs`).then((r) => r.text()); (0, eval)(compiler + '\n//# sourceURL=compiler.cjs@' + version); + } else if (version.startsWith('3.')) { + const compiler = await fetch(`${svelte_url}/compiler.js`).then((r) => r.text()); + (0, eval)(compiler + '\n//# sourceURL=compiler.js@' + version); } else { const compiler = await fetch(`${svelte_url}/compiler/index.js`).then((r) => r.text()); (0, eval)(compiler + '\n//# sourceURL=compiler/index.js@' + version); @@ -395,7 +402,7 @@ async function get_bundle( `.replace(/\t/g, ''); } } else if (id.endsWith('.svelte.js')) { - result = svelte.compileModule(code, { + result = svelte.compileModule?.(code, { filename: name + '.js', generate: 'client', dev: true