mirror of https://github.com/sveltejs/svelte
577 lines
13 KiB
577 lines
13 KiB
/// <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
|
|
})
|
|
};
|
|
}
|
|
}
|