Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(repl): esm.run as default CDN #551

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/repl/src/lib/Repl.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { set_repl_context } from './context.js';
import { get_full_filename } from './utils.js';

export let packagesUrl = 'https://unpkg.com';
export let packagesUrl = 'https://esm.run';
export let svelteUrl = `${packagesUrl}/svelte`;
export let embedded = false;
/** @type {'columns' | 'rows'} */
Expand Down
69 changes: 45 additions & 24 deletions packages/repl/src/lib/workers/bundler/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/// <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 { get_svelte_package_json, load_compiler } from '../worker-helpers.js';
import commonjs from './plugins/commonjs.js';
import glsl from './plugins/glsl.js';
import json from './plugins/json.js';
Expand All @@ -22,7 +23,7 @@ let svelte_url;
/** @type {number} */
let current_id;

/** @type {(...arg: never) => void} */
/** @type {(...arg: any) => void} */
let fulfil_ready;
const ready = new Promise((f) => {
fulfil_ready = f;
Expand All @@ -35,21 +36,10 @@ self.addEventListener(
case 'init': {
({ packages_url, svelte_url } = event.data);

const { version } = await fetch(`${svelte_url}/package.json`).then((r) => r.json());
const { version } = await get_svelte_package_json(svelte_url);
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 {
try {
importScripts(`${svelte_url}/compiler.js`);
} catch {
self.svelte = await import(/* @vite-ignore */ `${svelte_url}/compiler.mjs`);
}
}
await load_compiler(svelte_url, version);

fulfil_ready();
break;
Expand All @@ -63,7 +53,7 @@ self.addEventListener(

current_id = uid;

setTimeout(async () => {
Promise.resolve().then(async () => {
if (current_id !== uid) return;

const result = await bundle({ uid, files });
Expand Down Expand Up @@ -99,12 +89,13 @@ async function fetch_if_uncached(url, uid) {
}

// TODO: investigate whether this is necessary
await sleep(50);
// 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());
if (r.headers.get('content-type')?.includes('text/html')) throw new Error('HTML!');

return {
url: r.url,
Expand All @@ -125,7 +116,24 @@ async function fetch_if_uncached(url, uid) {
* @param {number} uid
*/
async function follow_redirects(url, uid) {
const res = await fetch_if_uncached(url, uid);
/** @type {{
* url: string;
* body: string;
* } | undefined} */
let res;
console.log(url);

const paths = ['', '.js', '/index.js', '.mjs', '/index.mjs', '.cjs', '/index.cjs'];

for (const path of paths) {
try {
res = await fetch_if_uncached(url.replace(/\/$/, '') + path, uid);
break;
} catch {
// maybe the next option will be successful
}
}

return res?.url;
}

Expand Down Expand Up @@ -314,7 +322,12 @@ async function get_bundle(uid, mode, cache, local_files_lookup) {

const fetch_package_info = async () => {
try {
const pkg_url = await follow_redirects(`${packages_url}/${pkg_name}/package.json`, uid);
const pkg_url = await follow_redirects(
`${
packages_url.includes('esm.run') ? 'https://cdn.jsdelivr.net/npm' : packages_url
}/${pkg_name}/package.json`,
uid
);

if (!pkg_url) throw new Error();

Expand All @@ -328,7 +341,7 @@ async function get_bundle(uid, mode, cache, local_files_lookup) {
pkg_url_base
};
} catch (_e) {
throw new Error(`Error fetching "${pkg_name}" from unpkg. Does the package exist?`);
throw new Error(`Error fetching "${pkg_name}". Does the package exist?`);
}
};

Expand Down Expand Up @@ -365,6 +378,7 @@ async function get_bundle(uid, mode, cache, local_files_lookup) {
const name = id.split('/').pop()?.split('.')[0];

const cached_id = cache.get(id);

const result =
cached_id && cached_id.code === code
? cached_id.result
Expand All @@ -377,17 +391,19 @@ async function get_bundle(uid, mode, cache, local_files_lookup) {
})
});

console.log(code);

new_cache.set(id, { code, result });

// @ts-expect-error
(result.warnings || result.stats.warnings)?.forEach((warning) => {
for (const warning of (result.warnings || result.stats.warnings) ?? []) {
// This is required, otherwise postMessage won't work
// @ts-ignore
delete warning.toString;
// TODO remove stats post-launch
// @ts-ignore
warnings.push(warning);
});
}

return result.js;
}
Expand All @@ -402,7 +418,10 @@ async function get_bundle(uid, mode, cache, local_files_lookup) {
json,
glsl,
replace({
'process.env.NODE_ENV': JSON.stringify('production')
'process.env.NODE_ENV': JSON.stringify('production'),
'import.meta.env.PROD': JSON.stringify(true),
'import.meta.env.DEV': JSON.stringify(false),
'import.meta.env.SSR': JSON.stringify(mode === 'ssr')
})
],
inlineDynamicImports: true,
Expand Down Expand Up @@ -464,6 +483,8 @@ async function bundle({ uid, files }) {
})
)?.output[0];

console.log(dom_result?.code);

const ssr = false // TODO how can we do SSR?
? await get_bundle(uid, 'ssr', cached.ssr, lookup)
: null;
Expand Down Expand Up @@ -495,7 +516,7 @@ async function bundle({ uid, files }) {
error: null
};
} catch (err) {
console.error(err);
console.trace(err);

/** @type {Error} */
// @ts-ignore
Expand Down
26 changes: 7 additions & 19 deletions packages/repl/src/lib/workers/compiler/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/// <reference lib="webworker" />
self.window = self; //TODO: still need?: egregious hack to get magic-string to work in a worker

// This is just for type-safety
/** @type {import('svelte/compiler')} */
var svelte;
import { get_svelte_package_json, load_compiler } from '../worker-helpers';

self.window = self; //TODO: still need?: egregious hack to get magic-string to work in a worker

/** @type {(...val: never) => void} */
/** @type {(...val: never[]) => void} */
let fulfil_ready;
const ready = new Promise((f) => {
fulfil_ready = f;
Expand All @@ -17,21 +16,10 @@ self.addEventListener(
async (event) => {
switch (event.data.type) {
case 'init':
const { svelte_url } = event.data;
const { version } = await fetch(`${svelte_url}/package.json`).then((r) => r.json());
const { svelte_url = '' } = event.data;
const { version } = await get_svelte_package_json(svelte_url);

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 {
try {
importScripts(`${svelte_url}/compiler.js`);
} catch {
self.svelte = await import(/* @vite-ignore */ `${svelte_url}/compiler.mjs`);
}
}
await load_compiler(svelte_url, version);

fulfil_ready();
break;
Expand Down
65 changes: 65 additions & 0 deletions packages/repl/src/lib/workers/worker-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Replaces export {} with svelte.EXPORT = EXPORT
* @param {string} input
*/
function remove_exports(input) {
const pattern = /export\{(.*?)\};/g;

return input.replace(
pattern,
/**
@param {string} _
@param {string} exports
*/ (_, exports) => {
return exports
.split(',')
.map((e) => {
const [original, alias] = e.split(' as ').map((s) => s.trim());
return `svelte.${alias} = ${original};`;
})
.join('');
}
);
}

/**
* @param {string} url
*/
export async function get_svelte_package_json(url) {
if (url.includes('https://esm.run')) {
// This will be an import, rather manually get the package.json
url = url.replace('https://esm.run', 'https://cdn.jsdelivr.net/npm');
}

return await fetch(`${url}/package.json`).then((r) => r.json());
}

/**
* Loads the compiler from the specified version
* @param {string} version
* @param {string} svelte_url
*/
export async function load_compiler(svelte_url, version) {
if (version.startsWith('4')) {
let compiler = await fetch(
`${
svelte_url.includes('esm.run')
? `https://cdn.jsdelivr.net/npm/svelte@${version}`
: svelte_url
}/compiler.cjs`
).then((r) => r.text());

if (svelte_url.includes('esm.run')) {
// Remove all the exports
compiler = remove_exports(compiler);
}

(0, eval)('var svelte = {};' + compiler + '\n//# sourceURL=compiler.js@' + version);
} else {
try {
importScripts(`${svelte_url}/compiler.js`);
} catch {
self.svelte = await import(/* @vite-ignore */ `${svelte_url}/compiler.mjs`);
}
}
}
64 changes: 47 additions & 17 deletions packages/repl/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,60 @@
name: 'App',
type: 'svelte',
source:
'<scri' +
"pt>\n\timport Timeline from './Timeline.svelte'\n\timport Sequence from './Sequence.svelte'\n\t\n\timport { tweened } from 'svelte/motion';\n</sc" +
'ript>\n\n<Timeline>\n\t<Sequence let:fps let:frame>\n\t\t<div style="height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">\n\t\t\t<h1 style="opacity: {Math.min(1, frame / fps)}; transform: translateY({Math.cos(frame/15) * 10}px);">\n\t\t\t\tHello Svelte\n\t\t\t</h1>\n\t\t\t<p style="opacity: {Math.min(1, frame / fps)}; transform: translateY({Math.cos((frame - 5)/15) * 10}px);">\n\t\t\t\tThis is a test.\n\t\t\t</p>\n\t\t</div>\n\t</Sequence>\n</Timeline>\n'
},
{
name: 'Sequence',
type: 'svelte',
source:
'<scri' +
"pt>\n\timport { onMount, onDestroy, getContext } from 'svelte';\n\t\n\tconst timeline = getContext('x:timeline');\n\t\n\t$: ({ width, height, fps } = $timeline);\n\t\n\texport let duration = fps * 10;\n\texport let start = 0;\n\texport let track = 1;\n\t\n\t$: frame = $timeline.frame - start;\n</sc" +
'ript>\n\n{#if timeline}\n\t<div class="sequence" style="width: {width}px; height: {height}px; border: 1px solid #ddd;">\n\t\t<slot {width} {height} {fps} {duration} {frame} />\n\t</div>\n{/if}\n'
},
{
name: 'Timeline',
type: 'svelte',
source: ''
`<scr` +
`ipt>
import { ConfettiExplosion } from 'svelte-confetti-explosion'
import { tick } from 'svelte'

let x, y;
let isVisible = false;

const handleClick = async e => {
x = e.clientX;
y = e.clientY;

isVisible = false;
await tick();
isVisible = true
}
</scr` +
`ipt>

<svelte:body on:click={handleClick} />

{#if isVisible}
<ConfettiExplosion --x="{x}px" --y="{y}px" />
{/if}

<div>
Click anywhere for confetti
</div>

<style>
div {
width: 100%;
height: 100vh;

display: grid;
place-items: center;

user-select: none;

color: grey;
}

:global(body) {
overflow: hidden;
}
</style>`
}
]
});
});
</script>

<main>
<Repl vim bind:this={repl} showAst autocomplete={true} previewTheme="dark" />
<Repl bind:this={repl} showAst autocomplete={true} previewTheme="dark" />
</main>

<style>
Expand Down