/// <reference lib="webworker" /> import '../patch_window.js'; import { sleep } from '$lib/utils.js'; import { rollup } from '@rollup/browser'; import { DEV } from 'esm-env'; import * as resolve from 'resolve.exports'; import commonjs from './plugins/commonjs.js'; import glsl from './plugins/glsl.js'; import json from './plugins/json.js'; import replace from './plugins/replace.js'; import loop_protect from './plugins/loop-protect.js'; /** @type {string} */ var pkg_name; /** @type {string} */ let packages_url; /** @type {string} */ let svelte_url; /** @type {number} */ let current_id; /** @type {(arg?: never) => void} */ let fulfil_ready; const ready = new Promise((f) => { fulfil_ready = f; }); /** * @type {{ * compile: typeof import('svelte/compiler').compile; * compileModule: typeof import('svelte/compiler').compileModule; * VERSION: string; * }} */ let svelte; self.addEventListener( 'message', /** @param {MessageEvent<import('../workers.js').BundleMessageData>} event */ async (event) => { switch (event.data.type) { case 'init': { ({ packages_url, svelte_url } = event.data); const { version } = await fetch(`${svelte_url}/package.json`).then((r) => r.json()); console.log(`Using Svelte compiler version ${version}`); const compiler = await fetch(`${svelte_url}/compiler/index.js`).then((r) => r.text()); (0, eval)(compiler + '\n//# sourceURL=compiler/index.js@' + version); svelte = globalThis.svelte; fulfil_ready(); break; } case 'bundle': { await ready; const { uid, files } = event.data; if (files.length === 0) return; current_id = uid; setTimeout(async () => { if (current_id !== uid) return; const result = await bundle({ uid, files }); if (JSON.stringify(result.error) === JSON.stringify(ABORT)) return; if (result && uid === current_id) postMessage(result); }); break; } } } ); /** @type {Record<'client' | 'server', Map<string, { code: string, result: ReturnType<typeof svelte.compile> }>>} */ let cached = { client: new Map(), server: new Map() }; const ABORT = { aborted: true }; /** @type {Map<string, Promise<{ url: string; body: string; }>>} */ const FETCH_CACHE = new Map(); /** * @param {string} url * @param {number} uid */ async function fetch_if_uncached(url, uid) { if (FETCH_CACHE.has(url)) { return FETCH_CACHE.get(url); } // TODO: investigate whether this is necessary await sleep(50); if (uid !== current_id) throw ABORT; const promise = fetch(url) .then(async (r) => { if (!r.ok) throw new Error(await r.text()); return { url: r.url, body: await r.text() }; }) .catch((err) => { FETCH_CACHE.delete(url); throw err; }); FETCH_CACHE.set(url, promise); return promise; } /** * @param {string} url * @param {number} uid */ async function follow_redirects(url, uid) { const res = await fetch_if_uncached(url, uid); return res?.url; } /** * * @param {number} major * @param {number} minor * @param {number} patch * @returns {number} */ function compare_to_version(major, minor, patch) { 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; } /** * * @param {Record<string, unknown>} pkg * @param {string} subpath * @param {number} uid * @param {string} pkg_url_base */ async function resolve_from_pkg(pkg, subpath, uid, pkg_url_base) { // match legacy Rollup logic — pkg.svelte takes priority over pkg.exports if (typeof pkg.svelte === 'string' && subpath === '.') { return pkg.svelte; } // modern if (pkg.exports) { try { const [resolved] = resolve.exports(pkg, subpath, { browser: true, conditions: ['svelte', 'development'] }) ?? []; return resolved; } catch { throw `no matched export path was found in "${pkg_name}/package.json"`; } } // legacy if (subpath === '.') { let resolved_id = resolve.legacy(pkg, { fields: ['browser', 'module', 'main'] }); if (typeof resolved_id === 'object' && !Array.isArray(resolved_id)) { const subpath = resolved_id['.']; if (subpath === false) return 'data:text/javascript,export {}'; resolved_id = subpath ?? resolve.legacy(pkg, { fields: ['module', 'main'] }); } if (!resolved_id) { // last ditch — try to match index.js/index.mjs for (const index_file of ['index.mjs', 'index.js']) { try { const indexUrl = new URL(index_file, `${pkg_url_base}/`).href; return (await follow_redirects(indexUrl, uid)) ?? ''; } catch { // maybe the next option will be successful } } throw `could not find entry point in "${pkg_name}/package.json"`; } return resolved_id; } if (typeof pkg.browser === 'object') { // this will either return `pkg.browser[subpath]` or `subpath` return resolve.legacy(pkg, { browser: subpath }); } return subpath; } /** * @param {number} uid * @param {'client' | 'server'} mode * @param {typeof cached['client']} cache * @param {Map<string, import('$lib/types.js').File>} local_files_lookup */ async function get_bundle(uid, mode, cache, local_files_lookup) { let bundle; /** A set of package names (without subpaths) to include in pkg.devDependencies when downloading an app */ /** @type {Set<string>} */ const imports = new Set(); /** @type {import('$lib/types.js').Warning[]} */ const warnings = []; /** @type {{ message: string }[]} */ const all_warnings = []; /** @type {typeof cache} */ const new_cache = new Map(); /** @type {import('@rollup/browser').Plugin} */ const repl_plugin = { name: 'svelte-repl', async resolveId(importee, importer) { if (uid !== current_id) throw ABORT; 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`; } } // importing from another file in REPL if (local_files_lookup.has(importee) && (!importer || local_files_lookup.has(importer))) return importee; if (local_files_lookup.has(importee + '.js')) return importee + '.js'; if (local_files_lookup.has(importee + '.json')) return importee + '.json'; // remove trailing slash if (importee.endsWith('/')) importee = importee.slice(0, -1); // importing from a URL if (/^https?:/.test(importee)) return importee; if (importee.startsWith('.')) { if (importer && local_files_lookup.has(importer)) { // relative import in a REPL file // should've matched above otherwise importee doesn't exist console.error(`Cannot find file "${importee}" imported by "${importer}" in the REPL`); return; } else { // 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 { // fetch from unpkg self.postMessage({ type: 'status', uid, message: `resolving ${importee}` }); const match = /^((?:@[^/]+\/)?[^/]+)(\/.+)?$/.exec(importee); if (!match) { return console.error(`Invalid import "${importee}"`); } const pkg_name = match[1]; const subpath = `.${match[2] ?? ''}`; // if this was imported by one of our files, add it to the `imports` set if (importer && local_files_lookup.has(importer)) { imports.add(pkg_name); } const fetch_package_info = async () => { try { const pkg_url = await follow_redirects( `${pkg_name === 'svelte' ? '' : packages_url}/${pkg_name}/package.json`, uid ); if (!pkg_url) throw new Error(); const pkg_json = (await fetch_if_uncached(pkg_url, uid))?.body; const pkg = JSON.parse(pkg_json ?? '""'); const pkg_url_base = pkg_url.replace(/\/package\.json$/, ''); return { pkg, pkg_url_base }; } catch (_e) { throw new Error(`Error fetching "${pkg_name}" from unpkg. Does the package exist?`); } }; const { pkg, pkg_url_base } = await fetch_package_info(); try { const resolved_id = await resolve_from_pkg(pkg, subpath, uid, pkg_url_base); return new URL(resolved_id + '', `${pkg_url_base}/`).href; } catch (reason) { throw new Error(`Cannot import "${importee}": ${reason}.`); } } }, async load(resolved) { if (uid !== current_id) throw ABORT; if (resolved === 'esm-env') { return `export const BROWSER = true; export const DEV = true`; } const cached_file = local_files_lookup.get(resolved); if (cached_file) return cached_file.source; if (!FETCH_CACHE.has(resolved)) { self.postMessage({ type: 'status', uid, message: `fetching ${resolved}` }); } const res = await fetch_if_uncached(resolved, uid); return res?.body; }, transform(code, id) { if (uid !== current_id) throw ABORT; self.postMessage({ type: 'status', uid, message: `bundling ${id}` }); if (!/\.(svelte|js)$/.test(id)) return null; const name = id.split('/').pop()?.split('.')[0]; const cached_id = cache.get(id); let result; if (cached_id && cached_id.code === code) { result = cached_id.result; } else if (id.endsWith('.svelte')) { result = svelte.compile(code, { filename: name + '.svelte', generate: 'client', dev: true }); if (result.css) { result.js.code += '\n\n' + ` const $$__style = document.createElement('style'); $$__style.textContent = ${JSON.stringify(result.css.code)}; document.head.append($$__style); `.replace(/\t/g, ''); } } else if (id.endsWith('.svelte.js')) { result = svelte.compileModule(code, { filename: name + '.js', generate: 'client', dev: true }); if (!result) { return null; } } else { return null; } new_cache.set(id, { code, result }); // @ts-expect-error (result.warnings || result.stats?.warnings)?.forEach((warning) => { // This is required, otherwise postMessage won't work // @ts-ignore delete warning.toString; // TODO remove stats post-launch // @ts-ignore warnings.push(warning); }); /** @type {import('@rollup/browser').TransformResult} */ const transform_result = { code: result.js.code, map: result.js.map }; return transform_result; } }; try { bundle = await rollup({ input: './__entry.js', plugins: [ repl_plugin, commonjs, json, glsl, loop_protect, replace({ 'process.env.NODE_ENV': JSON.stringify('production') }) ], inlineDynamicImports: true, onwarn(warning) { all_warnings.push({ message: warning.message }); } }); return { bundle, imports: Array.from(imports), cache: new_cache, error: null, warnings, all_warnings }; } catch (error) { return { error, imports: null, bundle: null, cache: new_cache, warnings, all_warnings }; } } /** * @param {{ uid: number; files: import('$lib/types.js').File[] }} param0 * @returns */ async function bundle({ uid, files }) { if (!DEV) { console.clear(); console.log(`running Svelte compiler version %c${svelte.VERSION}`, 'font-weight: bold'); } /** @type {Map<string, import('$lib/types').File>} */ const lookup = new Map(); lookup.set('./__entry.js', { name: '__entry', source: ` export { mount, unmount, untrack } from 'svelte'; export {default as App} from './App.svelte'; `, type: 'js', modified: false }); files.forEach((file) => { const path = `./${file.name}.${file.type}`; lookup.set(path, file); }); /** @type {Awaited<ReturnType<typeof get_bundle>>} */ let client = await get_bundle(uid, 'client', cached.client, lookup); let error; try { if (client.error) { throw client.error; } cached.client = client.cache; const client_result = ( await client.bundle?.generate({ format: 'iife', exports: 'named' // sourcemap: 'inline' }) )?.output[0]; const server = false // TODO how can we do SSR? ? await get_bundle(uid, 'server', cached.server, lookup) : null; if (server) { cached.server = server.cache; if (server.error) { throw server.error; } } const server_result = server ? ( await server.bundle?.generate({ format: 'iife', name: 'SvelteComponent', exports: 'named' // sourcemap: 'inline' }) )?.output?.[0] : null; return { uid, client: client_result, server: server_result, imports: client.imports, warnings: client.warnings, error: null }; } catch (err) { console.error(err); /** @type {Error} */ // @ts-ignore const e = error || err; // @ts-ignore delete e.toString; return { uid, client: null, server: null, imports: null, warnings: client.warnings, error: Object.assign({}, e, { message: e.message, stack: e.stack }) }; } }