diff --git a/apps/svelte.dev/src/config.js b/apps/svelte.dev/src/config.js index bdf5a5dec..86d210d0c 100644 --- a/apps/svelte.dev/src/config.js +++ b/apps/svelte.dev/src/config.js @@ -1,6 +1,5 @@ // REPL props -export const svelteUrl = `https://unpkg.com/svelte@4`; export const mapbox_setup = `window.MAPBOX_ACCESS_TOKEN = '${ import.meta.env.VITE_MAPBOX_ACCESS_TOKEN }';`; diff --git a/apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts b/apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts index f9b868b3f..2907bbe4f 100644 --- a/apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts +++ b/apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts @@ -24,8 +24,7 @@ export async function create(): Promise { bundler = new Bundler({ packages_url: 'https://unpkg.com', - svelte_url: `https://unpkg.com/svelte`, - // svelte_url: `${browser ? location.origin : ''}/svelte`, // TODO think about bringing back main-build for Playground? + svelte_version: 'latest', onstatus(val) { if (!done && val === null) { done = true; diff --git a/apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.server.ts b/apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.server.ts index 6bb2dfbdc..2abc3dbdb 100644 --- a/apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.server.ts +++ b/apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.server.ts @@ -1,7 +1,7 @@ import { error } from '@sveltejs/kit'; import type { Examples } from '../api/examples/all.json/+server.js'; -export async function load({ fetch, params, url }) { +export async function load({ fetch, params }) { const examples_res = fetch('/playground/api/examples/all.json').then((r) => r.json()); const res = await fetch(`/playground/api/${params.id}.json`); @@ -21,7 +21,6 @@ export async function load({ fetch, params, url }) { title: example.title, slug: example.slug })) - })), - version: url.searchParams.get('version') || 'latest' + })) }; } diff --git a/apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.svelte b/apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.svelte index a16ef352a..d11f2522b 100644 --- a/apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.svelte +++ b/apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.svelte @@ -4,7 +4,6 @@ import type { Gist } from '$lib/db/types'; import { Repl } from '@sveltejs/repl'; import { theme } from '@sveltejs/site-kit/stores'; - import { onMount } from 'svelte'; import { mapbox_setup } from '../../../../config.js'; import AppControls from './AppControls.svelte'; import { compress_and_encode_text, decode_and_decompress_text } from './gzip.js'; @@ -18,15 +17,18 @@ let repl = $state() as ReturnType; let name = $state(data.gist.name); let modified = $state(false); - let version = data.version; let setting_hash: any = null; + // svelte-ignore non_reactive_update + let version = $page.url.searchParams.get('version') || 'latest'; + let is_pr_or_commit_version = version.startsWith('pr-') || version.startsWith('commit-'); + // Hashed URLs are less safe (we can't delete malicious REPLs), therefore // don't allow links to escape the sandbox restrictions const can_escape = browser && !$page.url.hash; - onMount(() => { - if (version !== 'local') { + if (version !== 'local' && !is_pr_or_commit_version) { + $effect(() => { fetch(`https://unpkg.com/svelte@${version}/package.json`) .then((r) => r.json()) .then((pkg) => { @@ -40,8 +42,8 @@ replaceState(url, {}); } }); - } - }); + }); + } afterNavigate(() => { name = data.gist.name; @@ -148,11 +150,6 @@ } } - const svelteUrl = - browser && version === 'local' - ? `${location.origin}/playground/local` - : `https://unpkg.com/svelte@${version}`; - const relaxed = $derived(data.gist.relaxed || (data.user && data.user.id === data.gist.owner)); @@ -198,7 +195,7 @@
; - onMount(() => { - if (data.version !== 'local') { - fetch(`https://unpkg.com/svelte@${data.version}/package.json`) + // svelte-ignore non_reactive_update + let version = $page.url.searchParams.get('version') || 'latest'; + let is_pr_or_commit_version = version.startsWith('pr-') || version.startsWith('commit-'); + + if (version !== 'local' && !is_pr_or_commit_version) { + $effect(() => { + fetch(`https://unpkg.com/svelte@${version}/package.json`) .then((r) => r.json()) .then((pkg) => { if (pkg.version !== data.version) { replaceState(`/playground/${data.gist.id}/embed?version=${pkg.version}`, {}); } }); - } - }); + }); + } afterNavigate(() => { repl?.set({ @@ -28,11 +32,6 @@ }); }); - const svelteUrl = - browser && data.version === 'local' - ? `${location.origin}/playground/local` - : `https://unpkg.com/svelte@${data.version}`; - const relaxed = $derived(data.gist.relaxed || (data.user && data.user.id === data.gist.owner)); @@ -48,7 +47,7 @@ {#if browser} void; -const ready = new Promise((f) => { - fulfil_ready = f; -}); +let inited: PromiseWithResolvers; -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); +async function init(v: string) { + const svelte_url = `https://unpkg.com/svelte@${v}`; + const match = /^(?:pr|commit)-(.+)/.exec(v); + + let tarball: FileDescription[] | undefined; + let version: string; + + if (match) { + const response = await fetch(`https://pkg.pr.new/svelte@${match[1]}`); + + if (!response.ok) { + throw new Error('Could not fetch tarball'); } - fulfil_ready(); + tarball = await parseTar(await response.arrayBuffer()); + + const json = tarball.find((file) => file.name === 'package/package.json')!.text; + version = JSON.parse(json).version; + } else { + version = (await fetch(`${svelte_url}/package.json`).then((r) => r.json())).version; + } + + const entry = version.startsWith('3.') + ? 'compiler.js' + : version.startsWith('4.') + ? 'compiler.cjs' + : 'compiler/index.js'; + + const compiler = tarball + ? tarball.find((file) => file.name === `package/${entry}`)!.text + : await fetch(`${svelte_url}/${entry}`).then((r) => r.text()); + + (0, eval)(compiler + `\n//# sourceURL=${entry}@` + version); + + return self.svelte; +} + +addEventListener('message', async (event) => { + if (!inited) { + inited = Promise.withResolvers(); + init(event.data.version).then(inited.resolve, inited.reject); } - await ready; + const svelte = await inited.promise; const { id, file, options } = event.data as { id: number; @@ -43,7 +62,7 @@ addEventListener('message', async (event) => { options: ExposedCompilerOptions; }; - if (!file.name.endsWith('.svelte') && !self.svelte.compileModule) { + if (!file.name.endsWith('.svelte') && !svelte.compileModule) { // .svelte.js file compiled with Svelte 3/4 compiler postMessage({ id, @@ -59,9 +78,9 @@ addEventListener('message', async (event) => { let migration = null; - if (self.svelte.migrate) { + if (svelte.migrate) { try { - migration = self.svelte.migrate(file.contents, { filename: file.name }); + migration = svelte.migrate(file.contents, { filename: file.name }); } catch (e) { // can this happen? } @@ -71,7 +90,7 @@ addEventListener('message', async (event) => { let result: CompileResult; if (file.name.endsWith('.svelte')) { - const is_svelte_3_or_4 = !self.svelte.compileModule; + const is_svelte_3_or_4 = !svelte.compileModule; const compilerOptions: any = { generate: is_svelte_3_or_4 ? options.generate === 'client' @@ -84,9 +103,9 @@ addEventListener('message', async (event) => { if (!is_svelte_3_or_4) { compilerOptions.modernAst = options.modernAst; // else Svelte 3/4 will throw an "unknown option" error } - result = self.svelte.compile(file.contents, compilerOptions); + result = svelte.compile(file.contents, compilerOptions); } else { - result = self.svelte.compileModule(file.contents, { + result = svelte.compileModule(file.contents, { generate: options.generate, dev: options.dev, filename: file.name diff --git a/packages/repl/package.json b/packages/repl/package.json index 6d5683f2d..4e3598d59 100644 --- a/packages/repl/package.json +++ b/packages/repl/package.json @@ -86,6 +86,7 @@ "marked": "^14.1.2", "resolve.exports": "^2.0.2", "svelte": "5.0.1", + "tarparser": "^0.0.4", "zimmerframe": "^1.1.2" } } diff --git a/packages/repl/src/lib/Bundler.ts b/packages/repl/src/lib/Bundler.ts index 6fbef7eff..49caae67c 100644 --- a/packages/repl/src/lib/Bundler.ts +++ b/packages/repl/src/lib/Bundler.ts @@ -15,18 +15,20 @@ export default class Bundler { constructor({ packages_url, - svelte_url, - onstatus + svelte_version, + onstatus, + onerror }: { packages_url: string; - svelte_url: string; + svelte_version: string; onstatus: (val: string | null) => void; + onerror?: (message: string) => void; }) { - this.hash = `${packages_url}:${svelte_url}`; + this.hash = `${packages_url}:${svelte_version}`; if (!workers.has(this.hash)) { const worker = new Worker(); - worker.postMessage({ type: 'init', packages_url, svelte_url }); + worker.postMessage({ type: 'init', packages_url, svelte_version }); workers.set(this.hash, worker); } @@ -44,6 +46,11 @@ export default class Bundler { return; } + if (event.data.type === 'error') { + onerror?.(event.data.message); + return; + } + onstatus(null); handler(event.data); this.handlers.delete(event.data.uid); @@ -54,7 +61,6 @@ export default class Bundler { bundle(files: File[], options: CompileOptions = {}): Promise { return new Promise((fulfil) => { this.handlers.set(uid, fulfil); - this.worker.postMessage({ uid, type: 'bundle', diff --git a/packages/repl/src/lib/Repl.svelte b/packages/repl/src/lib/Repl.svelte index e7e776214..65619ad31 100644 --- a/packages/repl/src/lib/Repl.svelte +++ b/packages/repl/src/lib/Repl.svelte @@ -12,7 +12,7 @@ interface Props { packagesUrl?: string; - svelteUrl?: any; + svelteVersion?: string; embedded?: boolean; orientation?: 'columns' | 'rows'; relaxed?: boolean; @@ -27,7 +27,7 @@ let { packagesUrl = 'https://unpkg.com', - svelteUrl = `${BROWSER ? location.origin : ''}/svelte`, + svelteVersion = 'latest', embedded = false, orientation = 'columns', relaxed = false, @@ -51,7 +51,7 @@ const workspace = new Workspace([dummy], { initial: 'App.svelte', - svelte_version: svelteUrl.split('@')[1], + svelte_version: svelteVersion, onupdate() { rebundle(); onchange?.(); @@ -113,13 +113,14 @@ let width = $state(0); let show_output = $state(false); let status: string | null = $state(null); + let runtime_error: Error | null = $state(null); let status_visible = $state(false); let status_timeout: NodeJS.Timeout | undefined = undefined; const bundler = BROWSER ? new Bundler({ packages_url: packagesUrl, - svelte_url: svelteUrl, + svelte_version: svelteVersion, onstatus: (message) => { if (message) { // show bundler status, but only after time has elapsed, to @@ -134,8 +135,10 @@ status_visible = false; status_timeout = undefined; } - status = message; + }, + onerror: (message) => { + runtime_error = new Error(message); } }) : null; @@ -188,6 +191,7 @@ {injectedCSS} {previewTheme} {workspace} + runtimeError={status_visible ? runtime_error : null} /> diff --git a/packages/repl/src/lib/workers/bundler/index.ts b/packages/repl/src/lib/workers/bundler/index.ts index e510349fc..c241fa922 100644 --- a/packages/repl/src/lib/workers/bundler/index.ts +++ b/packages/repl/src/lib/workers/bundler/index.ts @@ -16,6 +16,7 @@ import type { BundleMessageData } from '../workers'; import type { Warning } from '../../types'; import type { CompileError, CompileOptions, CompileResult } from 'svelte/compiler'; import type { File } from 'editor'; +import { parseTar, type FileDescription } from 'tarparser'; // hack for magic-string and rollup inline sourcemaps // do not put this into a separate module and import it, would be treeshaken in prod @@ -26,38 +27,75 @@ let svelte_url: string; let version: string; let current_id: number; -let fulfil_ready: (arg?: never) => void; -const ready = new Promise((f) => { - fulfil_ready = f; -}); +let inited = Promise.withResolvers(); + +async function init(v: string, packages_url: string) { + const match = /^(pr|commit)-(.+)/.exec(v); + + let tarball: FileDescription[] | undefined; + + if (match) { + const response = await fetch(`https://pkg.pr.new/svelte@${match[2]}`); + + if (!response.ok) { + throw new Error( + `impossible to fetch the compiler from this ${match[1] === 'pr' ? 'PR' : 'commit'}` + ); + } + + tarball = await parseTar(await response.arrayBuffer()); + + const json = tarball.find((file) => file.name === 'package/package.json')!.text; + version = JSON.parse(json).version; + + svelte_url = `svelte://svelte@${version}`; + + for (const file of tarball) { + const url = `${svelte_url}/${file.name.slice('package/'.length)}`; + FETCH_CACHE.set(url, Promise.resolve({ url, body: file.text })); + } + } else { + const response = await fetch(`${packages_url}/svelte@${v}/package.json`); + const pkg = await response.json(); + version = pkg.version; + svelte_url = `${packages_url}/svelte@${version}`; + } + + console.log(`Using Svelte compiler version ${version}`); + + const entry = version.startsWith('3.') + ? 'compiler.js' + : version.startsWith('4.') + ? 'compiler.cjs' + : 'compiler/index.js'; + + const compiler = tarball + ? tarball.find((file) => file.name === `package/${entry}`)!.text + : await fetch(`${svelte_url}/${entry}`).then((r) => r.text()); + + (0, eval)(compiler + `\n//# sourceURL=${entry}@` + version); + + return svelte; +} self.addEventListener('message', async (event: MessageEvent) => { switch (event.data.type) { case 'init': { - ({ packages_url, svelte_url } = event.data); - - ({ version } = await fetch(`${svelte_url}/package.json`).then((r) => r.json())); - console.log(`Using Svelte compiler version ${version}`); - - 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(); + packages_url = event.data.packages_url; + init(event.data.svelte_version, packages_url).then(inited.resolve, inited.reject); break; } case 'bundle': { - await ready; + try { + await inited.promise; + } catch (e) { + self.postMessage({ + type: 'error', + uid: event.data.uid, + message: `Error loading the compiler: ${(e as Error).message}` + }); + } const { uid, files, options } = event.data; if (files.length === 0) return; @@ -122,29 +160,6 @@ async function follow_redirects(url: string, uid: number) { return res?.url; } -function compare_to_version(major: number, minor: number, patch: number): number { - const v = svelte.VERSION.match(/^(\d+)\.(\d+)\.(\d+)/); - - // @ts-ignore - return +v[1] - major || +v[2] - minor || +v[3] - patch; -} - -function is_v4() { - return compare_to_version(4, 0, 0) >= 0; -} - -function is_v5() { - return compare_to_version(5, 0, 0) >= 0; -} - -function is_legacy_package_structure() { - return compare_to_version(3, 4, 4) <= 0; -} - -function has_loopGuardTimeout_feature() { - return compare_to_version(3, 14, 0) >= 0; -} - async function resolve_from_pkg( pkg: Record, subpath: string, @@ -236,26 +251,6 @@ async function get_bundle( if (importee === 'esm-env') return importee; - const v5 = is_v5(); - const v4 = !v5 && is_v4(); - - if (!v5) { - // importing from Svelte - if (importee === `svelte`) - return v4 ? `${svelte_url}/src/runtime/index.js` : `${svelte_url}/index.mjs`; - - if (importee.startsWith(`svelte/`)) { - const sub_path = importee.slice(7); - if (v4) { - return `${svelte_url}/src/runtime/${sub_path}/index.js`; - } - - return is_legacy_package_structure() - ? `${svelte_url}/${sub_path}.mjs` - : `${svelte_url}/${sub_path}/index.mjs`; - } - } - if (importee === shared_file) return importee; // importing from another file in REPL @@ -280,7 +275,6 @@ async function get_bundle( // relative import in an external file const url = new URL(importee, importer).href; self.postMessage({ type: 'status', uid, message: `resolving ${url}` }); - return await follow_redirects(url, uid); } } else { @@ -293,7 +287,10 @@ async function get_bundle( } const pkg_name = match[1]; - const version = pkg_name === 'svelte' ? svelte.VERSION : match[2] ?? 'latest'; + const pkg_url = + pkg_name === 'svelte' + ? `${svelte_url}/package.json` + : `${packages_url}/${pkg_name}/package.json`; const subpath = `.${match[3] ?? ''}`; // if this was imported by one of our files, add it to the `imports` set @@ -301,19 +298,16 @@ async function get_bundle( imports.add(pkg_name); } - const fetch_package_info = async () => { + const fetch_package_info = async (pkg_url: string) => { try { - const pkg_url = await follow_redirects( - `${packages_url}/${pkg_name}@${version}/package.json`, - uid - ); + const redirected = await follow_redirects(pkg_url, uid); - if (!pkg_url) throw new Error(); + if (!redirected) throw new Error(); - const pkg_json = (await fetch_if_uncached(pkg_url, uid))?.body; + const pkg_json = (await fetch_if_uncached(redirected, uid))?.body; const pkg = JSON.parse(pkg_json ?? '""'); - const pkg_url_base = pkg_url.replace(/\/package\.json$/, ''); + const pkg_url_base = redirected.replace(/\/package\.json$/, ''); return { pkg, @@ -324,7 +318,7 @@ async function get_bundle( } }; - const { pkg, pkg_url_base } = await fetch_package_info(); + const { pkg, pkg_url_base } = await fetch_package_info(pkg_url); try { const resolved_id = await resolve_from_pkg(pkg, subpath, uid, pkg_url_base); diff --git a/packages/repl/src/lib/workers/workers.d.ts b/packages/repl/src/lib/workers/workers.d.ts index ab0b5d838..1515b1097 100644 --- a/packages/repl/src/lib/workers/workers.d.ts +++ b/packages/repl/src/lib/workers/workers.d.ts @@ -50,10 +50,10 @@ export interface MigrateOutput { export type BundleMessageData = { uid: number; - type: 'init' | 'bundle' | 'status'; + type: 'init' | 'bundle' | 'status' | 'error'; message: string; packages_url: string; - svelte_url: string; + svelte_version: string; files: File[]; options: CompileOptions; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c128a252..a0a8f1af5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,6 +204,9 @@ importers: esm-env: specifier: ^1.0.0 version: 1.0.0 + tarparser: + specifier: ^0.0.4 + version: 0.0.4 devDependencies: '@codemirror/autocomplete': specifier: ^6.9.0 @@ -358,6 +361,9 @@ importers: svelte: specifier: 5.0.1 version: 5.0.1 + tarparser: + specifier: ^0.0.4 + version: 0.0.4 zimmerframe: specifier: ^1.1.2 version: 1.1.2 @@ -2966,6 +2972,9 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + tarparser@0.0.4: + resolution: {integrity: sha512-H3+VR6ys/N4eRBYbcyKuy4i0lKAxW/tx+wZANTFPfXjHLkz7UDs7V9g0rdGJDsVWYyqnmvmLM7YqWZ7yrtoReA==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -5941,6 +5950,8 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + tarparser@0.0.4: {} + term-size@2.2.1: {} tiny-glob@0.2.9: