mirror of https://github.com/sveltejs/svelte
chore: remove preview site (#14428)
parent
0ec251f09d
commit
1d7f0fe313
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/dist
|
||||
/src/routes/status/results.json
|
||||
.env
|
||||
.env.*
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
{
|
||||
"name": "svelte-5-preview",
|
||||
"private": true,
|
||||
"version": "0.5.0",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check-disable-tmp": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/fira-mono": "^5.0.8",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@sveltejs/adapter-vercel": "^5.4.3",
|
||||
"@sveltejs/kit": "^2.8.3",
|
||||
"@sveltejs/site-kit": "6.0.0-next.64",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
|
||||
"@vercel/speed-insights": "^1.0.0",
|
||||
"esrap": "^1.2.3",
|
||||
"marked": "^9.0.0",
|
||||
"publint": "^0.2.7",
|
||||
"shiki": "^0.14.7",
|
||||
"shiki-twoslash": "^3.1.2",
|
||||
"svelte": "workspace:^",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.12.0",
|
||||
"@codemirror/commands": "^6.3.3",
|
||||
"@codemirror/lang-css": "^6.2.1",
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.2.4",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/lint": "^6.5.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.24.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@neocodemirror/svelte": "0.0.15",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
"@rich_harris/svelte-split-pane": "^1.1.3",
|
||||
"@rollup/browser": "^3.28.0",
|
||||
"acorn": "^8.12.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"esm-env": "^1.0.0",
|
||||
"marked": "^7.0.2",
|
||||
"resolve.exports": "^2.0.2",
|
||||
"svelte-json-tree": "^2.2.0",
|
||||
"zimmerframe": "^1.1.2"
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="theme-default typo-default">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
@ -1 +0,0 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
@ -1,72 +0,0 @@
|
||||
import Worker from './workers/bundler/index.js?worker';
|
||||
|
||||
const workers = new Map();
|
||||
|
||||
let uid = 1;
|
||||
|
||||
export default class Bundler {
|
||||
/** @type {Worker} */
|
||||
worker;
|
||||
|
||||
/** @param {{ packages_url: string; svelte_url: string; onstatus: (val: string | null) => void}} param0 */
|
||||
constructor({ packages_url, svelte_url, onstatus }) {
|
||||
const hash = `${packages_url}:${svelte_url}`;
|
||||
|
||||
if (!workers.has(hash)) {
|
||||
const worker = new Worker();
|
||||
worker.postMessage({ type: 'init', packages_url, svelte_url });
|
||||
workers.set(hash, worker);
|
||||
}
|
||||
|
||||
this.worker = workers.get(hash);
|
||||
|
||||
this.handlers = new Map();
|
||||
|
||||
this.worker.addEventListener(
|
||||
'message',
|
||||
/**
|
||||
*
|
||||
* @param {MessageEvent<import('./workers/workers').BundleMessageData>} event
|
||||
* @returns
|
||||
*/
|
||||
(event) => {
|
||||
const handler = this.handlers.get(event.data.uid);
|
||||
|
||||
if (handler) {
|
||||
// if no handler, was meant for a different REPL
|
||||
if (event.data.type === 'status') {
|
||||
onstatus(event.data.message);
|
||||
return;
|
||||
}
|
||||
|
||||
onstatus(null);
|
||||
handler(event.data);
|
||||
this.handlers.delete(event.data.uid);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('./types').File[]} files
|
||||
* @returns
|
||||
*/
|
||||
bundle(files) {
|
||||
return new Promise((fulfil) => {
|
||||
this.handlers.set(uid, fulfil);
|
||||
|
||||
this.worker.postMessage({
|
||||
uid,
|
||||
type: 'bundle',
|
||||
files
|
||||
});
|
||||
|
||||
uid += 1;
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.worker.terminate();
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
<script>
|
||||
export let checked = false;
|
||||
</script>
|
||||
|
||||
<input type="checkbox" bind:checked />
|
||||
|
||||
<style>
|
||||
input[type='checkbox'] {
|
||||
/* display: block; */
|
||||
position: relative;
|
||||
height: 1em;
|
||||
width: calc(100% - 0.6em);
|
||||
max-width: 2em;
|
||||
top: -2px;
|
||||
border-radius: 0.5em;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
border: transparent;
|
||||
margin: 0 0.6em 0 0;
|
||||
}
|
||||
|
||||
input[type='checkbox']::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
border-radius: 1em;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: var(--sk-theme-2);
|
||||
/* box-sizing: border-box; */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
input[type='checkbox']:checked::before {
|
||||
background: var(--sk-theme-1);
|
||||
}
|
||||
|
||||
input[type='checkbox']::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
border-radius: 1em;
|
||||
background: white;
|
||||
box-shadow:
|
||||
0 0px 1px rgba(0, 0, 0, 0.4),
|
||||
0 4px 2px rgba(0, 0, 0, 0.1);
|
||||
transition:
|
||||
background 0.2s ease-out,
|
||||
left 0.2s ease-out;
|
||||
}
|
||||
|
||||
input[type='checkbox']:checked::after {
|
||||
left: calc(100% - 1em + 2px);
|
||||
}
|
||||
</style>
|
@ -1,407 +0,0 @@
|
||||
<script module>
|
||||
export const cursorIndex = writable(0);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { historyField } from '@codemirror/commands';
|
||||
import { EditorState, Range, StateEffect, StateEffectType, StateField } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
import { codemirror, withCodemirrorInstance } from '@neocodemirror/svelte';
|
||||
import { svelteLanguage } from '@replit/codemirror-lang-svelte';
|
||||
import { javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { createEventDispatcher, tick } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { get_repl_context } from '$lib/context.js';
|
||||
import Message from './Message.svelte';
|
||||
import { svelteTheme } from './theme.js';
|
||||
import { autocomplete } from './autocomplete.js';
|
||||
|
||||
/** @type {import('@codemirror/lint').LintSource | undefined} */
|
||||
export let diagnostics = undefined;
|
||||
|
||||
export let readonly = false;
|
||||
export let tab = true;
|
||||
|
||||
/** @type {ReturnType<typeof createEventDispatcher<{ change: { value: string } }>>} */
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let code = '';
|
||||
|
||||
/** @type {import('./types').Lang} */
|
||||
let lang = 'svelte';
|
||||
|
||||
/**
|
||||
* @param {{ code: string; lang: import('./types').Lang }} options
|
||||
*/
|
||||
export async function set(options) {
|
||||
update(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ code?: string; lang?: import('./types').Lang }} options
|
||||
*/
|
||||
export async function update(options) {
|
||||
await isReady;
|
||||
|
||||
if (!$cmInstance.view) return;
|
||||
|
||||
if (options.lang && options.lang !== lang) {
|
||||
// This will trigger change_mode
|
||||
lang = options.lang;
|
||||
}
|
||||
|
||||
if (options.code !== undefined) {
|
||||
updating_externally = true;
|
||||
|
||||
const { scrollLeft: left, scrollTop: top } = $cmInstance.view.scrollDOM;
|
||||
|
||||
code = options.code;
|
||||
|
||||
updating_externally = false;
|
||||
|
||||
$cmInstance.view.scrollDOM.scrollTop = top;
|
||||
$cmInstance.view.scrollDOM.scrollLeft = left;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} pos
|
||||
*/
|
||||
export function setCursor(pos) {
|
||||
cursor_pos = pos;
|
||||
}
|
||||
|
||||
/** @type {(...val: any) => void} */
|
||||
let fulfil_module_editor_ready;
|
||||
export const isReady = new Promise((f) => (fulfil_module_editor_ready = f));
|
||||
|
||||
export function resize() {
|
||||
$cmInstance.view?.requestMeasure();
|
||||
}
|
||||
|
||||
export function focus() {
|
||||
$cmInstance.view?.focus();
|
||||
}
|
||||
|
||||
export function getEditorState() {
|
||||
return $cmInstance.view?.state.toJSON({ history: historyField });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} state
|
||||
*/
|
||||
export function setEditorState(state) {
|
||||
if (!$cmInstance.view) return;
|
||||
|
||||
$cmInstance.view.setState(
|
||||
EditorState.fromJSON(state, { extensions, doc: state.doc }, { history: historyField })
|
||||
);
|
||||
$cmInstance.view?.dispatch({
|
||||
changes: { from: 0, to: $cmInstance.view.state.doc.length, insert: state.doc },
|
||||
effects: [StateEffect.reconfigure.of($cmInstance.extensions ?? [])]
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearEditorState() {
|
||||
await tick();
|
||||
|
||||
$cmInstance.view?.setState(EditorState.create({ extensions, doc: '' }));
|
||||
$cmInstance.view?.dispatch({
|
||||
changes: { from: 0, to: $cmInstance.view.state.doc.length, insert: '' },
|
||||
effects: [StateEffect.reconfigure.of($cmInstance.extensions ?? [])]
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {StateEffectType<Range<Decoration>[]>} */
|
||||
const addMarksDecoration = StateEffect.define();
|
||||
|
||||
// This value must be added to the set of extensions to enable this
|
||||
const markField = StateField.define({
|
||||
// Start with an empty set of decorations
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
// This is called whenever the editor updates—it computes the new set
|
||||
update(value, tr) {
|
||||
// Move the decorations to account for document changes
|
||||
value = value.map(tr.changes);
|
||||
// If this transaction adds or removes decorations, apply those changes
|
||||
for (let effect of tr.effects) {
|
||||
if (effect.is(addMarksDecoration)) value = value.update({ add: effect.value, sort: true });
|
||||
}
|
||||
return value;
|
||||
},
|
||||
// Indicate that this field provides a set of decorations
|
||||
provide: (f) => EditorView.decorations.from(f)
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {object} param0
|
||||
* @param {number} param0.from
|
||||
* @param {number} param0.to
|
||||
* @param {string} [param0.className]
|
||||
*/
|
||||
export function markText({ from, to, className = 'mark-text' }) {
|
||||
const executedMark = Decoration.mark({
|
||||
class: className
|
||||
});
|
||||
|
||||
$cmInstance.view?.dispatch({
|
||||
effects: [
|
||||
StateEffect.appendConfig.of(markField),
|
||||
addMarksDecoration.of([executedMark.range(from, to)])
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
export function unmarkText() {
|
||||
$cmInstance.view?.dispatch({
|
||||
effects: StateEffect.reconfigure.of($cmInstance.extensions ?? [])
|
||||
});
|
||||
}
|
||||
|
||||
const cmInstance = withCodemirrorInstance();
|
||||
|
||||
/** @type {number} */
|
||||
let w;
|
||||
/** @type {number} */
|
||||
let h;
|
||||
|
||||
let marked = false;
|
||||
|
||||
let updating_externally = false;
|
||||
|
||||
/** @type {import('@codemirror/state').Extension[]} */
|
||||
let extensions = [];
|
||||
|
||||
let cursor_pos = 0;
|
||||
|
||||
$: if ($cmInstance.view) {
|
||||
fulfil_module_editor_ready();
|
||||
}
|
||||
|
||||
$: if ($cmInstance.view && w && h) resize();
|
||||
|
||||
$: if (marked) {
|
||||
unmarkText();
|
||||
marked = false;
|
||||
}
|
||||
|
||||
const watcher = EditorView.updateListener.of((viewUpdate) => {
|
||||
if (viewUpdate.selectionSet) {
|
||||
cursorIndex.set(viewUpdate.state.selection.main.head);
|
||||
}
|
||||
});
|
||||
|
||||
const { files, selected } = get_repl_context();
|
||||
|
||||
const svelte_rune_completions = svelteLanguage.data.of({
|
||||
/** @param {import('@codemirror/autocomplete').CompletionContext} context */
|
||||
autocomplete: (context) => autocomplete(context, $selected, $files)
|
||||
});
|
||||
|
||||
const js_rune_completions = javascriptLanguage.data.of({
|
||||
/** @param {import('@codemirror/autocomplete').CompletionContext} context */
|
||||
autocomplete: (context) => autocomplete(context, $selected, $files)
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="codemirror-container"
|
||||
use:codemirror={{
|
||||
value: code,
|
||||
setup: 'basic',
|
||||
useTabs: tab,
|
||||
tabSize: 2,
|
||||
theme: svelteTheme,
|
||||
readonly,
|
||||
cursorPos: cursor_pos,
|
||||
lang,
|
||||
langMap: {
|
||||
js: () => import('@codemirror/lang-javascript').then((m) => m.javascript()),
|
||||
json: () => import('@codemirror/lang-json').then((m) => m.json()),
|
||||
md: () => import('@codemirror/lang-markdown').then((m) => m.markdown()),
|
||||
css: () => import('@codemirror/lang-css').then((m) => m.css()),
|
||||
svelte: () => import('@replit/codemirror-lang-svelte').then((m) => m.svelte())
|
||||
},
|
||||
lint: diagnostics,
|
||||
lintOptions: { delay: 200 },
|
||||
autocomplete: true,
|
||||
extensions: [svelte_rune_completions, js_rune_completions, watcher],
|
||||
instanceStore: cmInstance
|
||||
}}
|
||||
on:codemirror:textChange={({ detail: value }) => {
|
||||
code = value;
|
||||
dispatch('change', { value: code });
|
||||
}}
|
||||
>
|
||||
{#if !$cmInstance.view}
|
||||
<pre style="position: absolute; left: 0; top: 0">{code}</pre>
|
||||
|
||||
<div style="position: absolute; width: 100%; bottom: 0">
|
||||
<Message kind="info">loading editor...</Message>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.codemirror-container {
|
||||
--warning: hsl(40 100% 70%);
|
||||
--error: hsl(0 100% 90%);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.dark) .codemirror-container {
|
||||
--warning: hsl(40 100% 50%);
|
||||
--error: hsl(0 100% 70%);
|
||||
}
|
||||
|
||||
.codemirror-container :global {
|
||||
* {
|
||||
font: 400 var(--sk-text-xs) / 1.7 var(--sk-font-mono);
|
||||
}
|
||||
|
||||
.mark-text {
|
||||
background-color: var(--sk-selection-color);
|
||||
backdrop-filter: opacity(40%);
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.error-loc {
|
||||
position: relative;
|
||||
border-bottom: 2px solid #da106e;
|
||||
}
|
||||
|
||||
.error-line {
|
||||
background-color: rgba(200, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.cm-tooltip {
|
||||
border: none;
|
||||
background: var(--sk-back-3);
|
||||
font-family: var(--sk-font);
|
||||
max-width: calc(100vw - 10em);
|
||||
position: relative;
|
||||
filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.cm-tooltip-section {
|
||||
position: relative;
|
||||
padding: 0.5em;
|
||||
left: -13px;
|
||||
background: var(--bg);
|
||||
border-radius: 2px;
|
||||
max-width: 64em;
|
||||
}
|
||||
|
||||
.cm-tooltip-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
transform: rotate(45deg);
|
||||
background-color: var(--bg);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.cm-tooltip-below .cm-tooltip-section {
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.cm-tooltip-above .cm-tooltip-section {
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.cm-tooltip-below .cm-tooltip-section::before {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
.cm-tooltip-above .cm-tooltip-section::before {
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
.cm-tooltip:has(.cm-diagnostic) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cm-tooltip:has(.cm-diagnostic-warning) {
|
||||
--bg: var(--warning);
|
||||
--fg: #222;
|
||||
}
|
||||
|
||||
.cm-tooltip:has(.cm-diagnostic-error) {
|
||||
--bg: var(--error);
|
||||
--fg: #222;
|
||||
}
|
||||
|
||||
.cm-diagnostic {
|
||||
padding: 0.2em 0.4em;
|
||||
position: relative;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.cm-diagnostic:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.cm-diagnostic-error {
|
||||
border: none;
|
||||
filter: drop-shadow(0px 0px 6px var(--error-bg));
|
||||
}
|
||||
|
||||
.cm-diagnostic :not(code) {
|
||||
font-family: var(--sk-font);
|
||||
}
|
||||
|
||||
.cm-diagnosticText {
|
||||
color: var(--fg);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.cm-diagnosticText code {
|
||||
color: inherit;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 2px;
|
||||
top: 0;
|
||||
padding: 0.2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.cm-diagnosticText strong {
|
||||
font-size: 0.9em;
|
||||
/* font-weight: 700; */
|
||||
font-family: var(--sk-font-mono);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: none;
|
||||
padding: 4px 4px 4px 60px;
|
||||
resize: none;
|
||||
font-family: var(--sk-font-mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
color: #ccc;
|
||||
tab-size: 2;
|
||||
-moz-tab-size: 2;
|
||||
}
|
||||
</style>
|
@ -1,462 +0,0 @@
|
||||
<script>
|
||||
import { get_repl_context } from '$lib/context.js';
|
||||
import { get_full_filename } from '$lib/utils.js';
|
||||
import { createEventDispatcher, tick } from 'svelte';
|
||||
import RunesInfo from './RunesInfo.svelte';
|
||||
import Migrate from './Migrate.svelte';
|
||||
|
||||
/** @type {boolean} */
|
||||
export let show_modified;
|
||||
|
||||
/** @type {boolean} */
|
||||
export let runes;
|
||||
|
||||
/** @type {ReturnType<typeof createEventDispatcher<{
|
||||
* remove: { files: import('$lib/types').File[]; diff: import('$lib/types').File },
|
||||
* add: { files: import('$lib/types').File[]; diff: import('$lib/types').File },
|
||||
* }>>} */
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const {
|
||||
files,
|
||||
handle_select,
|
||||
module_editor,
|
||||
rebundle,
|
||||
selected,
|
||||
selected_name,
|
||||
EDITOR_STATE_MAP
|
||||
} = get_repl_context();
|
||||
|
||||
/** @type {string | null} */
|
||||
let editing_name = null;
|
||||
|
||||
let input_value = '';
|
||||
|
||||
/** @param {string} filename */
|
||||
function select_file(filename) {
|
||||
if ($selected_name !== filename) {
|
||||
editing_name = null;
|
||||
handle_select(filename);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('$lib/types').File} file */
|
||||
function edit_tab(file) {
|
||||
if ($selected_name === get_full_filename(file)) {
|
||||
editing_name = get_full_filename(file);
|
||||
input_value = file.name;
|
||||
}
|
||||
}
|
||||
|
||||
async function close_edit() {
|
||||
const match = /(.+)\.(svelte|js|json|md|css)$/.exec(input_value ?? '');
|
||||
|
||||
const edited_file = $files.find((val) => get_full_filename(val) === editing_name);
|
||||
|
||||
if (!edited_file) return;
|
||||
|
||||
edited_file.name = match ? match[1] : input_value;
|
||||
|
||||
if (!$selected) return;
|
||||
|
||||
if (is_file_name_used($selected)) {
|
||||
let i = 1;
|
||||
let name = $selected.name;
|
||||
|
||||
do {
|
||||
const file = $files.find(
|
||||
(val) =>
|
||||
get_full_filename(val) === get_full_filename(edited_file) &&
|
||||
// @ts-ignore
|
||||
val.source === $selected.source
|
||||
);
|
||||
|
||||
if (!file) break;
|
||||
|
||||
file.name = `${name}_${i++}`;
|
||||
} while (is_file_name_used($selected));
|
||||
|
||||
const idx = $files.findIndex(
|
||||
(val) => get_full_filename(val) === get_full_filename(edited_file)
|
||||
);
|
||||
$files[idx] = edited_file;
|
||||
}
|
||||
|
||||
const idx = $files.findIndex(
|
||||
(val) => get_full_filename(val) === get_full_filename(edited_file)
|
||||
);
|
||||
if (match?.[2]) $files[idx].type = match[2];
|
||||
|
||||
if (editing_name) {
|
||||
const old_state = EDITOR_STATE_MAP.get(editing_name);
|
||||
if (old_state) {
|
||||
EDITOR_STATE_MAP.set(get_full_filename(edited_file), old_state);
|
||||
EDITOR_STATE_MAP.delete(editing_name);
|
||||
}
|
||||
}
|
||||
|
||||
editing_name = null;
|
||||
|
||||
// re-select, in case the type changed
|
||||
handle_select(get_full_filename(edited_file));
|
||||
|
||||
$files = $files;
|
||||
|
||||
// focus the editor, but wait a beat (so key events aren't misdirected)
|
||||
await tick();
|
||||
|
||||
$module_editor?.focus();
|
||||
|
||||
rebundle();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
*/
|
||||
function remove(filename) {
|
||||
const file = $files.find((val) => get_full_filename(val) === filename);
|
||||
const idx = $files.findIndex((val) => get_full_filename(val) === filename);
|
||||
|
||||
if (!file) return;
|
||||
|
||||
let result = confirm(`Are you sure you want to delete ${get_full_filename(file)}?`);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
$files = $files.filter((file) => get_full_filename(file) !== filename);
|
||||
|
||||
dispatch('remove', { files: $files, diff: file });
|
||||
|
||||
EDITOR_STATE_MAP.delete(get_full_filename(file));
|
||||
|
||||
$selected_name = idx === 1 ? 'App.svelte' : get_full_filename(file);
|
||||
handle_select($selected_name);
|
||||
}
|
||||
|
||||
/** @param {FocusEvent & { currentTarget: HTMLInputElement }} event */
|
||||
async function select_input(event) {
|
||||
await tick();
|
||||
|
||||
event.currentTarget.select();
|
||||
}
|
||||
|
||||
let uid = 1;
|
||||
|
||||
function add_new() {
|
||||
const file = {
|
||||
name: uid++ ? `Component${uid}` : 'Component1',
|
||||
type: 'svelte',
|
||||
source: '',
|
||||
modified: true
|
||||
};
|
||||
|
||||
$files = $files.concat(file);
|
||||
|
||||
editing_name = get_full_filename(file);
|
||||
|
||||
input_value = file.name;
|
||||
|
||||
handle_select(editing_name);
|
||||
|
||||
rebundle();
|
||||
|
||||
dispatch('add', { files: $files, diff: file });
|
||||
|
||||
$files = $files;
|
||||
}
|
||||
|
||||
/** @param {import('$lib/types').File} editing */
|
||||
function is_file_name_used(editing) {
|
||||
return $files.find(
|
||||
(file) => JSON.stringify(file) !== JSON.stringify($selected) && file.name === editing.name
|
||||
);
|
||||
}
|
||||
|
||||
// drag and drop
|
||||
/** @type {string | null} */
|
||||
let from = null;
|
||||
|
||||
/** @type {string | null} */
|
||||
let over = null;
|
||||
|
||||
/** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
|
||||
function dragStart(event) {
|
||||
from = event.currentTarget.id;
|
||||
}
|
||||
|
||||
function dragLeave() {
|
||||
over = null;
|
||||
}
|
||||
|
||||
/** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
|
||||
function dragOver(event) {
|
||||
over = event.currentTarget.id;
|
||||
}
|
||||
|
||||
function dragEnd() {
|
||||
if (from && over) {
|
||||
const from_index = $files.findIndex((file) => file.name === from);
|
||||
const to_index = $files.findIndex((file) => file.name === over);
|
||||
|
||||
const from_component = $files[from_index];
|
||||
|
||||
$files.splice(from_index, 1);
|
||||
|
||||
$files = $files.slice(0, to_index).concat(from_component).concat($files.slice(to_index));
|
||||
}
|
||||
|
||||
from = over = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="component-selector">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="file-tabs" on:dblclick={add_new}>
|
||||
{#each $files as file, index (file.name)}
|
||||
{@const filename = get_full_filename(file)}
|
||||
<div
|
||||
id={file.name}
|
||||
class="button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class:active={filename === $selected_name}
|
||||
class:draggable={filename !== editing_name && index !== 0}
|
||||
class:drag-over={over === file.name}
|
||||
on:click={() => select_file(filename)}
|
||||
on:keyup={(e) => e.key === ' ' && select_file(filename)}
|
||||
on:dblclick|stopPropagation={() => {}}
|
||||
draggable={filename !== editing_name}
|
||||
on:dragstart={dragStart}
|
||||
on:dragover|preventDefault={dragOver}
|
||||
on:dragleave={dragLeave}
|
||||
on:drop|preventDefault={dragEnd}
|
||||
>
|
||||
<i class="drag-handle"></i>
|
||||
{#if file.name === 'App' && filename !== editing_name}
|
||||
<div class="uneditable">
|
||||
App.svelte{#if show_modified && file.modified}*{/if}
|
||||
</div>
|
||||
{:else if filename === editing_name}
|
||||
{@const editing_file = $files.find((file) => get_full_filename(file) === editing_name)}
|
||||
|
||||
{#if editing_file}
|
||||
<span class="input-sizer">
|
||||
{input_value + (/\./.test(input_value) ? '' : `.${editing_file.type}`)}
|
||||
</span>
|
||||
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
spellcheck={false}
|
||||
bind:value={input_value}
|
||||
on:focus={select_input}
|
||||
on:blur={close_edit}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (!is_file_name_used(editing_file)) {
|
||||
e.currentTarget.blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
class:duplicate={is_file_name_used(editing_file)}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="editable"
|
||||
title="edit component name"
|
||||
on:click={() => edit_tab(file)}
|
||||
on:keyup={(e) => e.key === ' ' && edit_tab(file)}
|
||||
>
|
||||
{file.name}.{file.type}{#if show_modified && file.modified}*{/if}
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<span
|
||||
class="remove"
|
||||
on:click={() => remove(filename)}
|
||||
on:keyup={(e) => e.key === ' ' && remove(filename)}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24">
|
||||
<line stroke="#999" x1="18" y1="6" x2="6" y2="18" />
|
||||
<line stroke="#999" x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button class="add-new" on:click={add_new} title="add new component">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24">
|
||||
<line stroke="#999" x1="12" y1="5" x2="12" y2="19" />
|
||||
<line stroke="#999" x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="runes-info"><RunesInfo {runes} /></div>
|
||||
|
||||
<div class="migrate-info"><Migrate /></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.component-selector {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--sk-text-4);
|
||||
/* overflow: hidden; */
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.file-tabs {
|
||||
border: none;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.file-tabs .button,
|
||||
.add-new {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font: 400 12px/1.5 var(--sk-font);
|
||||
background: var(--sk-back-1);
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 12px 14px 8px 16px;
|
||||
margin: 0;
|
||||
color: var(--sk-text-3);
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-tabs .button.active {
|
||||
/* color: var(--second); */
|
||||
color: var(--sk-text-2, #333);
|
||||
border-bottom: 3px solid var(--sk-theme-1);
|
||||
}
|
||||
|
||||
.editable,
|
||||
.uneditable,
|
||||
.input-sizer,
|
||||
input {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.input-sizer {
|
||||
color: var(--sk-text-3, #ccc);
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 16px;
|
||||
top: 12px;
|
||||
font: 400 12px/1.5 var(--sk-font);
|
||||
border: none;
|
||||
color: var(--sk-theme-3);
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.duplicate {
|
||||
color: var(--sk-theme-1);
|
||||
}
|
||||
|
||||
.remove {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 1px;
|
||||
top: 4px;
|
||||
width: 16px;
|
||||
text-align: right;
|
||||
padding: 12px 0 12px 5px;
|
||||
font-size: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
color: var(--sk-theme-3);
|
||||
}
|
||||
|
||||
.file-tabs .button.active .editable {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.file-tabs .button.active .remove {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-tabs .button.drag-over {
|
||||
background: #67677814;
|
||||
}
|
||||
|
||||
.file-tabs .button.drag-over {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.add-new {
|
||||
padding: 12px 10px 8px 8px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-new:hover {
|
||||
color: var(--sk-theme-3) !important;
|
||||
}
|
||||
|
||||
.runes-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.migrate-info {
|
||||
flex: 0 1 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
width: 5px;
|
||||
height: 25px;
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 9px;
|
||||
--drag-handle-color: #dedede;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--sk-back-4, --drag-handle-color) 1px,
|
||||
var(--sk-back-1, white) 1px,
|
||||
var(--sk-back-1, white) 2px,
|
||||
var(--sk-back-4, --drag-handle-color) 2px,
|
||||
var(--sk-back-4, --drag-handle-color) 3px,
|
||||
var(--sk-back-1, white) 3px,
|
||||
var(--sk-back-1, white) 4px,
|
||||
var(--sk-back-4, --drag-handle-color) 4px
|
||||
);
|
||||
}
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
-o-object-fit: contain;
|
||||
object-fit: contain;
|
||||
-webkit-transform-origin: center center;
|
||||
transform-origin: center center;
|
||||
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
</style>
|
@ -1,22 +0,0 @@
|
||||
<script>
|
||||
import { get_repl_context } from '$lib/context.js';
|
||||
|
||||
const { migrate } = get_repl_context();
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<button on:click={migrate} title="Migrate this component towards the new syntax">migrate</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.4rem;
|
||||
padding: 0.8rem;
|
||||
gap: 0.5rem;
|
||||
margin-right: 0.3rem;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
@ -1,90 +0,0 @@
|
||||
<script>
|
||||
import { get_repl_context } from '$lib/context.js';
|
||||
import CodeMirror from '../CodeMirror.svelte';
|
||||
|
||||
/** @type {boolean} */
|
||||
export let autocomplete;
|
||||
|
||||
/** @type {any} */ // TODO
|
||||
export let error;
|
||||
|
||||
/** @type {any[]} */ // TODO
|
||||
export let warnings;
|
||||
|
||||
export function focus() {
|
||||
$module_editor?.focus();
|
||||
}
|
||||
|
||||
const { handle_change, module_editor } = get_repl_context();
|
||||
</script>
|
||||
|
||||
<div class="editor-wrapper">
|
||||
<div class="editor notranslate" translate="no">
|
||||
<CodeMirror
|
||||
bind:this={$module_editor}
|
||||
diagnostics={() => {
|
||||
if (error) {
|
||||
return [
|
||||
{
|
||||
severity: 'error',
|
||||
from: error.position[0],
|
||||
to: error.position[1],
|
||||
message: error.message,
|
||||
renderMessage: () => {
|
||||
// TODO expose error codes, so we can link to docs in future
|
||||
const span = document.createElement('span');
|
||||
span.innerHTML = `${error.message
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/`(.+?)`/g, `<code>$1</code>`)}`;
|
||||
return span;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (warnings) {
|
||||
return warnings.map((warning) => ({
|
||||
severity: 'warning',
|
||||
from: warning.start.character,
|
||||
to: warning.end.character,
|
||||
message: warning.message,
|
||||
renderMessage: () => {
|
||||
const span = document.createElement('span');
|
||||
span.innerHTML = `${warning.message
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/`(.+?)`/g, `<code>$1</code>`)} <strong>(${warning.code})</strong>`;
|
||||
return span;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
}}
|
||||
on:change={handle_change}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.editor-wrapper {
|
||||
z-index: 5;
|
||||
background: var(--sk-back-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor {
|
||||
height: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
:global(.columns) .editor-wrapper {
|
||||
/* make it easier to interact with scrollbar */
|
||||
padding-right: 8px;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -1,149 +0,0 @@
|
||||
<script>
|
||||
import { get_repl_context } from '$lib/context.js';
|
||||
|
||||
/** @type {boolean} */
|
||||
export let runes;
|
||||
|
||||
let open = false;
|
||||
|
||||
const { selected_name } = get_repl_context();
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Escape') open = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="container">
|
||||
<button class:active={runes} class:open on:click={() => (open = !open)}>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M9.4,1H19l-5.9,7.7h8L8.3,23L11,12.6H3.5L9.4,1z" />
|
||||
</svg>
|
||||
|
||||
runes
|
||||
</button>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions
|
||||
(This is taken care of by the <svelte:window> above) -->
|
||||
<div class="modal-backdrop" on:click={() => (open = false)}></div>
|
||||
<div class="popup">
|
||||
{#if $selected_name.endsWith('.svelte.js')}
|
||||
<p>
|
||||
Files with a <code>.svelte.js</code> extension are always in
|
||||
<a href="https://svelte.dev/blog/runes">runes mode</a>.
|
||||
</p>
|
||||
{:else if $selected_name.endsWith('.js')}
|
||||
<p>
|
||||
To use <a href="https://svelte.dev/blog/runes">runes</a> in a JavaScript file, change the
|
||||
extension to <code>.svelte.js</code>.
|
||||
</p>
|
||||
{:else if $selected_name.endsWith('.svelte')}
|
||||
{#if runes}
|
||||
<p>
|
||||
This component is in
|
||||
<a href="https://svelte.dev/blog/runes">runes mode</a>.
|
||||
</p>
|
||||
<p>To disable runes mode, add the following to the top of your component:</p>
|
||||
<pre><code><svelte:options runes={'{false}'} /></code></pre>
|
||||
{:else}
|
||||
<p>This component is not in <a href="https://svelte.dev/blog/runes">runes mode</a>.</p>
|
||||
<p>
|
||||
To enable runes mode, either start using runes in your code, or add the following to the
|
||||
top of your component:
|
||||
</p>
|
||||
<pre><code><svelte:options runes /></code></pre>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>
|
||||
Edit a <code>.svelte</code>, <code>.svelte.js</code> or <code>.js</code> file to see
|
||||
information on <a href="https://svelte.dev/blog/runes">runes mode</a>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.4rem;
|
||||
padding: 0.8rem;
|
||||
gap: 0.5rem;
|
||||
margin-right: 0.3rem;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
button.open {
|
||||
background: var(--sk-back-3);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1.6rem !important;
|
||||
height: 1.6rem !important;
|
||||
top: 0.05rem;
|
||||
}
|
||||
|
||||
path {
|
||||
stroke: #ccc;
|
||||
fill: transparent;
|
||||
transition:
|
||||
stroke 0.2s,
|
||||
fill 0.2s;
|
||||
}
|
||||
|
||||
.active svg {
|
||||
animation: bump 0.4s;
|
||||
}
|
||||
|
||||
.active path {
|
||||
stroke: #ff3e00;
|
||||
fill: #ff3e00;
|
||||
}
|
||||
|
||||
@keyframes bump {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: var(--sk-back-1);
|
||||
opacity: 0.7;
|
||||
backdrop-filter: blur(5px);
|
||||
z-index: 9998;
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
top: 2.2em;
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
max-width: 320px;
|
||||
z-index: 9999;
|
||||
background: var(--sk-back-3);
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.popup p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.popup p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
@ -1,39 +0,0 @@
|
||||
<script>
|
||||
export let checked = false;
|
||||
|
||||
import Checkbox from './Checkbox.svelte';
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="input-output-toggle">
|
||||
<span class:active={!checked} style="text-align: right">input</span>
|
||||
<span style="display:grid; place-items: center">
|
||||
<Checkbox bind:checked />
|
||||
</span>
|
||||
<span class:active={checked}>output</span>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.input-output-toggle {
|
||||
position: relative;
|
||||
display: grid;
|
||||
user-select: none;
|
||||
flex: 0;
|
||||
grid-template-columns: 1fr 40px 1fr;
|
||||
grid-gap: 0.5em;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
border-top: 1px solid var(--sk-theme-2);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--sk-text-4);
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--sk-text-2);
|
||||
}
|
||||
</style>
|
@ -1,104 +0,0 @@
|
||||
<script>
|
||||
import { slide } from 'svelte/transition';
|
||||
import { get_repl_context } from './context.js';
|
||||
|
||||
/** @type {'info' | 'warning' | 'error'} */
|
||||
export let kind = 'info';
|
||||
|
||||
/** @type {import('./types').MessageDetails | undefined} */
|
||||
export let details = undefined;
|
||||
|
||||
/** @type {string | undefined} */
|
||||
export let filename = undefined;
|
||||
|
||||
export let truncate = false;
|
||||
|
||||
const { go_to_warning_pos } = get_repl_context();
|
||||
|
||||
/** @param {import('./types').MessageDetails} details */
|
||||
function message(details) {
|
||||
let str = details.message || '[missing message]';
|
||||
|
||||
let loc = [];
|
||||
|
||||
if (details.filename && details.filename !== filename) loc.push(details.filename);
|
||||
|
||||
if (details.start) loc.push(details.start.line, details.start.column);
|
||||
|
||||
return str + (loc.length ? ` (${loc.join(':')})` : ``);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div transition:slide={{ duration: 100 }} class="message {kind}" class:truncate>
|
||||
{#if details}
|
||||
<button
|
||||
class:navigable={details.filename}
|
||||
on:click={() => go_to_warning_pos(details)}
|
||||
on:keyup={(e) => e.key === ' ' && go_to_warning_pos(details)}
|
||||
>
|
||||
{message(details)}
|
||||
</button>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
button {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.message {
|
||||
position: relative;
|
||||
color: white;
|
||||
padding: 12px 16px 12px 44px;
|
||||
font: 400 12px/1.7 var(--sk-font);
|
||||
margin: 0;
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
|
||||
.navigable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message::before {
|
||||
content: '!';
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 10px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
border: 2px solid white;
|
||||
box-sizing: content-box;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
white-space: pre;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #da106e;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: #e47e0a;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: var(--sk-theme-2);
|
||||
}
|
||||
</style>
|
@ -1,161 +0,0 @@
|
||||
<script>
|
||||
import { get_repl_context } from '$lib/context.js';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let key = '';
|
||||
/** @type {import('svelte/types/compiler/interfaces').Ast} */
|
||||
export let value;
|
||||
export let collapsed = true;
|
||||
/** @type {import('svelte/types/compiler/interfaces').Ast[]} */
|
||||
export let path_nodes = [];
|
||||
export let autoscroll = true;
|
||||
|
||||
const { module_editor, toggleable } = get_repl_context();
|
||||
|
||||
/** @type {HTMLLIElement} */
|
||||
let list_item_el;
|
||||
|
||||
$: is_root = path_nodes[0] === value;
|
||||
$: is_leaf = path_nodes[path_nodes.length - 1] === value;
|
||||
$: is_ast_array = Array.isArray(value);
|
||||
$: is_collapsable = value && typeof value === 'object';
|
||||
$: is_markable =
|
||||
is_collapsable &&
|
||||
'start' in value &&
|
||||
'end' in value &&
|
||||
typeof value.start === 'number' &&
|
||||
typeof value.end === 'number';
|
||||
$: key_text = key ? `${key}:` : '';
|
||||
|
||||
let preview_text = '';
|
||||
$: {
|
||||
if (!is_collapsable || !collapsed) break $;
|
||||
|
||||
if (is_ast_array) {
|
||||
if (!('length' in value)) break $;
|
||||
|
||||
preview_text = `[ ${value.length} element${value.length === 1 ? '' : 's'} ]`;
|
||||
} else {
|
||||
preview_text = `{ ${Object.keys(value).join(', ')} }`;
|
||||
}
|
||||
}
|
||||
|
||||
$: collapsed = !path_nodes.includes(value);
|
||||
|
||||
$: if (autoscroll && is_leaf && !$toggleable) {
|
||||
// wait for all nodes to render before scroll
|
||||
tick().then(() => {
|
||||
if (list_item_el) {
|
||||
list_item_el.scrollIntoView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {MouseEvent | FocusEvent} e */
|
||||
function handle_mark_text(e) {
|
||||
if (is_markable) {
|
||||
e.stopPropagation();
|
||||
|
||||
if (
|
||||
'start' in value &&
|
||||
'end' in value &&
|
||||
typeof value.start === 'number' &&
|
||||
typeof value.end === 'number'
|
||||
) {
|
||||
$module_editor?.markText({ from: value.start ?? 0, to: value.end ?? 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {MouseEvent} e */
|
||||
function handle_unmark_text(e) {
|
||||
if (is_markable) {
|
||||
e.stopPropagation();
|
||||
$module_editor?.unmarkText();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<li
|
||||
bind:this={list_item_el}
|
||||
class:marked={!is_root && is_leaf}
|
||||
on:mouseover={handle_mark_text}
|
||||
on:focus={handle_mark_text}
|
||||
on:mouseleave={handle_unmark_text}
|
||||
>
|
||||
{#if !is_root && is_collapsable}
|
||||
<button class="ast-toggle" class:open={!collapsed} on:click={() => (collapsed = !collapsed)}>
|
||||
{key_text}
|
||||
</button>
|
||||
{:else if key_text}
|
||||
<span>{key_text}</span>
|
||||
{/if}
|
||||
{#if is_collapsable}
|
||||
{#if collapsed && !is_root}
|
||||
<button class="preview" on:click={() => (collapsed = !collapsed)}>
|
||||
{preview_text}
|
||||
</button>
|
||||
{:else}
|
||||
<span>{is_ast_array ? '[' : '{'}</span>
|
||||
<ul>
|
||||
{#each Object.entries(value) as [k, v]}
|
||||
<svelte:self key={is_ast_array ? '' : k} value={v} {path_nodes} {autoscroll} />
|
||||
{/each}
|
||||
</ul>
|
||||
<span>{is_ast_array ? ']' : '}'}</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<span class="token {typeof value}">
|
||||
{JSON.stringify(value)}
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
padding: 0 0 0 2ch;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.marked {
|
||||
background-color: var(--sk-highlight-color);
|
||||
}
|
||||
|
||||
.preview {
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ast-toggle {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ast-toggle::before {
|
||||
content: '\25B6';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -1.3rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.ast-toggle.open::before {
|
||||
content: '\25BC';
|
||||
}
|
||||
|
||||
.token {
|
||||
color: var(--sk-code-base);
|
||||
}
|
||||
|
||||
.token.string {
|
||||
color: var(--sk-code-string);
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: var(--sk-code-number);
|
||||
}
|
||||
</style>
|
@ -1,105 +0,0 @@
|
||||
<script>
|
||||
import Message from '../Message.svelte';
|
||||
import AstNode from './AstNode.svelte';
|
||||
import { cursorIndex } from '../CodeMirror.svelte';
|
||||
|
||||
/** @type {import('svelte/types/compiler/interfaces').Ast} */
|
||||
export let ast;
|
||||
export let autoscroll = true;
|
||||
|
||||
// $cursor_index may go over the max since ast computation is usually slower.
|
||||
// clamping this helps prevent the collapse view flashing
|
||||
$: max_cursor_index = !ast ? $cursorIndex : Math.min($cursorIndex, get_ast_max_end(ast));
|
||||
|
||||
$: path_nodes = find_deepest_path(max_cursor_index, [ast]) || [];
|
||||
|
||||
/**
|
||||
* @param {number} cursor
|
||||
* @param {import('svelte/types/compiler/interfaces').Ast[]} paths
|
||||
* @returns {import('svelte/types/compiler/interfaces').Ast[] | undefined}
|
||||
*/
|
||||
function find_deepest_path(cursor, paths) {
|
||||
const value = paths[paths.length - 1];
|
||||
|
||||
if (!value) return;
|
||||
|
||||
for (const v of Object.values(value)) {
|
||||
if (typeof v === 'object') {
|
||||
const result = find_deepest_path(cursor, paths.concat([v]));
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
'start' in value &&
|
||||
'end' in value &&
|
||||
typeof value.start === 'number' &&
|
||||
typeof value.end === 'number' &&
|
||||
value.start <= cursor &&
|
||||
cursor <= value.end
|
||||
) {
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('svelte/types/compiler/interfaces').Ast} ast */
|
||||
function get_ast_max_end(ast) {
|
||||
let max_end = 0;
|
||||
|
||||
for (const node of Object.values(ast)) {
|
||||
if (node && typeof node.end === 'number' && node.end > max_end) {
|
||||
max_end = node.end;
|
||||
}
|
||||
}
|
||||
|
||||
return max_end;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ast-view">
|
||||
<pre>
|
||||
<code>
|
||||
{#if typeof ast === 'object'}
|
||||
<ul>
|
||||
<AstNode value={ast} {path_nodes} {autoscroll} collapsed={false} />
|
||||
</ul>
|
||||
{:else}
|
||||
<p>No AST available</p>
|
||||
{/if}
|
||||
</code>
|
||||
</pre>
|
||||
<Message kind="info">The AST is not public API and may change at any point in time</Message>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ast-view {
|
||||
--base: hsl(45, 7%, 45%);
|
||||
--string: hsl(41, 37%, 45%);
|
||||
--number: hsl(102, 27%, 50%);
|
||||
background: var(--sk-back-3);
|
||||
color: var(--sk-code-base);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ast-view,
|
||||
pre,
|
||||
code {
|
||||
height: 100%;
|
||||
block-size: 100%;
|
||||
font: 400 var(--sk-text-xs) / 1.7 var(--sk-font-mono);
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: normal;
|
||||
padding: 1rem;
|
||||
tab-size: 2;
|
||||
-moz-tab-size: 2;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
</style>
|
@ -1,92 +0,0 @@
|
||||
import Worker from '../workers/compiler/index.js?worker';
|
||||
|
||||
const workers = new Map();
|
||||
|
||||
let uid = 1;
|
||||
|
||||
export default class Compiler {
|
||||
/** @type {Worker} */
|
||||
worker;
|
||||
|
||||
/** @type {Map<number, (...arg: any) => void>} */
|
||||
handlers = new Map();
|
||||
|
||||
/** @param {string} svelte_url */
|
||||
constructor(svelte_url) {
|
||||
if (!workers.has(svelte_url)) {
|
||||
const worker = new Worker();
|
||||
worker.postMessage({ type: 'init', svelte_url });
|
||||
workers.set(svelte_url, worker);
|
||||
}
|
||||
|
||||
this.worker = workers.get(svelte_url);
|
||||
|
||||
this.worker.addEventListener(
|
||||
'message',
|
||||
/**
|
||||
* @param {MessageEvent<import('$lib/workers/workers').CompileMessageData>} event
|
||||
*/
|
||||
(event) => {
|
||||
const handler = this.handlers.get(event.data.id);
|
||||
|
||||
if (handler) {
|
||||
// if no handler, was meant for a different REPL
|
||||
handler(event.data);
|
||||
this.handlers.delete(event.data.id);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('$lib/types').File} file
|
||||
* @param {import('svelte/compiler').CompileOptions} options
|
||||
* @param {boolean} return_ast
|
||||
* @returns {Promise<import('$lib/workers/workers').CompileMessageData>}
|
||||
*/
|
||||
compile(file, options, return_ast) {
|
||||
return new Promise((fulfil) => {
|
||||
const id = uid++;
|
||||
|
||||
this.handlers.set(id, fulfil);
|
||||
|
||||
this.worker.postMessage({
|
||||
id,
|
||||
type: 'compile',
|
||||
source: file.source,
|
||||
options: Object.assign(
|
||||
{
|
||||
name: file.name,
|
||||
filename: `${file.name}.${file.type}`
|
||||
},
|
||||
options
|
||||
),
|
||||
entry: file.name === 'App',
|
||||
return_ast
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('$lib/types').File} file
|
||||
* @returns {Promise<import('$lib/workers/workers').MigrateMessageData>}
|
||||
*/
|
||||
migrate(file) {
|
||||
return new Promise((fulfil) => {
|
||||
const id = uid++;
|
||||
|
||||
this.handlers.set(id, fulfil);
|
||||
|
||||
this.worker.postMessage({
|
||||
id,
|
||||
type: 'migrate',
|
||||
source: file.source,
|
||||
filename: `${file.name}.${file.type}`
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.worker.terminate();
|
||||
}
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<script>
|
||||
import { get_repl_context } from '$lib/context.js';
|
||||
import Checkbox from '../Checkbox.svelte';
|
||||
|
||||
const { compile_options } = get_repl_context();
|
||||
</script>
|
||||
|
||||
<div class="options">
|
||||
result = svelte.compile(source, {
|
||||
<div class="option">
|
||||
<span class="key">generate:</span>
|
||||
|
||||
<input id="client" type="radio" bind:group={$compile_options.generate} value="client" />
|
||||
<label for="client"><span class="string">"client"</span></label>
|
||||
|
||||
<input id="server" type="radio" bind:group={$compile_options.generate} value="server" />
|
||||
<label for="server"><span class="string">"server"</span>,</label>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<span class="key">css:</span>
|
||||
|
||||
<input id="injected" type="radio" bind:group={$compile_options.css} value="injected" />
|
||||
<label for="injected"><span class="string">"injected"</span></label>
|
||||
|
||||
<input id="external" type="radio" bind:group={$compile_options.css} value="external" />
|
||||
<label for="external"><span class="string">"external"</span>,</label>
|
||||
</div>
|
||||
|
||||
<label class="option">
|
||||
<span class="key">dev:</span>
|
||||
<Checkbox bind:checked={$compile_options.dev} />
|
||||
<span class="boolean">{$compile_options.dev}</span>,
|
||||
</label>
|
||||
|
||||
<label class="option">
|
||||
<span class="key">hmr:</span>
|
||||
<Checkbox bind:checked={$compile_options.hmr} />
|
||||
<span class="boolean">{$compile_options.hmr}</span>,
|
||||
</label>
|
||||
});
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<p>
|
||||
note: these options affect the JS output tab, but not the bundle that executes in the Result
|
||||
tab.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.options {
|
||||
padding: 0 10px;
|
||||
font-family: var(--sk-font-mono);
|
||||
font-size: 13px;
|
||||
color: var(--sk-text-2, #999);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: block;
|
||||
padding: 0 0 0 1.25em;
|
||||
white-space: nowrap;
|
||||
color: var(--sk-text-3, #999);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.key {
|
||||
display: inline-block;
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.string {
|
||||
color: hsl(41, 37%, 45%);
|
||||
}
|
||||
|
||||
/* .boolean {
|
||||
color: hsl(45, 7%, 45%);
|
||||
} */
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
label[for] {
|
||||
color: var(--sk-code-string);
|
||||
}
|
||||
|
||||
label :global(input[type='checkbox']) {
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
position: absolute;
|
||||
top: auto;
|
||||
overflow: hidden;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input[type='radio'] + label {
|
||||
padding: 0 0 0 1.6em;
|
||||
margin: 0 0.6em 0 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
input[type='radio']:checked + label {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* input[type=radio]:focus + label {
|
||||
color: #00f;
|
||||
outline: 1px dotted #00f;
|
||||
} */
|
||||
|
||||
input[type='radio'] + label:before {
|
||||
content: '';
|
||||
background: #eee;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
float: left;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-left: -21px;
|
||||
margin-top: 4px;
|
||||
/* vertical-align: top; */
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: box-shadow 0.1s ease-out;
|
||||
}
|
||||
|
||||
input[type='radio'] + label:before {
|
||||
background-color: var(--sk-theme-2);
|
||||
border-radius: 100%;
|
||||
box-shadow: inset 0 0 0 0.5em rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid var(--sk-theme-2);
|
||||
}
|
||||
|
||||
input[type='radio']:checked + label:before {
|
||||
background-color: var(--sk-theme-1);
|
||||
box-shadow: inset 0 0 0 0.15em rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid var(--sk-theme-2);
|
||||
transition: box-shadow 0.2s ease-out;
|
||||
}
|
||||
|
||||
.note {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.note p {
|
||||
font-size: 1.4rem;
|
||||
text-wrap: balance;
|
||||
}
|
||||
</style>
|
@ -1,47 +0,0 @@
|
||||
<script>
|
||||
/** @type {import('svelte/compiler').CompileError} */
|
||||
export let error;
|
||||
</script>
|
||||
|
||||
<div class="error-overlay">
|
||||
<div class="error">
|
||||
<h2>Error compiling {error.filename ?? 'component'}</h2>
|
||||
<pre><code>{error.message}</code></pre>
|
||||
|
||||
{#if error.start}
|
||||
<small>line {error.start.line} column {error.start.column}</small>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 1em;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
background: var(--sk-back-1);
|
||||
padding: 1em;
|
||||
border-top: 4px solid var(--sk-theme-1);
|
||||
border-radius: 4px;
|
||||
filter: drop-shadow(2px 4px 8px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 0.5em;
|
||||
background: var(--sk-back-3);
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--sk-text-4);
|
||||
}
|
||||
</style>
|
@ -1,175 +0,0 @@
|
||||
<script>
|
||||
import { get_repl_context } from '$lib/context.js';
|
||||
import { marked } from 'marked';
|
||||
import CodeMirror from '../CodeMirror.svelte';
|
||||
import AstView from './AstView.svelte';
|
||||
import CompilerOptions from './CompilerOptions.svelte';
|
||||
import PaneWithPanel from './PaneWithPanel.svelte';
|
||||
import Viewer from './Viewer.svelte';
|
||||
|
||||
/** @type {string | null} */
|
||||
export let status;
|
||||
|
||||
/** @type {import('$lib/types').MessageDetails | null} */
|
||||
export let runtimeError = null;
|
||||
|
||||
export let embedded = false;
|
||||
export let relaxed = false;
|
||||
|
||||
/** @type {string} */
|
||||
export let injectedJS;
|
||||
|
||||
/** @type {string} */
|
||||
export let injectedCSS;
|
||||
|
||||
// export let theme;
|
||||
export let showAst = false;
|
||||
|
||||
/** @type {'light' | 'dark'} */
|
||||
export let previewTheme;
|
||||
|
||||
/** @type {import('../types').File | null} */
|
||||
export let selected;
|
||||
|
||||
/** @type {import('../workers/workers').CompileMessageData | null} */
|
||||
export let compiled;
|
||||
|
||||
$: if (selected && js_editor && css_editor) {
|
||||
if (selected.type === 'json') {
|
||||
js_editor.set({ code: `/* Select a component to see its compiled code */`, lang: 'js' });
|
||||
css_editor.set({ code: `/* Select a component to see its compiled code */`, lang: 'css' });
|
||||
} else if (selected.type === 'md') {
|
||||
markdown = marked(selected.source);
|
||||
} else if (compiled) {
|
||||
js_editor.set({ code: compiled.result.js, lang: 'js' });
|
||||
css_editor.set({ code: compiled.result.css, lang: 'css' });
|
||||
}
|
||||
}
|
||||
|
||||
const { module_editor } = get_repl_context();
|
||||
|
||||
/** @type {CodeMirror} */
|
||||
let js_editor;
|
||||
|
||||
/** @type {CodeMirror} */
|
||||
let css_editor;
|
||||
|
||||
/** @type {'result' | 'js' | 'css' | 'ast'} */
|
||||
let view = 'result';
|
||||
let markdown = '';
|
||||
|
||||
$: ast = compiled?.result?.ast;
|
||||
</script>
|
||||
|
||||
<div class="view-toggle">
|
||||
{#if selected?.type === 'md'}
|
||||
<button class="active">Markdown</button>
|
||||
{:else}
|
||||
<button class:active={view === 'result'} on:click={() => (view = 'result')}>Result</button>
|
||||
<button class:active={view === 'js'} on:click={() => (view = 'js')}>JS output</button>
|
||||
<button class:active={view === 'css'} on:click={() => (view = 'css')}>CSS output</button>
|
||||
{#if showAst}
|
||||
<button class:active={view === 'ast'} on:click={() => (view = 'ast')}>AST output</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- component viewer -->
|
||||
<div class="tab-content" class:visible={selected?.type !== 'md' && view === 'result'}>
|
||||
<Viewer
|
||||
bind:error={runtimeError}
|
||||
{status}
|
||||
{relaxed}
|
||||
{injectedJS}
|
||||
{injectedCSS}
|
||||
theme={previewTheme}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- js output -->
|
||||
<div class="tab-content" class:visible={selected?.type !== 'md' && view === 'js'}>
|
||||
{#if embedded}
|
||||
<CodeMirror bind:this={js_editor} readonly />
|
||||
{:else}
|
||||
<PaneWithPanel pos="50%" panel="Compiler options">
|
||||
<div slot="main">
|
||||
<CodeMirror bind:this={js_editor} readonly />
|
||||
</div>
|
||||
|
||||
<div slot="panel-body">
|
||||
<CompilerOptions />
|
||||
</div>
|
||||
</PaneWithPanel>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- css output -->
|
||||
<div class="tab-content" class:visible={selected?.type !== 'md' && view === 'css'}>
|
||||
<CodeMirror bind:this={css_editor} readonly />
|
||||
</div>
|
||||
|
||||
<!-- ast output -->
|
||||
{#if showAst && ast}
|
||||
<div class="tab-content" class:visible={selected?.type !== 'md' && view === 'ast'}>
|
||||
<!-- ast view interacts with the module editor, wait for it first -->
|
||||
{#if $module_editor}
|
||||
<AstView {ast} autoscroll={selected?.type !== 'md' && view === 'ast'} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- markdown output -->
|
||||
<div class="tab-content" class:visible={selected?.type === 'md'}>
|
||||
<iframe title="Markdown" srcdoc={markdown}></iframe>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.view-toggle {
|
||||
height: 4.2rem;
|
||||
border-bottom: 1px solid var(--sk-text-4);
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
/* width: 50%;
|
||||
height: 100%; */
|
||||
background: var(--sk-back-1, white);
|
||||
text-align: left;
|
||||
position: relative;
|
||||
font: 400 12px/1.5 var(--sk-font);
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 12px 12px 8px 12px;
|
||||
color: var(--sk-text-2, #999);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button.active {
|
||||
border-bottom: 3px solid var(--sk-theme-1, --prime);
|
||||
color: var(--sk-text-1, #333);
|
||||
}
|
||||
|
||||
div[slot] {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: calc(100% - 42px) !important;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tab-content.visible {
|
||||
visibility: visible;
|
||||
pointer-events: all;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
@ -1,83 +0,0 @@
|
||||
<script>
|
||||
import { spring } from 'svelte/motion';
|
||||
import { SplitPane } from '@rich_harris/svelte-split-pane';
|
||||
|
||||
const UNIT_REGEX = /(\d+)(?:(px|rem|%|em))/i;
|
||||
|
||||
/** @type {string} */
|
||||
export let panel;
|
||||
|
||||
/** @type {Exclude<import('@rich_harris/svelte-split-pane/dist/SplitPane.svelte').SplitPaneProps['max'], undefined>} */
|
||||
export let pos = '90%';
|
||||
|
||||
$: previous_pos = Math.min(+pos.replace(UNIT_REGEX, '$1'), 70);
|
||||
|
||||
/** @type {Exclude<import('@rich_harris/svelte-split-pane/dist/SplitPane.svelte').SplitPaneProps['max'], undefined>} */
|
||||
let max = '90%';
|
||||
|
||||
// we can't bind to the spring itself, but we
|
||||
// can still use the spring to drive `pos`
|
||||
const driver = spring(+pos.replace(UNIT_REGEX, '$1'), {
|
||||
stiffness: 0.2,
|
||||
damping: 0.5
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
$: pos = $driver + '%';
|
||||
|
||||
const toggle = () => {
|
||||
const numeric_pos = +pos.replace(UNIT_REGEX, '$1');
|
||||
|
||||
driver.set(numeric_pos, { hard: true });
|
||||
|
||||
if (numeric_pos > 80) {
|
||||
driver.set(previous_pos);
|
||||
} else {
|
||||
previous_pos = numeric_pos;
|
||||
driver.set(+max.replace(UNIT_REGEX, '$1'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<SplitPane {max} min="10%" type="vertical" bind:pos priority="max">
|
||||
<section slot="a">
|
||||
<slot name="main" />
|
||||
</section>
|
||||
|
||||
<section slot="b">
|
||||
<div class="panel-header">
|
||||
<button class="panel-heading" on:click={toggle}>{panel}</button>
|
||||
<slot name="panel-header" />
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<slot name="panel-body" />
|
||||
</div>
|
||||
</section>
|
||||
</SplitPane>
|
||||
|
||||
<style>
|
||||
.panel-header {
|
||||
height: 42px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
font: 700 12px/1.5 var(--sk-font);
|
||||
color: var(--sk-text-1, #333);
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
section {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@ -1,96 +0,0 @@
|
||||
let uid = 1;
|
||||
|
||||
export default class ReplProxy {
|
||||
/** @type {HTMLIFrameElement} */
|
||||
iframe;
|
||||
|
||||
/** @type {import("./proxy").Handlers} */
|
||||
handlers;
|
||||
|
||||
/** @type {Map<number, { resolve: (value: any) => void, reject: (value: any) => void }>} */
|
||||
pending_cmds = new Map();
|
||||
|
||||
/** @param {MessageEvent<any>} event */
|
||||
handle_event = (event) => {
|
||||
if (event.source !== this.iframe.contentWindow) return;
|
||||
|
||||
const { action, args } = event.data;
|
||||
|
||||
switch (action) {
|
||||
case 'cmd_error':
|
||||
case 'cmd_ok':
|
||||
return this.handle_command_message(event.data);
|
||||
case 'fetch_progress':
|
||||
return this.handlers.on_fetch_progress(args.remaining);
|
||||
case 'error':
|
||||
return this.handlers.on_error(event.data);
|
||||
case 'unhandledrejection':
|
||||
return this.handlers.on_unhandled_rejection(event.data);
|
||||
case 'console':
|
||||
return this.handlers.on_console(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLIFrameElement} iframe
|
||||
* @param {import("./proxy").Handlers} handlers
|
||||
*/
|
||||
constructor(iframe, handlers) {
|
||||
this.iframe = iframe;
|
||||
this.handlers = handlers;
|
||||
|
||||
window.addEventListener('message', this.handle_event, false);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
window.removeEventListener('message', this.handle_event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} action
|
||||
* @param {any} args
|
||||
*/
|
||||
iframe_command(action, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cmd_id = uid++;
|
||||
|
||||
this.pending_cmds.set(cmd_id, { resolve, reject });
|
||||
|
||||
this.iframe.contentWindow?.postMessage({ action, cmd_id, args }, '*');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ action: string; cmd_id: number; message: string; stack: any; args: any; }} cmd_data
|
||||
*/
|
||||
handle_command_message(cmd_data) {
|
||||
let action = cmd_data.action;
|
||||
let id = cmd_data.cmd_id;
|
||||
let handler = this.pending_cmds.get(id);
|
||||
|
||||
if (handler) {
|
||||
this.pending_cmds.delete(id);
|
||||
if (action === 'cmd_error') {
|
||||
let { message, stack } = cmd_data;
|
||||
let e = new Error(message);
|
||||
e.stack = stack;
|
||||
handler.reject(e);
|
||||
}
|
||||
|
||||
if (action === 'cmd_ok') {
|
||||
handler.resolve(cmd_data.args);
|
||||
}
|
||||
} else {
|
||||
console.error('command not found', id, cmd_data, [...this.pending_cmds.keys()]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} script */
|
||||
eval(script) {
|
||||
return this.iframe_command('eval', { script });
|
||||
}
|
||||
|
||||
handle_links() {
|
||||
return this.iframe_command('catch_clicks', {});
|
||||
}
|
||||
}
|
@ -1,337 +0,0 @@
|
||||
<script>
|
||||
import { get_repl_context } from '$lib/context.js';
|
||||
import { BROWSER } from 'esm-env';
|
||||
import { onMount } from 'svelte';
|
||||
import Message from '../Message.svelte';
|
||||
import PaneWithPanel from './PaneWithPanel.svelte';
|
||||
import ReplProxy from './ReplProxy.js';
|
||||
import Console from './console/Console.svelte';
|
||||
import getLocationFromStack from './get-location-from-stack';
|
||||
import srcdoc from './srcdoc/index.html?raw';
|
||||
import ErrorOverlay from './ErrorOverlay.svelte';
|
||||
|
||||
/** @type {import('$lib/types').MessageDetails | null} */
|
||||
export let error;
|
||||
/** @type {string | null} */
|
||||
export let status;
|
||||
export let relaxed = false;
|
||||
export let injectedJS = '';
|
||||
export let injectedCSS = '';
|
||||
|
||||
/** @type {'light' | 'dark'} */
|
||||
export let theme;
|
||||
|
||||
const { bundle } = get_repl_context();
|
||||
|
||||
/** @type {import('./console/console').Log[]} */
|
||||
let logs = [];
|
||||
|
||||
/** @type {import('./console/console').Log[][]} */
|
||||
let log_group_stack = [];
|
||||
|
||||
let current_log_group = logs;
|
||||
|
||||
/** @type {HTMLIFrameElement} */
|
||||
let iframe;
|
||||
let pending_imports = 0;
|
||||
let pending = false;
|
||||
|
||||
/** @type {ReplProxy | null} */
|
||||
let proxy = null;
|
||||
|
||||
let ready = false;
|
||||
let inited = false;
|
||||
|
||||
let log_height = 90;
|
||||
/** @type {number} */
|
||||
let prev_height;
|
||||
|
||||
/** @type {import('./console/console').Log} */
|
||||
let last_console_event;
|
||||
|
||||
onMount(() => {
|
||||
proxy = new ReplProxy(iframe, {
|
||||
on_fetch_progress: (progress) => {
|
||||
pending_imports = progress;
|
||||
},
|
||||
on_error: (event) => {
|
||||
push_logs({ command: 'error', args: [event.value] });
|
||||
},
|
||||
on_unhandled_rejection: (event) => {
|
||||
let error = event.value;
|
||||
if (typeof error === 'string') error = { message: error };
|
||||
error.message = 'Uncaught (in promise): ' + error.message;
|
||||
push_logs({ command: 'error', args: [error] });
|
||||
},
|
||||
on_console: (log) => {
|
||||
switch (log.command) {
|
||||
case 'clear':
|
||||
clear_logs();
|
||||
push_logs(log);
|
||||
break;
|
||||
|
||||
case 'group':
|
||||
group_logs(log);
|
||||
break;
|
||||
|
||||
case 'groupEnd':
|
||||
ungroup_logs();
|
||||
break;
|
||||
|
||||
case 'duplicate':
|
||||
increment_duplicate_log();
|
||||
break;
|
||||
|
||||
default:
|
||||
push_logs(log);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
iframe.addEventListener('load', () => {
|
||||
proxy?.handle_links();
|
||||
ready = true;
|
||||
});
|
||||
|
||||
return () => {
|
||||
proxy?.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
$: if (ready) proxy?.iframe_command('set_theme', { theme });
|
||||
|
||||
/**
|
||||
* @param {import('$lib/types').Bundle | null} $bundle
|
||||
*/
|
||||
async function apply_bundle($bundle) {
|
||||
if (!$bundle) return;
|
||||
|
||||
try {
|
||||
clear_logs();
|
||||
|
||||
if (!$bundle.error) {
|
||||
await proxy?.eval(`
|
||||
${injectedJS}
|
||||
|
||||
${styles}
|
||||
|
||||
{
|
||||
const styles = document.querySelectorAll('style[id^=svelte-]');
|
||||
|
||||
let i = styles.length;
|
||||
while (i--) styles[i].parentNode.removeChild(styles[i]);
|
||||
|
||||
if (window.__unmount_previous) {
|
||||
try {
|
||||
window.__unmount_previous();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
document.body.innerHTML = '';
|
||||
window._svelteTransitionManager = null;
|
||||
}
|
||||
|
||||
const __repl_exports = ${$bundle.client?.code};
|
||||
{
|
||||
const { mount, unmount, App, untrack } = __repl_exports;
|
||||
|
||||
const console_methods = ['log', 'error', 'trace', 'assert', 'warn', 'table', 'group'];
|
||||
|
||||
// The REPL hooks up to the console to provide a virtual console. However, the implementation
|
||||
// needs to stringify the console to pass over a MessageChannel, which means that the object
|
||||
// can get deeply read and tracked by accident when using the console. We can avoid this by
|
||||
// ensuring we untrack the main console methods.
|
||||
|
||||
const original = {};
|
||||
|
||||
for (const method of console_methods) {
|
||||
original[method] = console[method];
|
||||
console[method] = function (...v) {
|
||||
return untrack(() => original[method].apply(this, v));
|
||||
}
|
||||
}
|
||||
const component = mount(App, { target: document.body });
|
||||
window.__unmount_previous = () => {
|
||||
for (const method of console_methods) {
|
||||
console[method] = original[method];
|
||||
}
|
||||
unmount(component);
|
||||
}
|
||||
}
|
||||
//# sourceURL=playground:output
|
||||
`);
|
||||
error = null;
|
||||
}
|
||||
} catch (e) {
|
||||
// @ts-ignore
|
||||
show_error(e);
|
||||
}
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
$: if (ready) apply_bundle($bundle);
|
||||
|
||||
$: styles =
|
||||
injectedCSS &&
|
||||
`{
|
||||
const style = document.createElement('style');
|
||||
style.textContent = ${JSON.stringify(injectedCSS)};
|
||||
document.head.appendChild(style);
|
||||
}`;
|
||||
|
||||
/**
|
||||
* @param {import('$lib/types').Error & { loc: { line: number; column: number } }} e
|
||||
*/
|
||||
function show_error(e) {
|
||||
const map = $bundle?.client?.map;
|
||||
|
||||
// @ts-ignore INVESTIGATE
|
||||
const loc = map && getLocationFromStack(e.stack, map);
|
||||
if (loc) {
|
||||
e.filename = loc.source;
|
||||
e.loc = { line: loc.line, column: loc.column ?? 0 };
|
||||
}
|
||||
|
||||
error = e;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./console/console').Log} log
|
||||
*/
|
||||
function push_logs(log) {
|
||||
current_log_group.push((last_console_event = log));
|
||||
logs = logs;
|
||||
}
|
||||
|
||||
/** @param {import('./console/console').Log} log */
|
||||
function group_logs(log) {
|
||||
log.logs = [];
|
||||
current_log_group.push(log);
|
||||
// TODO: Investigate
|
||||
log_group_stack.push(current_log_group);
|
||||
current_log_group = log.logs;
|
||||
logs = logs;
|
||||
}
|
||||
|
||||
function ungroup_logs() {
|
||||
const last = log_group_stack.pop();
|
||||
|
||||
if (last) current_log_group = last;
|
||||
}
|
||||
|
||||
function increment_duplicate_log() {
|
||||
const last_log = current_log_group[current_log_group.length - 1];
|
||||
|
||||
if (last_log) {
|
||||
last_log.count = (last_log.count || 1) + 1;
|
||||
logs = logs;
|
||||
} else {
|
||||
last_console_event.count = 1;
|
||||
push_logs(last_console_event);
|
||||
}
|
||||
}
|
||||
|
||||
function on_toggle_console() {
|
||||
if (log_height < 90) {
|
||||
prev_height = log_height;
|
||||
log_height = 90;
|
||||
} else {
|
||||
log_height = prev_height || 45;
|
||||
}
|
||||
}
|
||||
|
||||
function clear_logs() {
|
||||
current_log_group = logs = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="iframe-container">
|
||||
<PaneWithPanel pos="90%" panel="Console">
|
||||
<div slot="main">
|
||||
<iframe
|
||||
title="Result"
|
||||
class:inited
|
||||
bind:this={iframe}
|
||||
sandbox={[
|
||||
'allow-popups-to-escape-sandbox',
|
||||
'allow-scripts',
|
||||
'allow-popups',
|
||||
'allow-forms',
|
||||
'allow-pointer-lock',
|
||||
'allow-top-navigation',
|
||||
'allow-modals',
|
||||
relaxed ? 'allow-same-origin' : ''
|
||||
].join(' ')}
|
||||
class={error || pending || pending_imports ? 'greyed-out' : ''}
|
||||
srcdoc={BROWSER ? srcdoc : ''}
|
||||
></iframe>
|
||||
|
||||
{#if $bundle?.error}
|
||||
<ErrorOverlay error={$bundle.error} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div slot="panel-header">
|
||||
<button on:click|stopPropagation={clear_logs}>
|
||||
{#if logs.length > 0}
|
||||
({logs.length})
|
||||
{/if}
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section slot="panel-body">
|
||||
<Console {logs} />
|
||||
</section>
|
||||
</PaneWithPanel>
|
||||
|
||||
<div class="overlay">
|
||||
{#if error}
|
||||
<Message kind="error" details={error} />
|
||||
{:else if status || !$bundle}
|
||||
<Message kind="info" truncate>{status || 'loading Svelte compiler...'}</Message>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.iframe-container {
|
||||
position: absolute;
|
||||
background-color: var(--sk-back-1, white);
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.greyed-out {
|
||||
filter: grayscale(50%) blur(1px);
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
button {
|
||||
color: var(--sk-text-2, #999);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: var(--sk-text-1, #333);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -1,46 +0,0 @@
|
||||
<script>
|
||||
import ConsoleLine from './ConsoleLine.svelte';
|
||||
|
||||
/** @type {import('./console').Log[]} */
|
||||
export let logs;
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each logs as log}
|
||||
<ConsoleLine {log} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
--error-fg: #da106e;
|
||||
--error-bg: #fff0f0;
|
||||
--error-border: rgb(242, 214, 219);
|
||||
--warning-bg: rgb(254, 251, 218);
|
||||
--warning-border: rgb(242, 232, 163);
|
||||
--json-tree-string-color: var(--sk-code-string);
|
||||
--json-tree-font-family: var(--sk-font-mono);
|
||||
|
||||
:global(.dark) & {
|
||||
--error-fg: rgb(235, 78, 109);
|
||||
--error-bg: rgb(71, 48, 54);
|
||||
--error-border: rgb(109, 65, 76);
|
||||
--warning-bg: rgb(64, 56, 34);
|
||||
--warning-border: rgb(86, 86, 51);
|
||||
|
||||
--json-tree-property-color: #72a2d3;
|
||||
--json-tree-string-color: #6cd1c7;
|
||||
--json-tree-symbol-color: #6cd1c7;
|
||||
--json-tree-boolean-color: #9681f7;
|
||||
--json-tree-function-color: #e59b6f;
|
||||
--json-tree-number-color: #9681f7;
|
||||
--json-tree-label-color: #9ca0a5;
|
||||
--json-tree-arrow-color: #e8eaed;
|
||||
--json-tree-null-color: #81868a;
|
||||
--json-tree-undefined-color: #81868a;
|
||||
--json-tree-date-color: #9ca0a5;
|
||||
--json-tree-operator-color: #e8eaed;
|
||||
--json-tree-regex-color: #6cd1c7;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,292 +0,0 @@
|
||||
<script>
|
||||
import JSONNode from 'svelte-json-tree';
|
||||
import ConsoleTable from './ConsoleTable.svelte';
|
||||
|
||||
/** @type {import('./console').Log} */
|
||||
export let log;
|
||||
export let depth = 0;
|
||||
|
||||
function toggle_group_collapse() {
|
||||
log.collapsed = !log.collapsed;
|
||||
}
|
||||
|
||||
let style;
|
||||
|
||||
/** @param {string} text */
|
||||
function sanitize_css(text) {
|
||||
style ??= document.createElement('span').style;
|
||||
style.cssText = text;
|
||||
|
||||
for (const key in style) {
|
||||
const value = style[key];
|
||||
if (typeof value === 'string' && value.includes('url(')) {
|
||||
style[key] = value.replace(/url\([^)]+\)/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
style.position = 'static';
|
||||
return style.cssText;
|
||||
}
|
||||
|
||||
/** @param {any[]} [args] */
|
||||
function format_args(args = []) {
|
||||
if (args.length === 0) return args;
|
||||
|
||||
if (typeof args[0] !== 'string') {
|
||||
return args.map((value) => ({ type: 'value', value }));
|
||||
}
|
||||
|
||||
args = args.slice();
|
||||
|
||||
const parts = args.shift().split(/(%[sdifoOc])/g);
|
||||
|
||||
const formatted = [];
|
||||
|
||||
if (parts[0] !== '') {
|
||||
formatted.push({ type: 'value', value: parts[0] });
|
||||
}
|
||||
|
||||
for (let i = 1; i < parts.length; i += 2) {
|
||||
const type = parts[i];
|
||||
const next = parts[i + 1];
|
||||
const value = args.shift();
|
||||
|
||||
switch (type) {
|
||||
case '%s':
|
||||
formatted.push({ type: 'value', value: String(value), formatted: true });
|
||||
break;
|
||||
|
||||
case '%d':
|
||||
case '%i':
|
||||
formatted.push({
|
||||
type: 'value',
|
||||
value: typeof value === 'symbol' ? NaN : parseInt(value, 10),
|
||||
formatted: true
|
||||
});
|
||||
break;
|
||||
|
||||
case '%f':
|
||||
formatted.push({
|
||||
type: 'value',
|
||||
value: typeof value === 'symbol' ? NaN : parseFloat(value),
|
||||
formatted: true
|
||||
});
|
||||
break;
|
||||
|
||||
case '%o':
|
||||
case '%O':
|
||||
formatted.push({ type: 'value', value, formatted: true });
|
||||
break;
|
||||
|
||||
case '%c':
|
||||
formatted.push({
|
||||
type: 'style',
|
||||
style: sanitize_css(String(value)),
|
||||
value: next,
|
||||
formatted: true
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (type !== '%c' && next !== '') {
|
||||
formatted.push({ type: 'value', value: next, formatted: true });
|
||||
}
|
||||
}
|
||||
|
||||
for (const value of args) {
|
||||
formatted.push({ type: 'value', value });
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if log.command === 'table'}
|
||||
<ConsoleTable data={log.data} columns={log.columns} />
|
||||
{/if}
|
||||
|
||||
<div class="{log.command} line" style="--indent: {depth * 15}px">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={toggle_group_collapse}
|
||||
class="log"
|
||||
class:expandable={log.stack || log.command === 'group'}
|
||||
>
|
||||
{#if log.count && log.count > 1}
|
||||
<span class="count">{log.count}</span>
|
||||
{/if}
|
||||
|
||||
{#if log.stack || log.command === 'group'}
|
||||
<span class="arrow" class:expand={!log.collapsed}>{'\u25B6'}</span>
|
||||
{/if}
|
||||
|
||||
{#if log.command === 'clear'}
|
||||
<span class="meta">Console was cleared</span>
|
||||
{:else if log.command === 'unclonable'}
|
||||
<span class="meta meta-error">Message could not be cloned. Open devtools to see it</span>
|
||||
{:else if log.command === 'table'}
|
||||
<JSONNode value={log.data} />
|
||||
{:else}
|
||||
<span class="values">
|
||||
{#each format_args(log.args) as part}
|
||||
<!-- we need to do some funky stuff to make whitespace behave as it does in devtools -->
|
||||
{#if !part.formatted}
|
||||
{' '}
|
||||
{/if}{#if part.type === 'value'}
|
||||
<JSONNode value={part.value} defaultExpandedLevel={log.expanded ? 1 : 0} />
|
||||
{:else}
|
||||
<span class="styled" style={part.style}>{part.value}</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if log.stack && !log.collapsed}
|
||||
<div class="stack">
|
||||
{#each log.stack as line}
|
||||
<span>{line.label}</span>
|
||||
<span class="location">{line.location}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#each new Array(depth) as _, idx}
|
||||
<div class="outline" style="left: {idx * 15 + 15}px"></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if log.command === 'group' && !log.collapsed}
|
||||
{#each log.logs ?? [] as childLog}
|
||||
<svelte:self log={childLog} depth={depth + 1} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.line {
|
||||
--bg: var(--sk-back-1);
|
||||
--border: var(--sk-back-3);
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-width: 1px;
|
||||
border-style: solid none none none;
|
||||
border-color: var(--border);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.warn {
|
||||
--bg: var(--warning-bg);
|
||||
--border: var(--warning-border);
|
||||
}
|
||||
|
||||
.error {
|
||||
--bg: var(--error-bg);
|
||||
--border: var(--error-border);
|
||||
}
|
||||
|
||||
.warn,
|
||||
.error {
|
||||
border-style: solid none;
|
||||
|
||||
& + :global(&) {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.group {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.log {
|
||||
padding: 0.5rem 1rem 0.5rem calc(1rem + var(--indent));
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
font-size: 1.2rem;
|
||||
font-family: var(--sk-font-mono);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.log.expandable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.values {
|
||||
display: block;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, auto) minmax(auto, 1fr);
|
||||
grid-gap: 0 2rem;
|
||||
font-size: 1.2rem;
|
||||
font-family: var(--sk-font-mono);
|
||||
margin: 0 1rem 0.4rem calc(1em + var(--indent));
|
||||
overflow: hidden;
|
||||
|
||||
.location {
|
||||
position: relative;
|
||||
background: var(--bg);
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 1rem;
|
||||
height: 100%;
|
||||
left: -1rem;
|
||||
top: 0;
|
||||
background: linear-gradient(to right, transparent, var(--bg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 1.5em;
|
||||
height: 1.4em;
|
||||
padding: 0.5em;
|
||||
border-radius: 0.4rem;
|
||||
background-color: var(--sk-text-3, #777);
|
||||
color: var(--sk-back-1);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--sk-text-2, #666);
|
||||
font-family: var(--sk-font) !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.meta-error {
|
||||
color: var(--error-fg);
|
||||
}
|
||||
|
||||
.outline {
|
||||
border-left: 1px solid #9c9cab;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 0.9rem;
|
||||
transition: 150ms;
|
||||
transform-origin: 50% 50%;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.arrow.expand {
|
||||
transform: translateY(0px) rotateZ(90deg);
|
||||
}
|
||||
|
||||
.styled {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
@ -1,145 +0,0 @@
|
||||
<script>
|
||||
import JSONNode from 'svelte-json-tree';
|
||||
|
||||
/** @type {any} */
|
||||
export let data;
|
||||
|
||||
/** @type {any} */
|
||||
export let columns;
|
||||
|
||||
$: table = create_table(data, columns);
|
||||
|
||||
/**
|
||||
* @param {any} data
|
||||
* @param {string[]} [custom_columns]
|
||||
*/
|
||||
function create_table(data, custom_columns) {
|
||||
let has_non_object = false;
|
||||
const columns = new Set();
|
||||
|
||||
if (custom_columns) {
|
||||
custom_columns.forEach((column) => columns.add(column));
|
||||
} else {
|
||||
for (const key in data) {
|
||||
const value = data[key];
|
||||
if (typeof value === 'object') {
|
||||
Object.keys(value).forEach((key) => columns.add(key));
|
||||
} else {
|
||||
has_non_object = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const is_array = Array.isArray(data);
|
||||
|
||||
const rows = Object.keys(data).map((key) => {
|
||||
const value = data[key];
|
||||
const values = [];
|
||||
|
||||
for (const column of columns) {
|
||||
values.push(typeof value === 'object' && column in value ? value[column] : '');
|
||||
}
|
||||
|
||||
if (has_non_object) {
|
||||
values.push(typeof value !== 'object' ? value : '');
|
||||
}
|
||||
|
||||
return {
|
||||
key: is_array ? parseInt(key) : key,
|
||||
values
|
||||
};
|
||||
});
|
||||
|
||||
if (has_non_object) {
|
||||
columns.add('Value');
|
||||
}
|
||||
|
||||
return { columns, rows };
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>(index)</th>
|
||||
|
||||
{#each table.columns as column}
|
||||
<th>{column}</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{#each table.rows as row}
|
||||
<tr>
|
||||
<td>
|
||||
{#if typeof row.key === 'string'}
|
||||
{row.key}
|
||||
{:else}
|
||||
<JSONNode value={row.key} />
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
{#each row.values as value}
|
||||
<td>
|
||||
{#if typeof value === 'string'}
|
||||
{value}
|
||||
{:else}
|
||||
<JSONNode {value} />
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.table {
|
||||
--json-tree-font-size: 1.2rem;
|
||||
--json-tree-font-family: var(--sk-font-mono);
|
||||
margin: 8px;
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
border: 1px solid var(--sk-back-4);
|
||||
border-radius: 2px;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--sk-back-3);
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
position: sticky;
|
||||
font-weight: normal;
|
||||
font-size: 1.4rem;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
font-family: var(--sk-font-mono);
|
||||
font-size: 1.2rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr {
|
||||
background: var(--sk-back-1);
|
||||
}
|
||||
|
||||
tr:nth-child(2n) {
|
||||
background: var(--sk-back-3);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border-right: 1px solid var(--sk-back-3);
|
||||
}
|
||||
</style>
|
@ -1,15 +0,0 @@
|
||||
export type Log = {
|
||||
command: 'info' | 'warn' | 'error' | 'table' | 'group' | 'clear' | 'unclonable';
|
||||
action?: 'console';
|
||||
args?: any[];
|
||||
collapsed?: boolean;
|
||||
expanded?: boolean;
|
||||
count?: number;
|
||||
logs?: Log[];
|
||||
stack?: Array<{
|
||||
label?: string;
|
||||
location?: string;
|
||||
}>;
|
||||
data?: any;
|
||||
columns?: string[];
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
import { decode } from '@jridgewell/sourcemap-codec';
|
||||
|
||||
/**
|
||||
* @param {string} stack
|
||||
* @param {import('@jridgewell/sourcemap-codec').SourceMapMappings} map
|
||||
* @returns
|
||||
*/
|
||||
export default function getLocationFromStack(stack, map) {
|
||||
if (!stack) return;
|
||||
const last = stack.split('\n')[1];
|
||||
const match = /<anonymous>:(\d+):(\d+)\)$/.exec(last);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
const line = +match[1];
|
||||
const column = +match[2];
|
||||
|
||||
return trace({ line, column }, map);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Omit<import('$lib/types').StartOrEnd, 'character'>} loc
|
||||
* @param {*} map
|
||||
* @returns
|
||||
*/
|
||||
function trace(loc, map) {
|
||||
const mappings = decode(map.mappings);
|
||||
const segments = mappings[loc.line - 1];
|
||||
|
||||
for (let i = 0; i < segments.length; i += 1) {
|
||||
const segment = segments[i];
|
||||
if (segment[0] === loc.column) {
|
||||
const [, sourceIndex, line, column] = segment;
|
||||
const source = map.sources[sourceIndex ?? 0].slice(2);
|
||||
|
||||
return { source, line: (line ?? 0) + 1, column };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export type Handlers = Record<
|
||||
'on_fetch_progress' | 'on_error' | 'on_unhandled_rejection' | 'on_console',
|
||||
(data: any) => void
|
||||
>;
|
@ -1,287 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
--bg-1: hsl(0, 0%, 100%);
|
||||
--bg-2: hsl(206, 20%, 90%);
|
||||
--bg-3: hsl(206, 20%, 80%);
|
||||
--fg-1: hsl(0, 0%, 13%);
|
||||
--fg-2: hsl(0, 0%, 20%);
|
||||
--fg-2: hsl(0, 0%, 30%);
|
||||
--link: hsl(208, 77%, 47%);
|
||||
--link-hover: hsl(208, 77%, 55%);
|
||||
--link-active: hsl(208, 77%, 40%);
|
||||
--border-radius: 4px;
|
||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||
'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
--font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas,
|
||||
'DejaVu Sans Mono', monospace;
|
||||
color-scheme: light;
|
||||
background: var(--bg-1);
|
||||
color: var(--fg-1);
|
||||
font-family: var(--font);
|
||||
line-height: 1.5;
|
||||
margin: 1rem;
|
||||
height: calc(100vh - 2rem);
|
||||
accent-color: var(--hover) !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link-hover);
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: var(--link-active);
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--bg-2);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.9em;
|
||||
padding: 0.15rem 0.3rem;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
ul.todos {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
color-scheme: dark;
|
||||
--bg-1: hsl(0, 0%, 18%);
|
||||
--bg-2: hsl(0, 0%, 30%);
|
||||
--bg-3: hsl(0, 0%, 40%);
|
||||
--fg-1: hsl(0, 0%, 90%);
|
||||
--fg-2: hsl(0, 0%, 70%);
|
||||
--fg-3: hsl(0, 0%, 60%);
|
||||
--link: hsl(206, 96%, 72%);
|
||||
--link-hover: hsl(206, 96%, 78%);
|
||||
--link-active: hsl(206, 96%, 64%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
function send(payload, origin = '*') {
|
||||
parent.postMessage(payload, origin);
|
||||
}
|
||||
|
||||
window.addEventListener('message', ({ origin, data }) => {
|
||||
let { action, cmd_id } = data;
|
||||
|
||||
const reply = (payload) => send({ ...payload, cmd_id }, origin);
|
||||
|
||||
try {
|
||||
if (action === 'set_theme') {
|
||||
document.body.classList.toggle('dark', data.args.theme === 'dark');
|
||||
}
|
||||
|
||||
if (action === 'eval') {
|
||||
(0, eval)(data.args.script);
|
||||
}
|
||||
|
||||
if (action === 'catch_clicks') {
|
||||
// attached to document to not interfere with svelte's event delegators that are attached to the app root (document.body)
|
||||
document.addEventListener('click', (event) => {
|
||||
if (event.which !== 1) return;
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
// ensure target is a link
|
||||
let el = event.target;
|
||||
while (el && el.nodeName !== 'A') el = el.parentNode;
|
||||
if (!el || el.nodeName !== 'A') return;
|
||||
|
||||
if (
|
||||
el.hasAttribute('download') ||
|
||||
el.getAttribute('rel') === 'external' ||
|
||||
el.target
|
||||
)
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (el.href.startsWith(origin)) {
|
||||
const url = new URL(el.href);
|
||||
if (url.hash[0] === '#') {
|
||||
window.location.hash = url.hash;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
window.open(el.href, '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
reply({ action: 'cmd_ok' });
|
||||
} catch ({ message, stack }) {
|
||||
reply({ action: 'cmd_error', message, stack });
|
||||
}
|
||||
});
|
||||
|
||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
send({ action: 'error', value: error });
|
||||
};
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
send({ action: 'unhandledrejection', value: event.reason });
|
||||
});
|
||||
|
||||
// Intercept console methods
|
||||
const timers = new Map();
|
||||
const counters = new Map();
|
||||
|
||||
function log(command, opts) {
|
||||
try {
|
||||
send({ action: 'console', command, ...opts });
|
||||
} catch {
|
||||
send({ action: 'console', command: 'unclonable' });
|
||||
}
|
||||
}
|
||||
|
||||
function stringify(args) {
|
||||
try {
|
||||
return JSON.stringify(args, (key, value) => {
|
||||
// if we don't do this, our Set/Map from svelte/reactivity would show up wrong in the console
|
||||
if (value instanceof Map) {
|
||||
return { type: 'Map', value };
|
||||
}
|
||||
|
||||
if (value instanceof Set) {
|
||||
return { type: 'Set', value };
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} method */
|
||||
function stack(method) {
|
||||
return new Error().stack
|
||||
.split('\n')
|
||||
.filter((line) => {
|
||||
if (/[(@]about:srcdoc/.test(line)) return false;
|
||||
return true;
|
||||
})
|
||||
.slice(1)
|
||||
.map((line) => {
|
||||
line = line
|
||||
.replace('console[method]', `console.${method}`)
|
||||
.replace(/console\.<computed> \[as \w+\]/, `console.${method}`);
|
||||
|
||||
let match =
|
||||
/^\s+at (.+) \((.+:\d+:\d+)\)/.exec(line) || /^(.+)@(.+:\d+:\d+)?/.exec(line);
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
label: match[1],
|
||||
location: match[2]
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((x) => x);
|
||||
}
|
||||
|
||||
const can_dedupe = ['log', 'info', 'dir', 'warn', 'error', 'assert', 'trace'];
|
||||
|
||||
const methods = {
|
||||
clear: () => log('clear'),
|
||||
// TODO make the command 'push' and the level/type 'info'
|
||||
log: (...args) => log('info', { args }),
|
||||
info: (...args) => log('info', { args }),
|
||||
dir: (...args) => log('info', { args: [args[0]], expanded: true }),
|
||||
warn: (...args) => log('warn', { args, stack: stack('warn'), collapsed: true }),
|
||||
error: (...args) => log('error', { args, stack: stack('error'), collapsed: true }),
|
||||
assert: (condition, ...args) => {
|
||||
if (condition) return;
|
||||
log('error', {
|
||||
args: ['Assertion failed:', ...args],
|
||||
stack: stack('assert'),
|
||||
collapsed: true
|
||||
});
|
||||
},
|
||||
group: (...args) => log('group', { args, collapsed: false }),
|
||||
groupCollapsed: (...args) => log('group', { args, collapsed: true }),
|
||||
groupEnd: () => log('groupEnd'),
|
||||
table: (...args) => {
|
||||
const data = args[0];
|
||||
if (data && typeof data === 'object') {
|
||||
log('table', { data, columns: args[1] });
|
||||
} else {
|
||||
log('info', { args });
|
||||
}
|
||||
},
|
||||
time: (label = 'default') => timers.set(label, performance.now()),
|
||||
timeLog: (label = 'default') => {
|
||||
const now = performance.now();
|
||||
if (timers.has(label)) {
|
||||
log('info', { args: [`${label}: ${now - timers.get(label)}ms`] });
|
||||
} else {
|
||||
log('warn', { args: [`Timer '${label}' does not exist`] });
|
||||
}
|
||||
},
|
||||
timeEnd: (label = 'default') => {
|
||||
const now = performance.now();
|
||||
if (timers.has(label)) {
|
||||
log('info', { args: [`${label}: ${now - timers.get(label)}ms`] });
|
||||
} else {
|
||||
log('warn', { args: [`Timer '${label}' does not exist`] });
|
||||
}
|
||||
timers.delete(label);
|
||||
},
|
||||
count: (label = 'default') => {
|
||||
counters.set(label, (counters.get(label) || 0) + 1);
|
||||
log('info', { args: [`${label}: ${counters.get(label)}`] });
|
||||
},
|
||||
countReset: (label = 'default') => {
|
||||
if (counters.has(label)) {
|
||||
counters.set(label, 0);
|
||||
} else {
|
||||
log('warn', { args: [`Count for '${label}' does not exist`] });
|
||||
}
|
||||
},
|
||||
trace: (...args) => {
|
||||
log('info', {
|
||||
args: args.length === 0 ? ['console.trace'] : args,
|
||||
stack: stack('trace'),
|
||||
collapsed: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let previous = '';
|
||||
|
||||
for (const method in methods) {
|
||||
const original = console[method];
|
||||
|
||||
console[method] = (...args) => {
|
||||
const stack = new Error().stack;
|
||||
|
||||
if (
|
||||
previous === (previous = stringify({ method, args, stack })) &&
|
||||
can_dedupe.includes(method) &&
|
||||
args.every((arg) => !arg || typeof arg !== 'object')
|
||||
) {
|
||||
send({ action: 'console', command: 'duplicate' });
|
||||
} else {
|
||||
methods[method](...args);
|
||||
}
|
||||
|
||||
original(...args);
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
@ -1,438 +0,0 @@
|
||||
<script>
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { SplitPane } from '@rich_harris/svelte-split-pane';
|
||||
import { BROWSER } from 'esm-env';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import Bundler from './Bundler.js';
|
||||
import ComponentSelector from './Input/ComponentSelector.svelte';
|
||||
import ModuleEditor from './Input/ModuleEditor.svelte';
|
||||
import InputOutputToggle from './InputOutputToggle.svelte';
|
||||
import Output from './Output/Output.svelte';
|
||||
import { set_repl_context } from './context.js';
|
||||
import { get_full_filename } from './utils.js';
|
||||
import Compiler from './Output/Compiler.js';
|
||||
|
||||
export let packagesUrl = 'https://unpkg.com';
|
||||
export let svelteUrl = `${BROWSER ? location.origin : ''}/svelte`;
|
||||
export let embedded = false;
|
||||
/** @type {'columns' | 'rows'} */
|
||||
export let orientation = 'columns';
|
||||
export let relaxed = false;
|
||||
export let fixed = false;
|
||||
export let fixedPos = 50;
|
||||
export let injectedJS = '';
|
||||
export let injectedCSS = '';
|
||||
/** @type {'light' | 'dark'} */
|
||||
export let previewTheme = 'light';
|
||||
export let showModified = false;
|
||||
export let showAst = false;
|
||||
export let autocomplete = true;
|
||||
|
||||
let runes = false;
|
||||
|
||||
export function toJSON() {
|
||||
return {
|
||||
imports: $bundle?.imports ?? [],
|
||||
files: $files
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ files: import('./types').File[], css?: string }} data
|
||||
*/
|
||||
export async function set(data) {
|
||||
$files = data.files;
|
||||
$selected_name = 'App.svelte';
|
||||
|
||||
rebundle();
|
||||
|
||||
// Wait for editors to be ready
|
||||
await $module_editor?.isReady;
|
||||
|
||||
await $module_editor?.set({ code: data.files[0].source, lang: data.files[0].type });
|
||||
|
||||
injectedCSS = data.css || '';
|
||||
|
||||
// when we set new files we also populate the EDITOR_STATE_MAP
|
||||
// with a new state for each file containing the source as docs
|
||||
// this allows the editor to behave correctly when renaming a tab
|
||||
// after having loaded the files externally
|
||||
populate_editor_state();
|
||||
|
||||
dispatch('change', { files: $files });
|
||||
}
|
||||
|
||||
export function markSaved() {
|
||||
$files = $files.map((val) => ({ ...val, modified: false }));
|
||||
}
|
||||
|
||||
/** @type {ReturnType<typeof createEventDispatcher<{ change: { files: import('./types').File[] } }>>} */
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
/**
|
||||
* @typedef {import('./types').ReplContext} ReplContext
|
||||
*/
|
||||
|
||||
/** @type {import('svelte/compiler').CompileOptions} */
|
||||
const DEFAULT_COMPILE_OPTIONS = {
|
||||
generate: 'client',
|
||||
dev: false,
|
||||
hmr: false,
|
||||
css: 'external'
|
||||
};
|
||||
|
||||
/** @type {Map<string, import('@codemirror/state').EditorState>} */
|
||||
const EDITOR_STATE_MAP = new Map();
|
||||
|
||||
/** @type {ReplContext['files']} */
|
||||
const files = writable([]);
|
||||
|
||||
/** @type {ReplContext['selected_name']} */
|
||||
const selected_name = writable('App.svelte');
|
||||
|
||||
/** @type {ReplContext['selected']} */
|
||||
const selected = derived([files, selected_name], ([$files, $selected_name]) => {
|
||||
return (
|
||||
$files.find((val) => get_full_filename(val) === $selected_name) ?? {
|
||||
name: '',
|
||||
type: '',
|
||||
source: '',
|
||||
modified: false
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/** @type {ReplContext['bundle']} */
|
||||
const bundle = writable(null);
|
||||
|
||||
/** @type {ReplContext['compile_options']} */
|
||||
const compile_options = writable(DEFAULT_COMPILE_OPTIONS);
|
||||
|
||||
/** @type {ReplContext['cursor_pos']} */
|
||||
const cursor_pos = writable(0);
|
||||
|
||||
/** @type {ReplContext['module_editor']} */
|
||||
const module_editor = writable(null);
|
||||
|
||||
/** @type {ReplContext['toggleable']} */
|
||||
const toggleable = writable(false);
|
||||
|
||||
/** @type {ReplContext['bundler']} */
|
||||
const bundler = writable(null);
|
||||
|
||||
/** @type {ReplContext['bundling']} */
|
||||
const bundling = writable(new Promise(() => {}));
|
||||
|
||||
set_repl_context({
|
||||
files,
|
||||
selected_name,
|
||||
selected,
|
||||
bundle,
|
||||
bundler,
|
||||
bundling,
|
||||
compile_options,
|
||||
cursor_pos,
|
||||
module_editor,
|
||||
toggleable,
|
||||
|
||||
EDITOR_STATE_MAP,
|
||||
|
||||
rebundle,
|
||||
migrate,
|
||||
clear_state,
|
||||
go_to_warning_pos,
|
||||
handle_change,
|
||||
handle_select
|
||||
});
|
||||
|
||||
/** @type {Symbol} */
|
||||
let current_token;
|
||||
async function rebundle() {
|
||||
const token = (current_token = Symbol());
|
||||
let resolver = () => {};
|
||||
$bundling = new Promise((resolve) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
const result = await $bundler?.bundle($files);
|
||||
if (result && token === current_token) $bundle = result;
|
||||
resolver();
|
||||
}
|
||||
|
||||
async function migrate() {
|
||||
if (!compiler || $selected?.type !== 'svelte') return;
|
||||
|
||||
const result = await compiler.migrate($selected);
|
||||
if (result.error) {
|
||||
// TODO show somehow
|
||||
return;
|
||||
}
|
||||
|
||||
const new_files = $files.map((file) => {
|
||||
if (file.name === $selected?.name) {
|
||||
return {
|
||||
...file,
|
||||
source: result.result.code
|
||||
};
|
||||
}
|
||||
return file;
|
||||
});
|
||||
set({ files: new_files });
|
||||
}
|
||||
|
||||
let is_select_changing = false;
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
*/
|
||||
async function handle_select(filename) {
|
||||
is_select_changing = true;
|
||||
|
||||
$selected_name = filename;
|
||||
|
||||
if (!$selected) return;
|
||||
|
||||
await $module_editor?.set({ code: $selected.source, lang: $selected.type });
|
||||
|
||||
if (EDITOR_STATE_MAP.has(filename)) {
|
||||
$module_editor?.setEditorState(EDITOR_STATE_MAP.get(filename));
|
||||
} else {
|
||||
$module_editor?.clearEditorState();
|
||||
}
|
||||
|
||||
is_select_changing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CustomEvent<{ value: string }>} event
|
||||
*/
|
||||
async function handle_change(event) {
|
||||
if (is_select_changing) return;
|
||||
|
||||
files.update(($files) => {
|
||||
const file = { ...$selected };
|
||||
|
||||
file.source = event.detail.value;
|
||||
file.modified = true;
|
||||
|
||||
const idx = $files.findIndex((val) => get_full_filename(val) === $selected_name);
|
||||
|
||||
// @ts-ignore
|
||||
$files[idx] = file;
|
||||
|
||||
return $files;
|
||||
});
|
||||
|
||||
if (!$selected) return;
|
||||
|
||||
EDITOR_STATE_MAP.set(get_full_filename($selected), $module_editor?.getEditorState());
|
||||
|
||||
dispatch('change', {
|
||||
files: $files
|
||||
});
|
||||
|
||||
rebundle();
|
||||
}
|
||||
|
||||
/** @param {import('./types').MessageDetails | undefined} item */
|
||||
async function go_to_warning_pos(item) {
|
||||
if (!item) return;
|
||||
|
||||
// If its a bundler error, can't do anything about it
|
||||
if (!item.filename) return;
|
||||
|
||||
await handle_select(item.filename);
|
||||
|
||||
$module_editor?.focus();
|
||||
$module_editor?.setCursor(item.start.character);
|
||||
}
|
||||
|
||||
/** Deletes all editor state */
|
||||
function clear_state() {
|
||||
$module_editor?.clearEditorState();
|
||||
|
||||
EDITOR_STATE_MAP.clear();
|
||||
}
|
||||
|
||||
function populate_editor_state() {
|
||||
for (const file of $files) {
|
||||
EDITOR_STATE_MAP.set(
|
||||
get_full_filename(file),
|
||||
EditorState.create({
|
||||
doc: file.source
|
||||
}).toJSON()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const compiler = BROWSER ? new Compiler(svelteUrl) : null;
|
||||
|
||||
/** @type {import('./workers/workers').CompileMessageData | null} */
|
||||
let compiled = null;
|
||||
|
||||
/**
|
||||
* @param {import('./types').File | null} $selected
|
||||
* @param {import('svelte/compiler').CompileOptions} $compile_options
|
||||
*/
|
||||
async function recompile($selected, $compile_options) {
|
||||
if (!compiler || !$selected) return;
|
||||
|
||||
if ($selected.type === 'svelte' || $selected.type === 'js') {
|
||||
compiled = await compiler.compile($selected, $compile_options, true);
|
||||
runes = compiled.result.metadata?.runes ?? false;
|
||||
} else {
|
||||
runes = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: recompile($selected, $compile_options);
|
||||
|
||||
$: mobile = width < 540;
|
||||
|
||||
$: $toggleable = mobile && orientation === 'columns';
|
||||
|
||||
let width = 0;
|
||||
let show_output = false;
|
||||
|
||||
/** @type {string | null} */
|
||||
let status = null;
|
||||
let status_visible = false;
|
||||
|
||||
/** @type {NodeJS.Timeout | undefined} */
|
||||
let status_timeout = undefined;
|
||||
|
||||
$bundler = BROWSER
|
||||
? new Bundler({
|
||||
packages_url: packagesUrl,
|
||||
svelte_url: svelteUrl,
|
||||
onstatus: (message) => {
|
||||
if (message) {
|
||||
// show bundler status, but only after time has elapsed, to
|
||||
// prevent the banner flickering
|
||||
if (!status_visible && !status_timeout) {
|
||||
status_timeout = setTimeout(() => {
|
||||
status_visible = true;
|
||||
}, 400);
|
||||
}
|
||||
} else {
|
||||
clearTimeout(status_timeout);
|
||||
status_visible = false;
|
||||
status_timeout = undefined;
|
||||
}
|
||||
|
||||
status = message;
|
||||
}
|
||||
})
|
||||
: null;
|
||||
|
||||
/**
|
||||
* @param {BeforeUnloadEvent} event
|
||||
*/
|
||||
function before_unload(event) {
|
||||
if (showModified && $files.find((file) => file.modified)) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={before_unload} />
|
||||
|
||||
<div class="container" class:toggleable={$toggleable} bind:clientWidth={width}>
|
||||
<div class="viewport" class:output={show_output}>
|
||||
<SplitPane
|
||||
--color="var(--sk-text-4)"
|
||||
id="main"
|
||||
type={orientation === 'rows' ? 'vertical' : 'horizontal'}
|
||||
pos="{mobile || fixed ? fixedPos : orientation === 'rows' ? 60 : 50}%"
|
||||
min="100px"
|
||||
max="-4.1rem"
|
||||
>
|
||||
<section slot="a">
|
||||
<ComponentSelector show_modified={showModified} {runes} on:add on:remove />
|
||||
<ModuleEditor
|
||||
{autocomplete}
|
||||
error={compiled?.result.error}
|
||||
warnings={compiled?.result.warnings ?? []}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section slot="b" style="height: 100%;">
|
||||
<Output
|
||||
status={status_visible ? status : null}
|
||||
{embedded}
|
||||
{relaxed}
|
||||
{injectedJS}
|
||||
{injectedCSS}
|
||||
{showAst}
|
||||
{previewTheme}
|
||||
selected={$selected}
|
||||
{compiled}
|
||||
/>
|
||||
</section>
|
||||
</SplitPane>
|
||||
</div>
|
||||
|
||||
{#if $toggleable}
|
||||
<InputOutputToggle bind:checked={show_output} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100dvh - var(--sk-nav-height));
|
||||
background: var(--sk-back-1);
|
||||
}
|
||||
|
||||
.container :global(section) {
|
||||
position: relative;
|
||||
padding: 42px 0 0 0;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container :global(section) > :global(*):first-child {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container :global(section) > :global(*):last-child {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toggleable .viewport {
|
||||
width: 200%;
|
||||
height: calc(100% - 42px);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.toggleable .viewport.output {
|
||||
transform: translate(-50%);
|
||||
}
|
||||
|
||||
/* on mobile, override the <SplitPane> controls */
|
||||
@media (max-width: 799px) {
|
||||
:global([data-pane='main']) {
|
||||
--pos: 50% !important;
|
||||
}
|
||||
|
||||
:global([data-pane='editor']) {
|
||||
--pos: 5.4rem !important;
|
||||
}
|
||||
|
||||
:global([data-pane]) :global(.divider) {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,209 +0,0 @@
|
||||
import { snippetCompletion } from '@codemirror/autocomplete';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
|
||||
/** @typedef {(node: import('@lezer/common').SyntaxNode, context: import('@codemirror/autocomplete').CompletionContext, selected: import('./types').File) => boolean} Test */
|
||||
|
||||
/**
|
||||
* Returns `true` if `$bindable()` is valid
|
||||
* @type {Test}
|
||||
*/
|
||||
function is_bindable(node, context) {
|
||||
// disallow outside `let { x = $bindable }`
|
||||
if (node.parent?.name !== 'PatternProperty') return false;
|
||||
if (node.parent.parent?.name !== 'ObjectPattern') return false;
|
||||
if (node.parent.parent.parent?.name !== 'VariableDeclaration') return false;
|
||||
|
||||
let last = node.parent.parent.parent.lastChild;
|
||||
if (!last) return true;
|
||||
|
||||
// if the declaration is incomplete, assume the best
|
||||
if (last.name === 'ObjectPattern' || last.name === 'Equals' || last.name === '⚠') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (last.name === ';') {
|
||||
last = last.prevSibling;
|
||||
if (!last || last.name === '⚠') return true;
|
||||
}
|
||||
|
||||
// if the declaration is complete, only return true if it is a `$props()` declaration
|
||||
return (
|
||||
last.name === 'CallExpression' &&
|
||||
last.firstChild?.name === 'VariableName' &&
|
||||
context.state.sliceDoc(last.firstChild.from, last.firstChild.to) === '$props'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if `$props()` is valid
|
||||
* TODO only allow in `.svelte` files, and only at the top level
|
||||
* @type {Test}
|
||||
*/
|
||||
function is_props(node, _, selected) {
|
||||
if (selected.type !== 'svelte') return false;
|
||||
|
||||
return (
|
||||
node.name === 'VariableName' &&
|
||||
node.parent?.name === 'VariableDeclaration' &&
|
||||
node.parent.parent?.name === 'Script'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` is this is a valid place to declare state
|
||||
* @type {Test}
|
||||
*/
|
||||
function is_state(node) {
|
||||
let parent = node.parent;
|
||||
|
||||
if (node.name === '.' || node.name === 'PropertyName') {
|
||||
if (parent?.name !== 'MemberExpression') return false;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
if (!parent) return false;
|
||||
|
||||
return parent.name === 'VariableDeclaration' || parent.name === 'PropertyDeclaration';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if we're already in a valid call expression, e.g.
|
||||
* changing an existing `$state()` to `$state.raw()`
|
||||
* @type {Test}
|
||||
*/
|
||||
function is_state_call(node) {
|
||||
let parent = node.parent;
|
||||
|
||||
if (node.name === '.' || node.name === 'PropertyName') {
|
||||
if (parent?.name !== 'MemberExpression') return false;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
if (parent?.name !== 'CallExpression') {
|
||||
return false;
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
if (!parent) return false;
|
||||
|
||||
return parent.name === 'VariableDeclaration' || parent.name === 'PropertyDeclaration';
|
||||
}
|
||||
|
||||
/** @type {Test} */
|
||||
function is_statement(node) {
|
||||
if (node.name === 'VariableName') {
|
||||
return node.parent?.name === 'ExpressionStatement';
|
||||
}
|
||||
|
||||
if (node.name === '.' || node.name === 'PropertyName') {
|
||||
return node.parent?.parent?.name === 'ExpressionStatement';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @type {Array<{ snippet: string, test?: Test }>} */
|
||||
const runes = [
|
||||
{ snippet: '$state(${})', test: is_state },
|
||||
{ snippet: '$state', test: is_state_call },
|
||||
{ snippet: '$props()', test: is_props },
|
||||
{ snippet: '$derived(${});', test: is_state },
|
||||
{ snippet: '$derived', test: is_state_call },
|
||||
{ snippet: '$derived.by(() => {\n\t${}\n});', test: is_state },
|
||||
{ snippet: '$derived.by', test: is_state_call },
|
||||
{ snippet: '$effect(() => {\n\t${}\n});', test: is_statement },
|
||||
{ snippet: '$effect.pre(() => {\n\t${}\n});', test: is_statement },
|
||||
{ snippet: '$state.raw(${});', test: is_state },
|
||||
{ snippet: '$state.raw', test: is_state_call },
|
||||
{ snippet: '$bindable()', test: is_bindable },
|
||||
{ snippet: '$effect.root(() => {\n\t${}\n})' },
|
||||
{ snippet: '$state.snapshot(${})' },
|
||||
{ snippet: '$effect.tracking()' },
|
||||
{ snippet: '$inspect(${});', test: is_statement }
|
||||
];
|
||||
|
||||
const options = runes.map(({ snippet, test }, i) => ({
|
||||
option: snippetCompletion(snippet, {
|
||||
type: 'keyword',
|
||||
boost: runes.length - i,
|
||||
label: snippet.includes('(') ? snippet.slice(0, snippet.indexOf('(')) : snippet
|
||||
}),
|
||||
test
|
||||
}));
|
||||
|
||||
/**
|
||||
* @param {import('@codemirror/autocomplete').CompletionContext} context
|
||||
* @param {import('./types.js').File} selected
|
||||
* @param {import('./types.js').File[]} files
|
||||
*/
|
||||
export function autocomplete(context, selected, files) {
|
||||
let node = syntaxTree(context.state).resolveInner(context.pos, -1);
|
||||
|
||||
if (node.name === 'String' && node.parent?.name === 'ImportDeclaration') {
|
||||
const modules = [
|
||||
'svelte',
|
||||
'svelte/animate',
|
||||
'svelte/easing',
|
||||
'svelte/events',
|
||||
'svelte/legacy',
|
||||
'svelte/motion',
|
||||
'svelte/reactivity',
|
||||
'svelte/store',
|
||||
'svelte/transition'
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
if (file === selected) continue;
|
||||
modules.push(`./${file.name}.${file.type}`);
|
||||
}
|
||||
|
||||
return {
|
||||
from: node.from + 1,
|
||||
options: modules.map((label) => ({
|
||||
label,
|
||||
type: 'string'
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
selected.type !== 'svelte' &&
|
||||
(selected.type !== 'js' || !selected.name.endsWith('.svelte'))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.name === 'VariableName' || node.name === 'PropertyName' || node.name === '.') {
|
||||
// special case — `$inspect(...).with(...)` is the only rune that 'returns'
|
||||
// an 'object' with a 'method'
|
||||
if (node.name === 'PropertyName' || node.name === '.') {
|
||||
if (
|
||||
node.parent?.name === 'MemberExpression' &&
|
||||
node.parent.firstChild?.name === 'CallExpression' &&
|
||||
node.parent.firstChild.firstChild?.name === 'VariableName' &&
|
||||
context.state.sliceDoc(
|
||||
node.parent.firstChild.firstChild.from,
|
||||
node.parent.firstChild.firstChild.to
|
||||
) === '$inspect'
|
||||
) {
|
||||
const open = context.matchBefore(/\.\w*/);
|
||||
if (!open) return null;
|
||||
|
||||
return {
|
||||
from: open.from,
|
||||
options: [snippetCompletion('.with(${})', { type: 'keyword', label: '.with' })]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const open = context.matchBefore(/\$[\w\.]*/);
|
||||
if (!open) return null;
|
||||
|
||||
return {
|
||||
from: open.from,
|
||||
options: options
|
||||
.filter((option) => (option.test ? option.test(node, context, selected) : true))
|
||||
.map((option) => option.option)
|
||||
};
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
|
||||
const key = Symbol('repl');
|
||||
|
||||
/** @returns {import("./types").ReplContext} */
|
||||
export function get_repl_context() {
|
||||
return getContext(key);
|
||||
}
|
||||
|
||||
/** @param {import("./types").ReplContext} value */
|
||||
export function set_repl_context(value) {
|
||||
setContext(key, value);
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { default } from './Repl.svelte';
|
@ -1,153 +0,0 @@
|
||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
|
||||
const ERROR_HUE = 0;
|
||||
const WARNING_HUE = 40;
|
||||
|
||||
const WARNING_FG = `hsl(${WARNING_HUE} 100% 60%)`;
|
||||
const WARNING_BG = `hsl(${WARNING_HUE} 100% 40% / 0.5)`;
|
||||
|
||||
const ERROR_FG = `hsl(${ERROR_HUE} 100% 40%)`;
|
||||
const ERROR_BG = `hsl(${ERROR_HUE} 100% 40% / 0.5)`;
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @param {string} attrs
|
||||
*/
|
||||
function svg(content, attrs = `viewBox="0 0 40 40"`) {
|
||||
return `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" ${attrs}>${encodeURIComponent(
|
||||
content
|
||||
)}</svg>')`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} color
|
||||
*/
|
||||
function underline(color) {
|
||||
return svg(
|
||||
`<path d="m0 3.5 l2 -1.5 l1 0 l2 1.5 l1 0" stroke="${color}" fill="none" stroke-width="1"/>`,
|
||||
`width="6" height="4"`
|
||||
);
|
||||
}
|
||||
|
||||
const svelteThemeStyles = EditorView.theme(
|
||||
{
|
||||
'&': {
|
||||
color: 'var(--sk-code-base)',
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
|
||||
'.cm-content': {
|
||||
caretColor: 'var(--sk-theme-3)'
|
||||
},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--sk-theme-3)' },
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
||||
{ backgroundColor: 'var(--sk-selection-color)' },
|
||||
|
||||
'.cm-panels': { backgroundColor: 'var(--sk-back-2)', color: 'var(--sk-text-2)' },
|
||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: 'var(--sk-theme-2)'
|
||||
// outline: '1px solid #457dff',
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: '#6199ff2f'
|
||||
},
|
||||
|
||||
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
|
||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: '#bad0f847'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'var(--sk-back-3)',
|
||||
border: 'none'
|
||||
},
|
||||
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: 'var(--sk-back-4)'
|
||||
},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: '#ddd'
|
||||
},
|
||||
|
||||
// https://github.com/codemirror/lint/blob/271b35f5d31a7e3645eaccbfec608474022098e1/src/lint.ts#L620
|
||||
'.cm-lintRange': {
|
||||
backgroundPosition: 'left bottom',
|
||||
backgroundRepeat: 'repeat-x',
|
||||
paddingBottom: '4px'
|
||||
},
|
||||
'.cm-lintRange-error': {
|
||||
backgroundImage: underline(ERROR_FG)
|
||||
},
|
||||
'.cm-lintRange-warning': {
|
||||
backgroundImage: underline(WARNING_FG)
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: 'var(--sk-back-3)',
|
||||
borderBottomColor: 'var(--sk-back-3)'
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
color: 'var(--sk-text-2) !important',
|
||||
perspective: '1px',
|
||||
'& > ul > li[aria-selected]': {
|
||||
backgroundColor: 'var(--sk-back-4)',
|
||||
color: 'var(--sk-text-1) !important'
|
||||
}
|
||||
}
|
||||
},
|
||||
{ dark: true }
|
||||
);
|
||||
|
||||
/// The highlighting style for code in the One Dark theme.
|
||||
const svelteHighlightStyle = HighlightStyle.define([
|
||||
{ tag: t.keyword, color: 'var(--sk-code-keyword)' },
|
||||
{
|
||||
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
|
||||
color: 'var(--sk-code-base)'
|
||||
},
|
||||
{ tag: [t.function(t.variableName), t.labelName], color: 'var(--sk-code-tags)' },
|
||||
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: 'var(--sk-code-base)' },
|
||||
{ tag: [t.definition(t.name), t.separator], color: 'var(--sk-code-base)' },
|
||||
{
|
||||
tag: [
|
||||
t.typeName,
|
||||
t.className,
|
||||
t.number,
|
||||
t.changed,
|
||||
t.annotation,
|
||||
t.modifier,
|
||||
t.self,
|
||||
t.namespace
|
||||
],
|
||||
color: 'var(--sk-code-tags)'
|
||||
},
|
||||
{
|
||||
tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)],
|
||||
color: 'var(--sk-code-base)'
|
||||
},
|
||||
{ tag: [t.meta, t.comment], color: 'var(--sk-code-comment)' },
|
||||
{ tag: t.strong, fontWeight: 'bold' },
|
||||
{ tag: t.emphasis, fontStyle: 'italic' },
|
||||
{ tag: t.strikethrough, textDecoration: 'line-through' },
|
||||
{ tag: t.link, color: 'var(--sk-code-base)', textDecoration: 'underline' },
|
||||
{ tag: t.heading, fontWeight: 'bold', color: 'var(--sk-text-1)' },
|
||||
{ tag: [t.atom, t.bool], color: 'var(--sk-code-atom)' },
|
||||
{ tag: [t.processingInstruction, t.string, t.inserted], color: 'var(--sk-code-string)' },
|
||||
{ tag: t.invalid, color: '#ff008c' }
|
||||
]);
|
||||
|
||||
export const svelteTheme = [svelteThemeStyles, syntaxHighlighting(svelteHighlightStyle)];
|
@ -1,77 +0,0 @@
|
||||
import type { EditorState } from '@codemirror/state';
|
||||
import { OutputChunk, RollupError } from '@rollup/browser';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type { CompileOptions, CompileError } from 'svelte/compiler';
|
||||
|
||||
export type Lang = 'js' | 'svelte' | 'json' | 'md' | 'css' | (string & Record<never, never>);
|
||||
|
||||
type StartOrEnd = {
|
||||
line: number;
|
||||
column: number;
|
||||
character: number;
|
||||
};
|
||||
|
||||
export type MessageDetails = {
|
||||
start: StartOrEnd;
|
||||
end: StartOrEnd;
|
||||
filename: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type Warning = MessageDetails;
|
||||
|
||||
export type Bundle = {
|
||||
uid: number;
|
||||
client: OutputChunk | null;
|
||||
error: (RollupError & CompileError) | null;
|
||||
server: OutputChunk | null;
|
||||
imports: string[];
|
||||
warnings: Warning[];
|
||||
};
|
||||
|
||||
export type File = {
|
||||
name: string;
|
||||
source: string;
|
||||
type: Lang;
|
||||
modified?: boolean;
|
||||
};
|
||||
|
||||
export type ReplState = {
|
||||
files: File[];
|
||||
selected_name: string;
|
||||
selected: File | null;
|
||||
bundle: Bundle | null;
|
||||
bundling: Promise<void>;
|
||||
bundler: import('./Bundler').default | null;
|
||||
compile_options: CompileOptions;
|
||||
cursor_pos: number;
|
||||
toggleable: boolean;
|
||||
module_editor: import('./CodeMirror.svelte').default | null;
|
||||
};
|
||||
|
||||
export type ReplContext = {
|
||||
files: Writable<ReplState['files']>;
|
||||
selected_name: Writable<ReplState['selected_name']>;
|
||||
selected: Readable<ReplState['selected']>;
|
||||
bundle: Writable<ReplState['bundle']>;
|
||||
bundling: Writable<ReplState['bundling']>;
|
||||
bundler: Writable<ReplState['bundler']>;
|
||||
compile_options: Writable<ReplState['compile_options']>;
|
||||
cursor_pos: Writable<ReplState['cursor_pos']>;
|
||||
toggleable: Writable<ReplState['toggleable']>;
|
||||
module_editor: Writable<ReplState['module_editor']>;
|
||||
|
||||
EDITOR_STATE_MAP: Map<string, EditorState>;
|
||||
|
||||
// Methods
|
||||
rebundle(): Promise<void>;
|
||||
migrate(): Promise<void>;
|
||||
handle_select(filename: string): Promise<void>;
|
||||
handle_change(
|
||||
event: CustomEvent<{
|
||||
value: string;
|
||||
}>
|
||||
): Promise<void>;
|
||||
go_to_warning_pos(item?: MessageDetails): Promise<void>;
|
||||
clear_state(): void;
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
/**
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
* @param {number} value
|
||||
*/
|
||||
export const clamp = (min, max, value) => Math.max(min, Math.min(max, value));
|
||||
|
||||
/**
|
||||
* @param {number} ms
|
||||
*/
|
||||
export const sleep = (ms) => new Promise((f) => setTimeout(f, ms));
|
||||
|
||||
/** @param {import('./types').File} file */
|
||||
export function get_full_filename(file) {
|
||||
return `${file.name}.${file.type}`;
|
||||
}
|
@ -1,576 +0,0 @@
|
||||
/// <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
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
import { parse } from 'acorn';
|
||||
import { walk } from 'zimmerframe';
|
||||
|
||||
const require = `function require(id) {
|
||||
if (id in __repl_lookup) return __repl_lookup[id];
|
||||
throw new Error(\`Cannot require modules dynamically (\${id})\`);
|
||||
}`;
|
||||
|
||||
/** @type {import('@rollup/browser').Plugin} */
|
||||
export default {
|
||||
name: 'commonjs',
|
||||
|
||||
transform: (code, id) => {
|
||||
if (!/\b(require|module|exports)\b/.test(code)) return;
|
||||
|
||||
try {
|
||||
const ast = parse(code, {
|
||||
ecmaVersion: 'latest'
|
||||
});
|
||||
|
||||
/** @type {string[]} */
|
||||
const requires = [];
|
||||
|
||||
walk(/** @type {import('estree').Node} */ (ast), null, {
|
||||
CallExpression: (node) => {
|
||||
if (node.callee.type === 'Identifier' && node.callee.name === 'require') {
|
||||
if (node.arguments.length !== 1) return;
|
||||
const arg = node.arguments[0];
|
||||
if (arg.type !== 'Literal' || typeof arg.value !== 'string') return;
|
||||
|
||||
requires.push(arg.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const imports = requires.map((id, i) => `import __repl_${i} from '${id}';`).join('\n');
|
||||
const lookup = `const __repl_lookup = { ${requires
|
||||
.map((id, i) => `'${id}': __repl_${i}`)
|
||||
.join(', ')} };`;
|
||||
|
||||
const transformed = [
|
||||
imports,
|
||||
lookup,
|
||||
require,
|
||||
`const exports = {}; const module = { exports };`,
|
||||
code,
|
||||
`export default module.exports;`
|
||||
].join('\n\n');
|
||||
|
||||
return {
|
||||
code: transformed,
|
||||
map: null
|
||||
};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
/** @type {import('@rollup/browser').Plugin} */
|
||||
export default {
|
||||
name: 'glsl',
|
||||
transform: (code, id) => {
|
||||
if (!id.endsWith('.glsl')) return;
|
||||
|
||||
return {
|
||||
code: `export default ${JSON.stringify(code)};`,
|
||||
map: null
|
||||
};
|
||||
}
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
/** @type {import('@rollup/browser').Plugin} */
|
||||
export default {
|
||||
name: 'json',
|
||||
transform: (code, id) => {
|
||||
if (!id.endsWith('.json')) return;
|
||||
|
||||
return {
|
||||
code: `export default ${code};`,
|
||||
map: null
|
||||
};
|
||||
}
|
||||
};
|
@ -1,111 +0,0 @@
|
||||
import { parse } from 'acorn';
|
||||
import { print } from 'esrap';
|
||||
import { walk } from 'zimmerframe';
|
||||
|
||||
const TIMEOUT = 100;
|
||||
|
||||
const regex = /\b(for|while)\b/;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} code
|
||||
* @returns {import('estree').Statement}
|
||||
*/
|
||||
function parse_statement(code) {
|
||||
return /** @type {import('estree').Statement} */ (parse(code, { ecmaVersion: 'latest' }).body[0]);
|
||||
}
|
||||
|
||||
const declaration = parse_statement(`
|
||||
const __start = Date.now();
|
||||
`);
|
||||
|
||||
const check = parse_statement(`
|
||||
if (Date.now() > __start + ${TIMEOUT}) {
|
||||
throw new Error('Infinite loop detected');
|
||||
}
|
||||
`);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('estree').Node[]} path
|
||||
* @returns {null | import('estree').FunctionExpression | import('estree').FunctionDeclaration | import('estree').ArrowFunctionExpression}
|
||||
*/
|
||||
export function get_current_function(path) {
|
||||
for (let i = path.length - 1; i >= 0; i--) {
|
||||
const node = path[i];
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {import('estree').DoWhileStatement | import('estree').ForStatement | import('estree').WhileStatement} Statement
|
||||
* @param {Statement} node
|
||||
* @param {import('zimmerframe').Context<import('estree').Node, null>} context
|
||||
* @returns {import('estree').Node | void}
|
||||
*/
|
||||
function loop_protect(node, context) {
|
||||
const current_function = get_current_function(context.path);
|
||||
|
||||
if (current_function === null || (!current_function.async && !current_function.generator)) {
|
||||
const body = /** @type {import('estree').Statement} */ (context.visit(node.body));
|
||||
|
||||
const statements = body.type === 'BlockStatement' ? [...body.body] : [body];
|
||||
|
||||
/** @type {import('estree').BlockStatement} */
|
||||
const replacement = {
|
||||
type: 'BlockStatement',
|
||||
body: [
|
||||
declaration,
|
||||
{
|
||||
.../** @type {Statement} */ (context.next() ?? node),
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
body: [...statements, check]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return replacement;
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
|
||||
/** @type {import('@rollup/browser').Plugin} */
|
||||
export default {
|
||||
name: 'loop-protect',
|
||||
transform: (code, id) => {
|
||||
// only applies to local files, not imports
|
||||
if (!id.startsWith('./')) return;
|
||||
|
||||
// only applies to JS and Svelte files
|
||||
if (!id.endsWith('.js') && !id.endsWith('.svelte')) return;
|
||||
|
||||
// fast path
|
||||
if (!regex.test(code)) return;
|
||||
|
||||
const ast = parse(code, {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
});
|
||||
|
||||
const transformed = walk(/** @type {import('estree').Node} */ (ast), null, {
|
||||
WhileStatement: loop_protect,
|
||||
DoWhileStatement: loop_protect,
|
||||
ForStatement: loop_protect
|
||||
});
|
||||
|
||||
// nothing changed
|
||||
if (ast === transformed) return null;
|
||||
|
||||
return print(transformed);
|
||||
}
|
||||
};
|
@ -1,72 +0,0 @@
|
||||
/** @param {string} str */
|
||||
function escape(str) {
|
||||
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
|
||||
}
|
||||
|
||||
/** @param {unknown} functionOrValue */
|
||||
function ensureFunction(functionOrValue) {
|
||||
if (typeof functionOrValue === 'function') {
|
||||
return functionOrValue;
|
||||
}
|
||||
return function () {
|
||||
return functionOrValue;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
*/
|
||||
function longest(a, b) {
|
||||
return b.length - a.length;
|
||||
}
|
||||
|
||||
/** @param {Record<string, unknown>} object */
|
||||
function mapToFunctions(object) {
|
||||
return Object.keys(object).reduce(
|
||||
/** @param {Record<string, Function>} functions */ function (functions, key) {
|
||||
functions[key] = ensureFunction(object[key]);
|
||||
return functions;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} options
|
||||
* @returns {import('@rollup/browser').Plugin}
|
||||
*/
|
||||
function replace(options) {
|
||||
const functionValues = mapToFunctions(options);
|
||||
const keys = Object.keys(functionValues).sort(longest).map(escape);
|
||||
|
||||
const pattern = new RegExp('\\b(' + keys.join('|') + ')\\b', 'g');
|
||||
|
||||
return {
|
||||
name: 'replace',
|
||||
|
||||
transform: function transform(code, id) {
|
||||
let hasReplacements = false;
|
||||
let match;
|
||||
let start;
|
||||
let end;
|
||||
let replacement;
|
||||
|
||||
code = code.replace(pattern, (_, key) => {
|
||||
hasReplacements = true;
|
||||
return String(functionValues[key](id));
|
||||
});
|
||||
|
||||
if (!hasReplacements) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
map: null
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default replace;
|
@ -1,154 +0,0 @@
|
||||
/// <reference lib="webworker" />
|
||||
self.window = self; //TODO: still need?: egregious hack to get magic-string to work in a worker
|
||||
|
||||
/**
|
||||
* @type {{
|
||||
* parse: typeof import('svelte/compiler').parse;
|
||||
* compile: typeof import('svelte/compiler').compile;
|
||||
* compileModule: typeof import('svelte/compiler').compileModule;
|
||||
* VERSION: string;
|
||||
* }}
|
||||
*/
|
||||
let svelte;
|
||||
|
||||
/** @type {(arg?: never) => void} */
|
||||
let fulfil_ready;
|
||||
const ready = new Promise((f) => {
|
||||
fulfil_ready = f;
|
||||
});
|
||||
|
||||
self.addEventListener(
|
||||
'message',
|
||||
/** @param {MessageEvent<import("../workers").CompileMessageData>} event */
|
||||
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())
|
||||
.catch(() => ({ version: 'experimental' }));
|
||||
|
||||
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 'compile':
|
||||
await ready;
|
||||
postMessage(compile(event.data));
|
||||
break;
|
||||
|
||||
case 'migrate':
|
||||
await ready;
|
||||
postMessage(migrate(event.data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const common_options = {
|
||||
dev: false,
|
||||
css: false
|
||||
};
|
||||
|
||||
/** @param {import("../workers").CompileMessageData} param0 */
|
||||
function compile({ id, source, options, return_ast }) {
|
||||
try {
|
||||
const css = `/* Select a component to see compiled CSS */`;
|
||||
|
||||
if (options.filename.endsWith('.svelte')) {
|
||||
const compiled = svelte.compile(source, {
|
||||
...options,
|
||||
discloseVersion: false // less visual noise in the output tab
|
||||
});
|
||||
|
||||
const { js, css, warnings, metadata } = compiled;
|
||||
|
||||
const ast = return_ast ? svelte.parse(source, { modern: true }) : undefined;
|
||||
|
||||
return {
|
||||
id,
|
||||
result: {
|
||||
js: js.code,
|
||||
css: css?.code || `/* Add a <sty` + `le> tag to see compiled CSS */`,
|
||||
error: null,
|
||||
warnings: warnings.map((warning) => warning.toJSON()),
|
||||
metadata,
|
||||
ast
|
||||
}
|
||||
};
|
||||
} else if (options.filename.endsWith('.svelte.js')) {
|
||||
const compiled = svelte.compileModule(source, {
|
||||
filename: options.filename,
|
||||
generate: options.generate,
|
||||
dev: options.dev
|
||||
});
|
||||
|
||||
if (compiled) {
|
||||
return {
|
||||
id,
|
||||
result: {
|
||||
js: compiled.js.code,
|
||||
css,
|
||||
error: null,
|
||||
warnings: compiled.warnings.map((warning) => warning.toJSON()),
|
||||
metadata: compiled.metadata
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
result: {
|
||||
js: `// Select a component, or a '.svelte.js' module that uses runes, to see compiled output`,
|
||||
css,
|
||||
error: null,
|
||||
warnings: [],
|
||||
metadata: null
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
let message = `/*\nError compiling ${err.filename ?? 'component'}:\n${err.message}\n*/`;
|
||||
|
||||
return {
|
||||
id,
|
||||
result: {
|
||||
js: message,
|
||||
css: message,
|
||||
error: {
|
||||
message: err.message,
|
||||
position: err.position
|
||||
},
|
||||
warnings: [],
|
||||
metadata: null
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import("../workers").MigrateMessageData} param0 */
|
||||
function migrate({ id, source, filename }) {
|
||||
try {
|
||||
const result = svelte.migrate(source, { filename });
|
||||
|
||||
return {
|
||||
id,
|
||||
result
|
||||
};
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
let message = `/*\nError migrating ${err.filename ?? 'component'}:\n${err.message}\n*/`;
|
||||
|
||||
return {
|
||||
id,
|
||||
result: { code: source },
|
||||
error: message
|
||||
};
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"include": ["./**/*"],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"svelte": ["../../../static/svelte/main"],
|
||||
"svelte/*": ["../../../static/svelte/*"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
self.window = self; // hack for magic-sring and rollup inline sourcemaps
|
@ -1,34 +0,0 @@
|
||||
import type { CompileOptions, File } from '../types';
|
||||
|
||||
export type CompileMessageData = {
|
||||
id: number;
|
||||
type: 'compile' | 'init';
|
||||
source: string;
|
||||
options: CompileOptions;
|
||||
is_entry: boolean;
|
||||
return_ast: boolean;
|
||||
svelte_url?: string;
|
||||
result: {
|
||||
js: string;
|
||||
css: string;
|
||||
ast?: import('svelte/types/compiler/interfaces').Ast;
|
||||
metadata?: {
|
||||
runes: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type BundleMessageData = {
|
||||
uid: number;
|
||||
type: 'init' | 'bundle' | 'status';
|
||||
message: string;
|
||||
packages_url: string;
|
||||
svelte_url: string;
|
||||
files: File[];
|
||||
};
|
||||
|
||||
export type MigrateMessageData = {
|
||||
id: number;
|
||||
result: { code: string };
|
||||
error?: string;
|
||||
};
|
@ -1,73 +0,0 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
|
||||
// we don't want to use <svelte:window bind:online> here,
|
||||
// because we only care about the online state when
|
||||
// the page first loads
|
||||
const online = typeof navigator !== 'undefined' ? navigator.onLine : true;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$page.status}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
{#if online}
|
||||
{#if $page.status === 404}
|
||||
<h1>Not found!</h1>
|
||||
<p>
|
||||
If you were expecting to find something here, please drop by the
|
||||
<a href="https://svelte.dev/chat"> Discord chatroom </a>
|
||||
and let us know, or raise an issue on
|
||||
<a href="https://github.com/sveltejs/sites">GitHub</a>. Thanks!
|
||||
</p>
|
||||
{:else}
|
||||
<h1>Yikes!</h1>
|
||||
<p>Something went wrong when we tried to render this page.</p>
|
||||
{#if $page.error.message}
|
||||
<p class="error">{$page.status}: {$page.error.message}</p>
|
||||
{:else}
|
||||
<p class="error">Encountered a {$page.status} error.</p>
|
||||
{/if}
|
||||
<p>Please try reloading the page.</p>
|
||||
<p>
|
||||
If the error persists, please drop by the
|
||||
<a href="https://svelte.dev/chat"> Discord chatroom </a>
|
||||
and let us know, or raise an issue on
|
||||
<a href="https://github.com/sveltejs/sites">GitHub</a>. Thanks!
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<h1>It looks like you're offline</h1>
|
||||
<p>Reload the page once you've found the internet.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: var(--sk-page-padding-top) var(--sk-page-padding-side) 6rem var(--sk-page-padding-side);
|
||||
}
|
||||
|
||||
h1,
|
||||
p {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.8em;
|
||||
font-weight: 300;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--sk-theme-2);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
font: 600 16px/1.7 var(--sk-font);
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
@ -1,12 +0,0 @@
|
||||
export const prerender = true;
|
||||
|
||||
/** @type {import('@sveltejs/adapter-vercel').EdgeConfig} */
|
||||
export const config = {
|
||||
runtime: 'edge'
|
||||
};
|
||||
|
||||
export const load = async ({ fetch }) => {
|
||||
const nav_data = await fetch('/nav.json').then((r) => r.json());
|
||||
|
||||
return { nav_links: nav_data };
|
||||
};
|
@ -1,101 +0,0 @@
|
||||
<script>
|
||||
import { injectSpeedInsights } from '@vercel/speed-insights/sveltekit';
|
||||
import { page } from '$app/stores';
|
||||
import { Icon, Shell } from '@sveltejs/site-kit/components';
|
||||
import { Nav, Separator } from '@sveltejs/site-kit/nav';
|
||||
import '@sveltejs/site-kit/styles/index.css';
|
||||
|
||||
injectSpeedInsights();
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Svelte 5 preview</title>
|
||||
|
||||
<!-- TODO separate og image? -->
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:image" content="https://svelte.dev/images/twitter-thumbnail.jpg" />
|
||||
<meta name="og:image" content="https://svelte.dev/images/twitter-thumbnail.jpg" />
|
||||
</svelte:head>
|
||||
|
||||
<Shell nav_visible={$page.url.pathname !== '/repl/embed'}>
|
||||
<Nav slot="top-nav" title={$page.data.nav_title} links={data.nav_links}>
|
||||
<svelte:fragment slot="home-large">
|
||||
<strong>Preview</strong>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="home-small">
|
||||
<strong>Preview</strong>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="search">
|
||||
<div class="disclaimer">
|
||||
<span class="show-when-small">Work in progress!</span>
|
||||
<span class="show-when-large">Work in progress. Expect bugs!</span>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="external-links">
|
||||
<a href="https://svelte.dev">Svelte</a>
|
||||
|
||||
<Separator />
|
||||
|
||||
<a href="https://svelte.dev/chat" title="Discord Chat">
|
||||
<span class="small">Discord</span>
|
||||
<span class="large"><Icon name="discord" /></span>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/sveltejs/svelte" title="GitHub Repo">
|
||||
<span class="small">GitHub</span>
|
||||
<span class="large"><Icon name="github" /></span>
|
||||
</a>
|
||||
</svelte:fragment>
|
||||
</Nav>
|
||||
|
||||
<slot />
|
||||
</Shell>
|
||||
|
||||
<style>
|
||||
:global(:root) {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
:global(html, body) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.disclaimer > span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: var(--sk-theme-1);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.show-when-large {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.show-when-small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.show-when-large {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
/* site-kit overrides */
|
||||
:global(button.search[aria-label='Search']) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
@ -1,98 +0,0 @@
|
||||
<script>
|
||||
import { theme } from '@sveltejs/site-kit/stores';
|
||||
import '@sveltejs/site-kit/styles/index.css';
|
||||
import { replaceState } from '$app/navigation';
|
||||
|
||||
import Repl from '$lib/Repl.svelte';
|
||||
import { default_files } from './defaults.js';
|
||||
import { compress_and_encode_text, decode_and_decompress_text } from './gzip.js';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
|
||||
/** @type {Repl} */
|
||||
let repl;
|
||||
|
||||
let setting_hash = false;
|
||||
let navigating = false;
|
||||
|
||||
afterNavigate(change_from_hash);
|
||||
|
||||
async function change_from_hash() {
|
||||
navigating = true;
|
||||
|
||||
const hash = location.hash.slice(1);
|
||||
|
||||
if (!hash) {
|
||||
repl.set({
|
||||
files: default_files()
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Uncompressed hashes that were used at some point start with 'ey' (JSON)
|
||||
const data = hash.startsWith('ey')
|
||||
? atob(hash.replaceAll('-', '+').replaceAll('_', '/'))
|
||||
: await decode_and_decompress_text(hash);
|
||||
let files;
|
||||
|
||||
try {
|
||||
files = JSON.parse(data).files;
|
||||
} catch {
|
||||
// probably an old link from when we only had a single component
|
||||
files = [
|
||||
{
|
||||
name: 'App',
|
||||
type: 'svelte',
|
||||
source: data
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
repl.set({
|
||||
files
|
||||
});
|
||||
} catch {
|
||||
alert(`Couldn't load the code from the URL. Make sure you copied the link correctly.`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {CustomEvent<any>} e*/
|
||||
async function change_from_editor(e) {
|
||||
if (navigating) {
|
||||
navigating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const json = JSON.stringify({
|
||||
files: e.detail.files
|
||||
});
|
||||
|
||||
setting_hash = true;
|
||||
|
||||
replaceState(
|
||||
`${location.pathname}${location.search}#${await compress_and_encode_text(json)}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:hashchange={() => {
|
||||
if (!setting_hash) {
|
||||
change_from_hash();
|
||||
}
|
||||
|
||||
setting_hash = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
autocomplete={true}
|
||||
on:add={change_from_editor}
|
||||
on:change={change_from_editor}
|
||||
on:remove={change_from_editor}
|
||||
previewTheme={$theme.current}
|
||||
showAst={true}
|
||||
/>
|
@ -1,21 +0,0 @@
|
||||
export const default_files = () => [
|
||||
{
|
||||
name: 'App',
|
||||
type: 'svelte',
|
||||
source: `
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
function increment() {
|
||||
count += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={increment}>
|
||||
clicks: {count}
|
||||
</button>
|
||||
`
|
||||
.replace(/^\t{3}/gm, '')
|
||||
.trim()
|
||||
}
|
||||
];
|
@ -1,13 +0,0 @@
|
||||
export async function load({ url }) {
|
||||
if (url.pathname === '/docs') {
|
||||
return {
|
||||
sections: []
|
||||
};
|
||||
}
|
||||
|
||||
const { get_docs_data, get_docs_list } = await import('./render.js');
|
||||
|
||||
return {
|
||||
sections: get_docs_list(await get_docs_data())
|
||||
};
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { DocsContents } from '@sveltejs/site-kit/docs';
|
||||
|
||||
export let data;
|
||||
|
||||
$: pageData = $page.data.page;
|
||||
|
||||
$: title = pageData?.title;
|
||||
$: category = pageData?.category;
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="toc-container" style="order: 1">
|
||||
<DocsContents contents={data.sections} />
|
||||
</div>
|
||||
|
||||
<div class="page content">
|
||||
{#if category}
|
||||
<p class="category">{category}</p>
|
||||
{/if}
|
||||
{#if title}
|
||||
<h1>{title}</h1>
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
--sidebar-menu-width: 28rem;
|
||||
--sidebar-width: var(--sidebar-menu-width);
|
||||
--ts-toggle-height: 4.2rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding: var(--sk-page-padding-top) var(--sk-page-padding-side);
|
||||
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.page :global(:where(h2, h3) code) {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.category {
|
||||
font: 700 var(--sk-text-s) var(--sk-font);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
margin: 0 0 0.5em;
|
||||
color: var(--sk-text-3);
|
||||
}
|
||||
|
||||
@media (min-width: 832px) {
|
||||
.content {
|
||||
padding-left: calc(var(--sidebar-width) + var(--sk-page-padding-side));
|
||||
}
|
||||
}
|
||||
|
||||
.toc-container {
|
||||
background: var(--sk-back-3);
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 832px) {
|
||||
.toc-container {
|
||||
display: block;
|
||||
width: var(--sidebar-width);
|
||||
height: calc(100vh - var(--sk-nav-height));
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: var(--sk-nav-height);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.toc-container::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: calc(var(--sidebar-width) - 1px);
|
||||
border-right: 1px solid var(--sk-back-5);
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-left: calc(var(--sidebar-width) + var(--sk-page-padding-side));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
--sidebar-width: max(28rem, 23vw);
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.page {
|
||||
--on-this-page-display: block;
|
||||
padding: var(--sk-page-padding-top) calc(var(--sidebar-width) + var(--sk-page-padding-side));
|
||||
margin: 0 auto;
|
||||
max-width: var(--sk-line-max-width);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,5 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
redirect(307, '/docs/introduction');
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function entries() {
|
||||
const { get_docs_data } = await import('../render.js');
|
||||
|
||||
const data = await get_docs_data();
|
||||
return data[0].pages.map((page) => ({ slug: page.slug }));
|
||||
}
|
||||
|
||||
export async function load({ params }) {
|
||||
const { get_docs_data, get_parsed_docs } = await import('../render.js');
|
||||
|
||||
const data = await get_docs_data();
|
||||
const processed_page = await get_parsed_docs(data, params.slug);
|
||||
|
||||
if (!processed_page) error(404);
|
||||
|
||||
return { page: processed_page };
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { copy_code_descendants } from '@sveltejs/site-kit/actions';
|
||||
import { DocsOnThisPage, setupDocsHovers } from '@sveltejs/site-kit/docs';
|
||||
|
||||
export let data;
|
||||
|
||||
$: pages = data.sections.flatMap((section) => section.pages);
|
||||
$: index = pages.findIndex(({ path }) => path === $page.url.pathname);
|
||||
$: prev = pages[index - 1];
|
||||
$: next = pages[index + 1];
|
||||
|
||||
setupDocsHovers();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.page?.title} • Docs • Svelte 5 preview</title>
|
||||
|
||||
<meta name="twitter:title" content="{data.page.title} • Docs • Svelte 5 preview" />
|
||||
<meta name="twitter:description" content="{data.page.title} • Svelte 5 preview documentation" />
|
||||
<meta name="description" content="{data.page.title} • Svelte 5 preview documentation" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="text" id="docs-content" use:copy_code_descendants>
|
||||
<DocsOnThisPage details={data.page} />
|
||||
|
||||
{@html data.page.content}
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div>
|
||||
<span class:faded={!prev}>previous</span>
|
||||
|
||||
{#if prev}
|
||||
<a href={prev.path}>{prev.title}</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class:faded={!next}>next</span>
|
||||
{#if next}
|
||||
<a href={next.path}>{next.title}</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.controls {
|
||||
max-width: calc(var(--sk-line-max-width) + 1rem);
|
||||
border-top: 1px solid var(--sk-back-4);
|
||||
padding: 1rem 0 0 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin: 6rem 0 0 0;
|
||||
}
|
||||
|
||||
.controls > :first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.controls > :last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.controls span {
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
color: var(--sk-text-3);
|
||||
}
|
||||
|
||||
.controls span.faded {
|
||||
opacity: 0.4;
|
||||
}
|
||||
</style>
|
@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
Welcome to the Svelte 5 preview documentation! This is intended as a resource for people who already have some familiarity with Svelte and want to learn about the new <em>runes</em> API, which you can learn about in the [Introducing runes](https://svelte.dev/blog/runes) blog post.
|
||||
|
||||
You can try runes for yourself in the [playground](/), or learn more about our plans via the [FAQ](/docs/faq).
|
@ -1,700 +0,0 @@
|
||||
---
|
||||
title: Runes
|
||||
---
|
||||
|
||||
Svelte 5 introduces _runes_, a powerful set of primitives for controlling reactivity inside your Svelte components and — for the first time — inside `.svelte.js` and `.svelte.ts` modules.
|
||||
|
||||
Runes are function-like symbols that provide instructions to the Svelte compiler. You don't need to import them from anywhere — when you use Svelte, they're part of the language.
|
||||
|
||||
When you [opt in to runes mode](#how-to-opt-in), the non-runes features listed in the 'What this replaces' sections are no longer available.
|
||||
|
||||
> Check out the [Introducing runes](https://svelte.dev/blog/runes) blog post before diving into the docs!
|
||||
|
||||
## `$state`
|
||||
|
||||
Reactive state is declared with the `$state` rune:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>
|
||||
clicks: {count}
|
||||
</button>
|
||||
```
|
||||
|
||||
You can also use `$state` in class fields (whether public or private):
|
||||
|
||||
```js
|
||||
// @errors: 7006 2554
|
||||
class Todo {
|
||||
done = $state(false);
|
||||
text = $state();
|
||||
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> In this example, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields
|
||||
|
||||
Only plain objects and arrays [are made deeply reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21DviPOwZY3jVpZEtIqUBz9e-UUt9BTj7M784bdmZ21wciq48xsPyGr2MF7Jhl9-kXEKxrCoqNLQS2TOqqgPbWd7cgggU3TgCFCAw-RekJ-3Et4lvByEq-drbe_dlsPichZcFYZrT6amQto2pXw5FO88FUYtG90gUfYi3zvWrYL75vxL57zfA07_zfr23k1vjtt-aZ0bQTcbrDL5ZifZcAxKeS8lzDc8X0xDhJ2ItdbX1jlOZMb9VnjyCoKCfMpfwG975NFVwEAAA==) by wrapping them with [`Proxies`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy):
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let numbers = $state([1, 2, 3]);
|
||||
</script>
|
||||
|
||||
<button onclick={() => numbers.push(numbers.length + 1)}>
|
||||
push
|
||||
</button>
|
||||
|
||||
<button onclick={() => numbers.pop()}> pop </button>
|
||||
|
||||
<p>
|
||||
{numbers.join(' + ') || 0}
|
||||
=
|
||||
{numbers.reduce((a, b) => a + b, 0)}
|
||||
</p>
|
||||
```
|
||||
|
||||
### What this replaces
|
||||
|
||||
In non-runes mode, a `let` declaration is treated as reactive state if it is updated at some point. Unlike `$state(...)`, which works anywhere in your app, `let` only behaves this way at the top level of a component.
|
||||
|
||||
## `$state.raw`
|
||||
|
||||
State declared with `$state.raw` cannot be mutated; it can only be _reassigned_. In other words, rather than assigning to a property of an object, or using an array method like `push`, replace the object or array altogether if you'd like to update it:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- let numbers = $state([1, 2, 3]);
|
||||
+ let numbers = $state.raw([1, 2, 3]);
|
||||
</script>
|
||||
|
||||
-<button onclick={() => numbers.push(numbers.length + 1)}>
|
||||
+<button onclick={() => numbers = [...numbers, numbers.length + 1]}>
|
||||
push
|
||||
</button>
|
||||
|
||||
-<button onclick={() => numbers.pop()}> pop </button>
|
||||
+<button onclick={() => numbers = numbers.slice(0, -1)}> pop </button>
|
||||
|
||||
<p>
|
||||
{numbers.join(' + ') || 0}
|
||||
=
|
||||
{numbers.reduce((a, b) => a + b, 0)}
|
||||
</p>
|
||||
```
|
||||
|
||||
This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can _contain_ reactive state (for example, a raw array of reactive objects).
|
||||
|
||||
## `$state.snapshot`
|
||||
|
||||
To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let counter = $state({ count: 0 });
|
||||
|
||||
function onclick() {
|
||||
// Will log `{ count: ... }` rather than `Proxy { ... }`
|
||||
console.log($state.snapshot(counter));
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`.
|
||||
|
||||
## `$derived`
|
||||
|
||||
Derived state is declared with the `$derived` rune:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
let count = $state(0);
|
||||
+ let doubled = $derived(count * 2);
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>
|
||||
{doubled}
|
||||
</button>
|
||||
|
||||
+<p>{count} doubled is {doubled}</p>
|
||||
```
|
||||
|
||||
The expression inside `$derived(...)` should be free of side-effects. Svelte will disallow state changes (e.g. `count++`) inside derived expressions.
|
||||
|
||||
As with `$state`, you can mark class fields as `$derived`.
|
||||
|
||||
### What this replaces
|
||||
|
||||
If the value of a reactive variable is being computed it should be replaced with `$derived` whether it previously took the form of `$: double = count * 2` or `$: { double = count * 2; }` There are some important differences to be aware of:
|
||||
|
||||
- With the `$derived` rune, the value of `double` is always current (for example if you update `count` then immediately `console.log(double)`). With `$:` declarations, values are not updated until right before Svelte updates the DOM
|
||||
- In non-runes mode, Svelte determines the dependencies of `double` by statically analysing the `count * 2` expression. If you refactor it...
|
||||
```js
|
||||
// @errors: 2304
|
||||
const doubleCount = () => count * 2;
|
||||
$: double = doubleCount();
|
||||
```
|
||||
...that dependency information is lost, and `double` will no longer update when `count` changes. With runes, dependencies are instead tracked at runtime.
|
||||
- In non-runes mode, reactive statements are ordered _topologically_, meaning that in a case like this...
|
||||
```js
|
||||
// @errors: 2304
|
||||
$: triple = double + count;
|
||||
$: double = count * 2;
|
||||
```
|
||||
...`double` will be calculated first despite the source order. In runes mode, `triple` cannot reference `double` before it has been declared.
|
||||
|
||||
## `$derived.by`
|
||||
|
||||
Sometimes you need to create complex derivations that don't fit inside a short expression. In these cases, you can use `$derived.by` which accepts a function as its argument.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let numbers = $state([1, 2, 3]);
|
||||
let total = $derived.by(() => {
|
||||
let total = 0;
|
||||
for (const n of numbers) {
|
||||
total += n;
|
||||
}
|
||||
return total;
|
||||
});
|
||||
</script>
|
||||
|
||||
<button onclick={() => numbers.push(numbers.length + 1)}>
|
||||
{numbers.join(' + ')} = {total}
|
||||
</button>
|
||||
```
|
||||
|
||||
In essence, `$derived(expression)` is equivalent to `$derived.by(() => expression)`.
|
||||
|
||||
## `$effect`
|
||||
|
||||
To run _side-effects_ when the component is mounted to the DOM, and when values change, we can use the `$effect` rune ([demo](/#H4sIAAAAAAAAE31T24rbMBD9lUG7kAQ2sbdlX7xOYNk_aB_rQhRpbAsU2UiTW0P-vbrYubSlYGzmzMzROTPymdVKo2PFjzMzfIusYB99z14YnfoQuD1qQh-7bmdFQEonrOppVZmKNBI49QthCc-OOOH0LZ-9jxnR6c7eUpOnuv6KeT5JFdcqbvbcBcgDz1jXKGg6ncFyBedYR6IzLrAZwiN5vtSxaJA-EzadfJEjKw11C6GR22-BLH8B_wxdByWpvUYtqqal2XB6RVkG1CoHB6U1WJzbnYFDiwb3aGEdDa3Bm1oH12sQLTcNPp7r56m_00mHocSG97_zd7ICUXonA5fwKbPbkE2ZtMJGGVkEdctzQi4QzSwr9prnFYNk5hpmqVuqPQjNnfOJoMF22lUsrq_UfIN6lfSVyvQ7grB3X2mjMZYO3XO9w-U5iLx42qg29md3BP_ni5P4gy9ikTBlHxjLzAtPDlyYZmRdjAbGq7HprEQ7p64v4LU_guu0kvAkhBim3nMplWl8FreQD-CW20aZR0wq12t-KqDWeBywhvexKC3memmDwlHAv9q4Vo2ZK8KtK0CgX7u9J8wXbzdKv-nRnfF_2baTqlYoWUF2h5efl9-n0O6koAMAAA==)):
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let size = $state(50);
|
||||
let color = $state('#ff3e00');
|
||||
|
||||
let canvas;
|
||||
|
||||
$effect(() => {
|
||||
const context = canvas.getContext('2d');
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// this will re-run whenever `color` or `size` change
|
||||
context.fillStyle = color;
|
||||
context.fillRect(0, 0, size, size);
|
||||
});
|
||||
</script>
|
||||
|
||||
<canvas bind:this={canvas} width="100" height="100" />
|
||||
```
|
||||
|
||||
The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
|
||||
|
||||
Values that are read asynchronously — after an `await` or inside a `setTimeout`, for example — will _not_ be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/#H4sIAAAAAAAAE31T24rbMBD9lUG7kCxsbG_LvrhOoPQP2r7VhSjy2BbIspHGuTT436tLnMtSCiaOzpw5M2dGPrNaKrQs_3VmmnfIcvZ1GNgro9PgD3aPitCdbT8a4ZHCCiMH2pS6JIUEVv5BWMOzJU64fM9evswR0ave3EKLp7r-jFm2iIwri-s9tx5ywDPWNQpaLl9gvYFz4JHotfVqmvBITi9mJA3St4gtF5-qWZUuvEQo5Oa7F8tewT2XrIOsqL2eWpRNS7eGSkpToFZaOEilwODKjBoOLWrco4FtsLQF0XLdoE2S5LGmm6X6QSflBxKod8IW6afssB8_uAslndJuJNA9hWKw9VO91pmJ92XunHlu_J1nMDk8_p_8q0hvO9NFtA47qavcW12fIzJBmM26ZG9ZVjKIs7ke05hdyT0Ixa11Ad-P6ZUtWbgNheI7VJvYQiH14Bz5a-SYxvtwIqHonqsR12ff8ORkQ-chP70T-L9eGO4HvYAFwRh9UCxS13h0YP2CgmoyG5h3setNhWZF_ZDD23AE2ytZwZMQ4jLYgVeV1I2LYgfZBey4aaR-xCppB8VPOdQKjxes4UMgxcVcvwHf4dzAv9K4ko1eScLO5iDQXQFzL5gl7zdJt-nZnXYfbddXspZYsZzMiNPv6S8Bl41G7wMAAA==)):
|
||||
|
||||
```ts
|
||||
// @filename: index.ts
|
||||
declare let canvas: {
|
||||
width: number;
|
||||
height: number;
|
||||
getContext(
|
||||
type: '2d',
|
||||
options?: CanvasRenderingContext2DSettings
|
||||
): CanvasRenderingContext2D;
|
||||
};
|
||||
declare let color: string;
|
||||
declare let size: number;
|
||||
|
||||
// ---cut---
|
||||
$effect(() => {
|
||||
const context = canvas.getContext('2d');
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// this will re-run whenever `color` changes...
|
||||
context.fillStyle = color;
|
||||
|
||||
setTimeout(() => {
|
||||
// ...but not when `size` changes
|
||||
context.fillRect(0, 0, size, size);
|
||||
}, 0);
|
||||
});
|
||||
```
|
||||
|
||||
An effect only reruns when the object it reads changes, not when a property inside it changes. (If you want to observe changes _inside_ an object at dev time, you can use [`$inspect`](#$inspect).)
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let state = $state({ value: 0 });
|
||||
let derived = $derived({ value: state.value * 2 });
|
||||
|
||||
// this will run once, because `state` is never reassigned (only mutated)
|
||||
$effect(() => {
|
||||
state;
|
||||
});
|
||||
|
||||
// this will run whenever `state.value` changes...
|
||||
$effect(() => {
|
||||
state.value;
|
||||
});
|
||||
|
||||
// ...and so will this, because `derived` is a new object each time
|
||||
$effect(() => {
|
||||
derived;
|
||||
});
|
||||
</script>
|
||||
|
||||
<button onclick={() => (state.value += 1)}>
|
||||
{state.value}
|
||||
</button>
|
||||
|
||||
<p>{state.value} doubled is {derived.value}</p>
|
||||
```
|
||||
|
||||
An effect only depends on the values that it read the last time it ran. If `a` is true, changes to `b` will [not cause this effect to rerun](/#H4sIAAAAAAAAE3WQ0WrDMAxFf0U1hTow1vcsMfQ7lj3YjlxEXTvEymC4_vfFC6Ewtidxde8RkrJw5DGJ9j2LoO8oWnGZJvEi-GuqIn2iZ1x1istsa6dLdqaJ1RAG9sigoYdjYs0onfYJm7fdMX85q3dE59CylA30CnJtDWxjSNHjq49XeZqXEChcT9usLUAOpIbHA0yzM78oColGhDVofLS3neZSS6mqOz-XD51ZmGOAGKwne-vztk-956CL0kAJsi7decupf4l658EUZX4I8yTWt93jSI5wFC3PC5aP8g0Aje5DcQEAAA==):
|
||||
|
||||
```ts
|
||||
let a = false;
|
||||
let b = false;
|
||||
// ---cut---
|
||||
$effect(() => {
|
||||
console.log('running');
|
||||
|
||||
if (a || b) {
|
||||
console.log('inside if block');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/#H4sIAAAAAAAAE42SzW6DMBCEX2Vl5RDaVCQ9JoDUY--9lUox9lKsGBvZC1GEePcaKPnpqSe86_m0M2t6ViqNnu0_e2Z4jWzP3pqGbRhdmrHwHWrCUHvbOjF2Ei-caijLTU4aCYRtDUEKK0-ccL2NDstNrbRWHoU10t8Eu-121gTVCssSBa3XEaQZ9GMrpziGj0p5OAccCgSHwmEgJZwrNNihg6MyhK7j-gii4uYb_YyGUZ5guQwzPdL7b_U4ZNSOvp9T2B3m1rB5cLx4zMkhtc7AHz7YVCVwEFzrgosTBMuNs52SKDegaPbvWnMH8AhUXaNUIY6-hHCldQhUIcyLCFlfAuHvkCKaYk8iYevGGgy2wyyJnpy9oLwG0sjdNe2yhGhJN32HsUzi2xOapNpl_bSLIYnDeeoVLZE1YI3QSpzSfo7-8J5PKbwOmdf2jC6JZyD7HxpPaMk93aHhF6utVKVCyfbkWhy-hh9Z3o_2nQIAAA==)).
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let milliseconds = $state(1000);
|
||||
|
||||
$effect(() => {
|
||||
// This will be recreated whenever `milliseconds` changes
|
||||
const interval = setInterval(() => {
|
||||
count += 1;
|
||||
}, milliseconds);
|
||||
|
||||
return () => {
|
||||
// if a callback is provided, it will run
|
||||
// a) immediately before the effect re-runs
|
||||
// b) when the component is destroyed
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1>{count}</h1>
|
||||
|
||||
<button onclick={() => (milliseconds *= 2)}>slower</button>
|
||||
<button onclick={() => (milliseconds /= 2)}>faster</button>
|
||||
```
|
||||
|
||||
### When not to use `$effect`
|
||||
|
||||
In general, `$effect` is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let doubled = $state();
|
||||
|
||||
// don't do this!
|
||||
$effect(() => {
|
||||
doubled = count * 2;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
...do this:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let doubled = $derived(count * 2);
|
||||
</script>
|
||||
```
|
||||
|
||||
> For things that are more complicated than a simple expression like `count * 2`, you can also use [`$derived.by`](#$derived-by).
|
||||
|
||||
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRQWrDMBD8ihA5ONDG7qEXxQ70HXUPir0KgrUsrHWIMf57pXWdlFIKPe6MZmZnNUtjEYJU77N0ugOp5Jv38knS5NMQroAEcQ79ODQJKUMzWE-n2tWEQIJ60igq8VIUxw0LHhxFbBdIE2TF_s4gmG8Ea5mM9A6MgYaybC-qk5gTlDT8fg15Xo3ZbPlTti2w6ZLNQ1bmjw6uRH0G5DqldX6MjWL1qpaDdheopThb16qrxhGqmX0X0elbNbP3InKWfjH5hvKYku7u_wtKC_-aw8Q9Jk0_UgJNCOvvJHC7SGuDRz0pYRBuxxW7aK9EcXiFbr0NX4bl8cO7vrXGQisVDSMsH8sniirsuSsCAAA=)):
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let total = 100;
|
||||
let spent = $state(0);
|
||||
let left = $state(total);
|
||||
|
||||
$effect(() => {
|
||||
left = total - spent;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
spent = total - left;
|
||||
});
|
||||
</script>
|
||||
|
||||
<label>
|
||||
<input type="range" bind:value={spent} max={total} />
|
||||
{spent}/{total} spent
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="range" bind:value={left} max={total} />
|
||||
{left}/{total} left
|
||||
</label>
|
||||
```
|
||||
|
||||
Instead, use callbacks where possible ([demo](/#H4sIAAAAAAAACo1SMW6EMBD8imWluFNyQIo0HERKf13KkMKB5WTJGAsvp0OIv8deMEEJRcqdmZ1ZjzzyWiqwPP0YuRYN8JS_GcOfOA7GD_YGCsHNtu270iOZLTtp8LXQBSpAhi0KxXL2nCTngFkDGh32YFEgHJLjyiioNwTtEunoutclylaz3lSOfPceBziy0ZMFBs9HiFB0V8DoJlQP55ldfOdjTvMBRE275hcn33gv2_vWITh4e3GwzuKfNnSmxBcoKiaT2vSuG1diXvBO6CsUnJFrPpLhxFpNonzcvHdijbjnI0VNLCavRR8HlEYfvcb9O9mf_if4QuBOLqnXWD_9SrU4KJg_ggdDm5W0RokhZbWC-1LiVZiUJdELNJvqaN39raatZC2h4il2PUyf0zcIbC-7lgIAAA==)):
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let total = 100;
|
||||
let spent = $state(0);
|
||||
let left = $state(total);
|
||||
|
||||
function updateSpent(e) {
|
||||
spent = +e.target.value;
|
||||
left = total - spent;
|
||||
}
|
||||
|
||||
function updateLeft(e) {
|
||||
left = +e.target.value;
|
||||
spent = total - left;
|
||||
}
|
||||
</script>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="range"
|
||||
value={spent}
|
||||
oninput={updateSpent}
|
||||
max={total}
|
||||
/>
|
||||
{spent}/{total} spent
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="range"
|
||||
value={left}
|
||||
oninput={updateLeft}
|
||||
max={total}
|
||||
/>
|
||||
{left}/{total} left
|
||||
</label>
|
||||
```
|
||||
|
||||
If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/#H4sIAAAAAAAACpVRQW7DIBD8CkI9JFIau4deiB2p7yg9kHhtIWGMYG3Fsvh7ARs3qnrpCWZGM8MuC22lAkfZ50K16IEy-mEMPVGcTQRuAoUQsBtGe49M5e5WGrxyzVEBEhxQKFKTt7K8ZM4Z0Bi4F4cC4VAeo7JpCtooLRFz7AIzCTXC4ZgpjhZwtHpLfl3TLqvoT-vpdt_0ZMy92TllVzx8AFXx83pdKXEDlQappDZjmCUMXXNqhe6AU3KTumGppV5StCe9eNRLivekSNZNKTKbYGza0_9XFPdzTvc_257kvTJyvxodzgrWP4pkXlEjnVFiZqRV8NiW0wnDSHl-hz4RPm0p2cO390MjWwkNZWhD5Zf_BkCCa6AxAgAA)):
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let total = 100;
|
||||
let spent = $state(0);
|
||||
|
||||
let left = {
|
||||
get value() {
|
||||
return total - spent;
|
||||
},
|
||||
set value(v) {
|
||||
spent = total - v;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<label>
|
||||
<input type="range" bind:value={spent} max={total} />
|
||||
{spent}/{total} spent
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="range" bind:value={left.value} max={total} />
|
||||
{left.value}/{total} left
|
||||
</label>
|
||||
```
|
||||
|
||||
If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](functions#untrack).
|
||||
|
||||
### What this replaces
|
||||
|
||||
The portions of `$: {}` that are triggering side-effects can be replaced with `$effect` while being careful to migrate updates of reactive variables to use `$derived`. There are some important differences:
|
||||
|
||||
- Effects only run in the browser, not during server-side rendering
|
||||
- They run after the DOM has been updated, whereas `$:` statements run immediately _before_
|
||||
- You can return a cleanup function that will be called whenever the effect refires
|
||||
|
||||
Additionally, you may prefer to use effects in some places where you previously used `onMount` and `afterUpdate` (the latter of which will be deprecated in Svelte 5). There are some differences between these APIs as `$effect` should not be used to compute reactive values and will be triggered each time a referenced reactive variable changes (unless using `untrack`).
|
||||
|
||||
## `$effect.pre`
|
||||
|
||||
In rare cases, you may need to run code _before_ the DOM updates. For this we can use the `$effect.pre` rune:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { tick } from 'svelte';
|
||||
|
||||
let div = $state();
|
||||
let messages = $state([]);
|
||||
|
||||
// ...
|
||||
|
||||
$effect.pre(() => {
|
||||
if (!div) return; // not yet mounted
|
||||
|
||||
// reference `messages` array length so that this code re-runs whenever it changes
|
||||
messages.length;
|
||||
|
||||
// autoscroll when new messages are added
|
||||
if (
|
||||
div.offsetHeight + div.scrollTop >
|
||||
div.scrollHeight - 20
|
||||
) {
|
||||
tick().then(() => {
|
||||
div.scrollTo(0, div.scrollHeight);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={div}>
|
||||
{#each messages as message}
|
||||
<p>{message}</p>
|
||||
{/each}
|
||||
</div>
|
||||
```
|
||||
|
||||
Apart from the timing, `$effect.pre` works exactly like [`$effect`](#$effect) — refer to its documentation for more info.
|
||||
|
||||
### What this replaces
|
||||
|
||||
Previously, you would have used `beforeUpdate`, which — like `afterUpdate` — is deprecated in Svelte 5.
|
||||
|
||||
## `$effect.tracking`
|
||||
|
||||
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/#H4sIAAAAAAAACn3PQWrDMBAF0KtMRSA2xPFeUQU5R92FUUZBVB4N1rgQjO9eKSlkEcjyfz6PmVX5EDEr_bUqGidUWp2Z1UHJjWvIvxgFS85pmV1tTHZzYLEDDeIS5RTxGNO12QcClyZOhCSQURbW-wPs0Ht0cpR5dD-Brk3bnqDvwY8xYzGK8j9pmhY-Lay1eqUfm3eizEsFZWtPA5n-eSYZtkUQnDiOghrWV2IzPVswH113d6DrbHl6SpfgA16UruX2vf0BWo7W2y8BAAA=)):
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
console.log('in component setup:', $effect.tracking()); // false
|
||||
|
||||
$effect(() => {
|
||||
console.log('in effect:', $effect.tracking()); // true
|
||||
});
|
||||
</script>
|
||||
|
||||
<p>in template: {$effect.tracking()}</p> <!-- true -->
|
||||
```
|
||||
|
||||
This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects.
|
||||
|
||||
## `$effect.root`
|
||||
|
||||
The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for
|
||||
nested effects that you want to manually control. This rune also allows for creation of effects outside of the component initialisation phase.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
const cleanup = $effect.root(() => {
|
||||
$effect(() => {
|
||||
console.log(count);
|
||||
});
|
||||
|
||||
return () => {
|
||||
console.log('effect root cleanup');
|
||||
};
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## `$props`
|
||||
|
||||
To declare component props, use the `$props` rune:
|
||||
|
||||
```js
|
||||
let { optionalProp = 42, requiredProp } = $props();
|
||||
```
|
||||
|
||||
You can use familiar destructuring syntax to rename props, in cases where you need to (for example) use a reserved word like `catch` in `<MyComponent catch={22} />`:
|
||||
|
||||
```js
|
||||
let { catch: theCatch } = $props();
|
||||
```
|
||||
|
||||
To get all properties, use rest syntax:
|
||||
|
||||
```js
|
||||
let { a, b, c, ...everythingElse } = $props();
|
||||
```
|
||||
|
||||
You can also use an identifier:
|
||||
|
||||
```js
|
||||
let props = $props();
|
||||
```
|
||||
|
||||
If you're using TypeScript, you can declare the prop types:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
interface MyProps {
|
||||
required: string;
|
||||
optional?: number;
|
||||
partOfEverythingElse?: boolean;
|
||||
};
|
||||
|
||||
let { required, optional, ...everythingElse }: MyProps = $props();
|
||||
```
|
||||
|
||||
> In an earlier preview, `$props()` took a type argument. This caused bugs, since in a case like this...
|
||||
>
|
||||
> ```ts
|
||||
> // @errors: 2558
|
||||
> let { x = 42 } = $props<{ x?: string }>();
|
||||
> ```
|
||||
>
|
||||
> ...TypeScript [widens the type](https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwBIAHGHIgZwB4AVeAXnilQE8A+ACgEoAueagbgBQgiCAzwA3vAAe9eABYATPAC+c4qQqUp03uQwwsqAOaqOnIfCsB6a-AB6AfiA) of `x` to be `string | number`, instead of erroring.
|
||||
|
||||
If you're using JavaScript, you can declare the prop types using JSDoc:
|
||||
|
||||
```js
|
||||
/** @type {{ x: string }} */
|
||||
let { x } = $props();
|
||||
|
||||
// or use @typedef if you want to document the properties:
|
||||
|
||||
/**
|
||||
* @typedef {Object} MyProps
|
||||
* @property {string} y Some documentation
|
||||
*/
|
||||
|
||||
/** @type {MyProps} */
|
||||
let { y } = $props();
|
||||
```
|
||||
|
||||
By default props are treated as readonly, meaning reassignments will not propagate upwards and mutations will result in a warning at runtime in development mode. You will also get a runtime error when trying to `bind:` to a readonly prop in a parent component. To declare props as bindable, use [`$bindable()`](#$bindable).
|
||||
|
||||
### What this replaces
|
||||
|
||||
`$props` replaces the `export let` and `export { x as y }` syntax for declaring props. It also replaces `$$props` and `$$restProps`, and the little-known `interface $$Props {...}` construct.
|
||||
|
||||
Note that you can still use `export const` and `export function` to expose things to users of your component (if they're using `bind:this`, for example).
|
||||
|
||||
## `$bindable`
|
||||
|
||||
To declare props as bindable, use `$bindable()`. Besides using them as regular props, the parent can (_can_, not _must_) then also `bind:` to them.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { bindableProp = $bindable() } = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
You can pass an argument to `$bindable()`. This argument is used as a fallback value when the property is `undefined`.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { bindableProp = $bindable('fallback') } = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
Note that the parent is not allowed to pass `undefined` to a property with a fallback if it `bind:`s to that property.
|
||||
|
||||
## `$inspect`
|
||||
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its
|
||||
argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object
|
||||
or array using [fine-grained reactivity](/docs/fine-grained-reactivity) will cause it to re-fire. ([Demo:](/#H4sIAAAAAAAACkWQ0YqDQAxFfyUMhSotdZ-tCvu431AXtGOqQ2NmmMm0LOK_r7Utfby5JzeXTOpiCIPKT5PidkSVq2_n1F7Jn3uIcEMSXHSw0evHpAjaGydVzbUQCmgbWaCETZBWMPlKj29nxBDaHj_edkAiu12JhdkYDg61JGvE_s2nR8gyuBuiJZuDJTyQ7eE-IEOzog1YD80Lb0APLfdYc5F9qnFxjiKWwbImo6_llKRQVs-2u91c_bD2OCJLkT3JZasw7KLA2XCX31qKWE6vIzNk1fKE0XbmYrBTufiI8-_8D2cUWBA_AQAA))
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let message = $state('hello');
|
||||
|
||||
$inspect(count, message); // will console.log when `count` or `message` change
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
<input bind:value={message} />
|
||||
```
|
||||
|
||||
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`, all following arguments are the values passed to `$inspect`. [Demo:](/#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA)
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
$inspect(count).with((type, count) => {
|
||||
if (type === 'update') {
|
||||
debugger; // or `console.trace`, or whatever you want
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
```
|
||||
|
||||
A convenient way to find the origin of some change is to pass `console.trace` to `with`:
|
||||
|
||||
```js
|
||||
// @errors: 2304
|
||||
$inspect(stuff).with(console.trace);
|
||||
```
|
||||
|
||||
> `$inspect` only works during development.
|
||||
|
||||
## `$host`
|
||||
|
||||
Retrieves the `this` reference of the custom element that contains this component. Example:
|
||||
|
||||
```svelte
|
||||
<svelte:options customElement="my-element" />
|
||||
|
||||
<script>
|
||||
function greet(greeting) {
|
||||
$host().dispatchEvent(
|
||||
new CustomEvent('greeting', { detail: greeting })
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => greet('hello')}>say hello</button>
|
||||
```
|
||||
|
||||
> Only available inside custom element components, and only on the client-side
|
||||
|
||||
## How to opt in
|
||||
|
||||
Current Svelte code will continue to work without any adjustments. Components using the Svelte 4 syntax can use components using runes and vice versa.
|
||||
|
||||
The easiest way to opt in to runes mode is to just start using them in your code. Alternatively, you can force the compiler into runes or non-runes mode either on a per-component basis...
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```svelte
|
||||
<!--- file: YourComponent.svelte --->
|
||||
<!-- this can be `true` or `false` -->
|
||||
<svelte:options runes={true} />
|
||||
```
|
||||
|
||||
...or for your entire app:
|
||||
|
||||
```js
|
||||
/// file: svelte.config.js
|
||||
export default {
|
||||
compilerOptions: {
|
||||
runes: true
|
||||
}
|
||||
};
|
||||
```
|
@ -1,269 +0,0 @@
|
||||
---
|
||||
title: Snippets
|
||||
---
|
||||
|
||||
Snippets, and _render tags_, are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like [this](/#H4sIAAAAAAAAE5VUYW-kIBD9K8Tmsm2yXXRzvQ-s3eR-R-0HqqOQKhAZb9sz_vdDkV1t000vRmHewMx7w2AflbIGG7GnPlK8gYhFv42JthG-m9Gwf6BGcLbVXZuPSGrzVho8ZirDGpDIhldgySN5GpEMez9kaNuckY1ANJZRamRuu2ZnhEZt6a84pvs43mzD4pMsUDDi8DMkQFYCGdkvsJwblFq5uCik9bmJ4JZwUkv1eoknWigX2eGNN6aGXa6bjV8ybP-X7sM36T58SVcrIIV2xVIaA41xeD5kKqWXuqpUJEefOqVuOkL9DfBchGrzWfu0vb-RpTd3o-zBR045Ga3HfuE5BmJpKauuhbPtENlUF2sqR9jqpsPSxWsMrlngyj3VJiyYjJXb1-lMa7IWC-iSk2M5Zzh-SJjShe-siq5kpZRPs55BbSGU5YPyte4vVV_VfFXxVb10dSLf17pS2lM5HnpPxw4Zpv6x-F57p0jI3OKlVnhv5V9wPQrNYQQ9D_f6aGHlC89fq1Z3qmDkJCTCweOGF4VUFSPJvD_DhreVdA0eu8ehJJ5x91dBaBkpWm3ureCFPt3uzRv56d4kdp-2euG38XZ6dsnd3ZmPG9yRBCrzRUvi-MccOdwz3qE-fOZ7AwAhlrtTUx3c76vRhSwlFBHDtoPhefgHX3dM0PkEAAA=)...
|
||||
|
||||
```svelte
|
||||
{#each images as image}
|
||||
{#if image.href}
|
||||
<a href={image.href}>
|
||||
<figure>
|
||||
<img
|
||||
src={image.src}
|
||||
alt={image.caption}
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
/>
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
</a>
|
||||
{:else}
|
||||
<figure>
|
||||
<img
|
||||
src={image.src}
|
||||
alt={image.caption}
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
/>
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
...you can write [this](/#H4sIAAAAAAAAE5VUYW-bMBD9KxbRlERKY4jWfSA02n5H6QcXDmwVbMs-lnaI_z6D7TTt1moTAnPvzvfenQ_GpBEd2CS_HxPJekjy5IfWyS7BFz0b9id0CM62ajDVjBS2MkLjqZQldoBE9KwFS-7I_YyUOPqlRGuqnKw5orY5pVpUduj3mitUln5LU3pI0_UuBp9FjTwnDr9AHETLMSeHK6xiGoWSLi9yYT034cwSRjohn17zcQPNFTs8s153sK9Uv_Yh0-5_5d7-o9zbD-UqCaRWrllSYZQxLw_HUhb0ta-y4NnJUxfUvc7QuLJSaO0a3oh2MLBZat8u-wsPnXzKQvTtVVF34xK5d69ThFmHEQ4SpzeVRediTG8rjD5vBSeN3E5JyHh6R1DQK9-iml5kjzQUN_lSgVU8DhYLx7wwjSvRkMDvTjiwF4zM1kXZ7DlF1eN3A7IG85e-zRrYEjjm0FkI4Cc7Ripm0pHOChexhcWXzreeZyRMU6Mk3ljxC9w4QH-cQZ_b3T5pjHxk1VNr1CDrnJy5QDh6XLO6FrLNSRb2l9gz0wo3S6m7HErSgLsPGMHkpDZK31jOanXeHPQz-eruLHUP0z6yTbpbrn223V70uMXNSpQSZjpL0y8hcxxpNqA6_ql3BQAxlxvfpQ_uT9GrWjQC6iRHM8D0MP0GQsIi92QEAAA=):
|
||||
|
||||
```diff
|
||||
+{#snippet figure(image)}
|
||||
<figure>
|
||||
<img
|
||||
src={image.src}
|
||||
alt={image.caption}
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
/>
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
+{/snippet}
|
||||
|
||||
{#each images as image}
|
||||
{#if image.href}
|
||||
<a href={image.href}>
|
||||
+ {@render figure(image)}
|
||||
</a>
|
||||
{:else}
|
||||
+ {@render figure(image)}
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
Snippet parameters can be destructured ([demo](/#H4sIAAAAAAAAE5VTYW-bMBD9KyeiKYlEY4jWfSAk2n5H6QcXDmwVbMs2SzuL_z6DTRqp2rQJ2Ycfd_ced2eXtLxHkxRPLhF0wKRIfiiVpIl9V_PB_MTeoj8bOep6RkpTa67spRKV7dECH2iHBs7wNCOVdcFU1ui6gC2zVpmCEMVrMw4HxaSVhnzLMnLMsm26Ol95Y1kBHr9BDHnHbAHHO6ymynIpfF7LuAncwKgBCj0Xrx_5mMb2jh3f6KB6PNRy2AaXKf1fuY__KPfxj3KlQGikL5aQdpUxm-dTJUryUVdRsvwSqEviX2fIbYzgSvmCt7wbNe4ceMUpRIoUFkkpBBkw7ZfMZXC-BLKSDx3Q3p5djJrA-SR-X4K9DdHT6u-jo-flFlKSO3ThIDcSR6LIKUhGWrN1QGhs16LLbXgbjoe5U1PkozCfzu7uy2WtpfuuUTSo1_9ffPZrJKGLoyuwNxjBv0Q4wmdSR2aFi9jS2Pc-FIrlEKeilcI-GP4LfVtxOM1gyO1XSLp6vtD6tdNyFE0BV8YtngKuaNNw0RWQx_jKDlR33M9E5h-PQhZxfxEt6gIaLdWDYbSR191RvcFXv_LMb7p7obssXZ5Dvt_f9HgzdzZKibOZZ9mXmHkdTTpaefqsd4OIay4_hksd_I0fZMNbjk1SWD3i9Dz9BpdEPu8sBAAA)):
|
||||
|
||||
```svelte
|
||||
{#snippet figure({ src, caption, width, height })}
|
||||
<figure>
|
||||
<img alt={caption} {src} {width} {height} />
|
||||
<figcaption>{caption}</figcaption>
|
||||
</figure>
|
||||
{/snippet}
|
||||
```
|
||||
|
||||
Like function declarations, snippets can have an arbitrary number of parameters, which can have default values. You cannot use rest parameters however.
|
||||
|
||||
## Snippet scope
|
||||
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks ([demo](/#H4sIAAAAAAAAE12P0QrCMAxFfyWrwhSEvc8p-h1OcG5RC10bmkyQ0n-3HQPBx3vCPUmCemiDrOpLULYbUdXqTKR2Sj6UA7_RCKbMbvJ9Jg33XpMcW9uKQYEAIzJ3T4QD3LSUDE-PnYA4YET4uOkGMc3W5B3xZrtvbVP9HDas2GqiZHqhMW6Tr9jGbG_oOCMImcUCwrIpFk1FqRyqpRpn0cmjHdAvnrIzuscyq_4nd3dPPD01ukE_NA6qFj9hvMYvGjJADw8BAAA=))...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { message = `it's great to see you!` } = $props();
|
||||
</script>
|
||||
|
||||
{#snippet hello(name)}
|
||||
<p>hello {name}! {message}!</p>
|
||||
{/snippet}
|
||||
|
||||
{@render hello('alice')}
|
||||
{@render hello('bob')}
|
||||
```
|
||||
|
||||
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
|
||||
|
||||
```svelte
|
||||
<div>
|
||||
{#snippet x()}
|
||||
{#snippet y()}...{/snippet}
|
||||
|
||||
<!-- this is fine -->
|
||||
{@render y()}
|
||||
{/snippet}
|
||||
|
||||
<!-- this will error, as `y` is not in scope -->
|
||||
{@render y()}
|
||||
</div>
|
||||
|
||||
<!-- this will also error, as `x` is not in scope -->
|
||||
{@render x()}
|
||||
```
|
||||
|
||||
Snippets can reference themselves and each other ([demo](/#H4sIAAAAAAAAE2WPTQqDMBCFrxLiRqH1Zysi7TlqF1YnENBJSGJLCYGeo5tesUeosfYH3c2bee_jjaWMd6BpfrAU6x5oTvdS0g01V-mFPkNnYNRaDKrxGxto5FKCIaeu1kYwFkauwsoUWtZYPh_3W5FMY4U2mb3egL9kIwY0rbhgiO-sDTgjSEqSTvIDs-jiOP7i_MHuFGAL6p9BtiSbOTl0GtzCuihqE87cqtyam6WRGz_vRcsZh5bmRg3gju4Fptq_kzQBAAA=)):
|
||||
|
||||
```svelte
|
||||
{#snippet blastoff()}
|
||||
<span>🚀</span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet countdown(n)}
|
||||
{#if n > 0}
|
||||
<span>{n}...</span>
|
||||
{@render countdown(n - 1)}
|
||||
{:else}
|
||||
{@render blastoff()}
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{@render countdown(10)}
|
||||
```
|
||||
|
||||
## Passing snippets to components
|
||||
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/#H4sIAAAAAAAAE41SwY6bMBD9lRGplKQlYRMpF5ZF7T_0ttmDwSZYJbZrT9pGlv-9g4Fkk-xhxYV5vHlvhjc-aWQnXJK_-kSxo0jy5IcxSZrg2fSF-yM6FFQ7fbJ1jxSuttJguVd7lEejLcJPVnUCGquPMF9nsVoPjfNnohGx1sohMU4SHbzAa4_t0UNvmcOcGUNDzFP4jeccdikYK2v6sIWQ3lErpui5cDdPF_LmkVy3wlp5Vd5e2U_rHYSe_kYjFtl1KeVnTkljBEIrGBd2sYy8AtsyLlBk9DYhJHtTR_UbBDWybkR8NkqHWyOr_y74ZMNLz9f9AoG6ePkOJLMHLBp-xISvcPf11r0YUuMM2Ysfkgngh5XphUYKkJWU_FFz2UjBkxztSYT0cihR4LOn0tGaPrql439N-7Uh0Dl8MVYbt1jeJ1Fg7xDb_Uw2Y18YQqZ_S2U5FH1pS__dCkWMa3C0uR0pfQRTg89kE4bLLLDS_Dxy_Eywuo1TAnPAw4fqY1rvtH3W9w35ZZMgvU3jq8LhedwkguCHRhT_cMU6eVA5dKLB5wGutCWjlTOslupAxxrxceKoD2hzhe2qbmXHF1v1bbOcNCtW_zpYfVI8h5kQ4qY3mueHTlesW2C7TOEO4hcdwzgf3Nc7cZxUKKC4yuNhvIX_MlV_Xk0EAAA=)):
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 }
|
||||
];
|
||||
</script>
|
||||
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
<th>qty</th>
|
||||
<th>price</th>
|
||||
<th>total</th>
|
||||
{/snippet}
|
||||
|
||||
{#snippet row(d)}
|
||||
<td>{d.name}</td>
|
||||
<td>{d.qty}</td>
|
||||
<td>{d.price}</td>
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
|
||||
<Table data={fruits} {header} {row} />
|
||||
```
|
||||
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/#H4sIAAAAAAAAE41Sy27bMBD8lYVcwHYrW4kBXxRFaP-htzgHSqQsojLJkuu2BqF_74qUrfhxCHQRh7MzO9z1SSM74ZL8zSeKHUSSJz-MSdIET2Y4uD-iQ0Fnp4-2HpDC1VYaLHdqh_JgtEX4yapOQGP1AebrLJzWsXD-QjQi1lo5JMZRooNXeBuwHXoYLHOYM2OoiXkKv_GUwzYFY2VNFxvo0xtqxRR9F-7z04X8fE-uW2GtnJQ3E_tpvYV-oL9Ti0U2hVJFjMMZslcfW-5DWj9zShojEFrBuLCLZR_9CmzLQCwy-psw8rxBgvkNhhpZd8F8NppE7Stbq_8u-GTKS8_XQ9Keqnl5BZP1AzTYP2bDV7i7_9hLEeda0iocNJeNFDzJ0R5Fn142JzA-uzsdBfLhldPxPdMhIPS0H1-M1cYtlnejwdBDfBXZjHXTFOg4BhuOtvTfrVDEmAZG2ew5ezYV-Ew2fVzVAivNTyPHzwSr29AlMAe8f6g-zuWDts-GusAmdBSkv3P7qnB4GpMEEHwsRPEPV6yTe5VDJxp8iXClLRmtnGG1VHva3oCPHQd9QJsrbFd1Kzu-2Khvz8uzZsXqX3urj4rnMBNCXNUG83zf6Yp1C2yXKdxA_KJjGOfRfb0Vh7MKDShEuV-M9_4_nq6svF4EAAA=)):
|
||||
|
||||
```svelte
|
||||
<!-- this is semantically the same as the above -->
|
||||
<Table data={fruits}>
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
<th>qty</th>
|
||||
<th>price</th>
|
||||
<th>total</th>
|
||||
{/snippet}
|
||||
|
||||
{#snippet row(d)}
|
||||
<td>{d.name}</td>
|
||||
<td>{d.qty}</td>
|
||||
<td>{d.price}</td>
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
</Table>
|
||||
```
|
||||
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/#H4sIAAAAAAAAE41S247aMBD9lVFYCegGsiDxks1G7T_0bdkHJ3aI1cR27aEtsvzvtZ0LZeGhiiJ5js-cmTMemzS8YybJ320iSM-SPPmmVJImeFEhML9Yh8zHRp51HZDC1JorLI_iiLxXUiN8J1XHoNGyh-U2i9F2SFy-epon1lIY9IwzRwNv8B6wI1oIJXNYEqV8E8sUfuIlh0MKSvPaX-zBpZ-oFRH-m7m7l5m8uyfXLdOaX5X3V_bL9gAu0D98i0V2NSWKwQ4lSN7s0LKLbgtsyxgXmT9NiBe-iaP-DYISSTcj4bcLI7hSDEHL3yu6dkPfBdLS0m1o3nk-LW9gX-gBGss9ZsMXuLu32VjZBdfRaelft5eUN5zRJEd9Zi6dlyEy_ncdOm_IxsGlULe8o5qJNFgE5x_9SWmpzGp9N2-MXQxz4c2cOQ-lZWQyF0Jd2q_-mjI9U1fr4FBPE8iuKTbjjRt2sMBK0svIsQtG6jb2CsQAdQ_1x9f5R9tmIS-yPToK-tNkQRQGL6ObCIIdEpH9wQ3p-Enk0LEGXwe4ktoX2hhFai5Ofi0jPnYc9QF1LrDdRK-rvXjerSfNitQ_TlqeBc1hwRi7yY3F81MnK9KtsF2n8Amis44ilA7VtwfWTyr-kaKV-_X4cH8BTOhfRzcEAAA=)):
|
||||
|
||||
```diff
|
||||
<Table data={fruits}>
|
||||
- {#snippet header()}
|
||||
- <th>fruit</th>
|
||||
- <th>qty</th>
|
||||
- <th>price</th>
|
||||
- <th>total</th>
|
||||
- {/snippet}
|
||||
+ <th>fruit</th>
|
||||
+ <th>qty</th>
|
||||
+ <th>price</th>
|
||||
+ <th>total</th>
|
||||
|
||||
<!-- ... -->
|
||||
</Table>
|
||||
```
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- let { data, header, row } = $props();
|
||||
+ let { data, children, row } = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
- {#if header}
|
||||
+ {#if children}
|
||||
<thead>
|
||||
- <tr>{@render header()}</tr>
|
||||
+ <tr>{@render children()}</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<!-- ... -->
|
||||
</table>
|
||||
```
|
||||
|
||||
> Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
|
||||
|
||||
## Typing snippets
|
||||
|
||||
Snippets implement the `Snippet` interface imported from `'svelte'`:
|
||||
|
||||
```diff
|
||||
-<script>
|
||||
- let { data, children, row } = $props();
|
||||
+<script lang="ts">
|
||||
+ import type { Snippet } from 'svelte';
|
||||
+
|
||||
+ let { data, children, row }: {
|
||||
+ data: any[];
|
||||
+ children: Snippet;
|
||||
+ row: Snippet<[any]>;
|
||||
+ } = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters.
|
||||
|
||||
We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type:
|
||||
|
||||
```diff
|
||||
-<script lang="ts">
|
||||
+<script lang="ts" generics="T">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { data, children, row }: {
|
||||
- data: any[];
|
||||
+ data: T[];
|
||||
children: Snippet;
|
||||
- row: Snippet<[any]>;
|
||||
+ row: Snippet<[T]>;
|
||||
} = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
## Creating snippets programmatically
|
||||
|
||||
In advanced scenarios, you may need to create a snippet programmatically. For this, you can use [`createRawSnippet`](/docs/imports#svelte-createrawsnippet)
|
||||
|
||||
## Snippets and slots
|
||||
|
||||
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/special-elements#slot). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5.
|
||||
|
||||
They continue to work, however, and you can mix and match snippets and slots in your components.
|
||||
|
||||
When using custom elements, you should still use `<slot />` like before. In a future version, when Svelte removes its internal version of slots, it will leave those slots as-is, i.e. output a regular DOM tag instead of transforming it.
|
@ -1,205 +0,0 @@
|
||||
---
|
||||
title: Event handlers
|
||||
---
|
||||
|
||||
Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
let count = $state(0);
|
||||
</script>
|
||||
|
||||
-<button on:click={() => count++}>
|
||||
+<button onclick={() => count++}>
|
||||
clicks: {count}
|
||||
</button>
|
||||
```
|
||||
|
||||
Since they're just properties, you can use the normal shorthand syntax...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
function onclick() {
|
||||
count++;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button {onclick}>
|
||||
clicks: {count}
|
||||
</button>
|
||||
```
|
||||
|
||||
...though when using a named event handler function it's usually better to use a more descriptive name.
|
||||
|
||||
Traditional `on:` event handlers will continue to work, but are deprecated in Svelte 5.
|
||||
|
||||
## Component events
|
||||
|
||||
In Svelte 4, components could emit events by creating a dispatcher with [`createEventDispatcher`](https://svelte.dev/docs/svelte#createeventdispatcher).
|
||||
|
||||
This function is deprecated in Svelte 5. Instead, components should accept _callback props_ - which means you then pass functions as properties to these components ([demo](/#H4sIAAAAAAAACo1US27bMBC9yoBtELu2ZDmAG0CRhPYG3VddyPIwIUKRgjiOkwrcd9VFL5BV75cjFKQo2e5_IQnzeW-GM3zqGRcSDUs_9kxVDbKUvW9btmT01DrDPKAkZEtm9L6rnSczdSdaKkpVkmha3RF82Dct8E43cBmvnBEPsMsbl-QeiQRGfEbI4bWhinC23sxvxsh23xk6hnglDfqoKonvVU1CK-jQIM3m0HtOCmzrzVCDRg4P9j5bqmx1bFZlrjPfteKyIsz7WasP2M0hL85YFzn4QGAWHGbeX8D1Zj41S90-1LHuvcM_kp4QJPNhDNFpCUew8i32rwQfCnjObLsn0gq0qqWo7_Pez8AWCg-wraTUWmWrIcevIzNtpaCWlTF5ybZaNyUrXp6_fc9WLlKUqk9RGrS_SR7oSgaGniTmJTN1JTGFPomTNbzxbduSFcORXp6_fvEkE_FKcOun7PE-zRcIM2i1EW6NKXDxiLswWomcUkiCRbo9Ggexo7sU1klyETx3KG7v6MzFtaLIdea9D4eRCB8pqqS4VSnUqGhapRQKo4nnZmxNuJQIH1CRSUFpNV0g94nDbMajUFep8TB-SJDEV-YcoXUzpldKNNWQ7d1JvDHAdXeout0Z6t09PvGuatDAKT65gB7CMpL4LdjBfbU5819vxoAbz0lkcA9aCJthS9boneACdyx119guJ_E7jfyv-p10ewhqWkJQAFin5LbTrZkdJe5v-1HiXvzn6vz5rs-8hAJ7EJUtgn1y7f8ADN1MwGD_G-gBUWSLaModfnA-kELvvxb-Bl8sbLGY4L_O-5P9ATwVcA54BQAA)):
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import Pump from './Pump.svelte';
|
||||
|
||||
let size = $state(15);
|
||||
let burst = $state(false);
|
||||
|
||||
function reset() {
|
||||
size = 15;
|
||||
burst = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Pump
|
||||
inflate={(power) => {
|
||||
size += power;
|
||||
if (size > 75) burst = true;
|
||||
}}
|
||||
deflate={(power) => {
|
||||
if (size > 0) size -= power;
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if burst}
|
||||
<button onclick={reset}>new balloon</button>
|
||||
<span class="boom">💥</span>
|
||||
{:else}
|
||||
<span class="balloon" style="scale: {0.01 * size}">
|
||||
🎈
|
||||
</span>
|
||||
{/if}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { inflate, deflate } = $props();
|
||||
let power = $state(5);
|
||||
</script>
|
||||
|
||||
<button onclick={() => inflate(power)}>inflate</button>
|
||||
<button onclick={() => deflate(power)}>deflate</button>
|
||||
<button onclick={() => power--}>-</button>
|
||||
Pump power: {power}
|
||||
<button onclick={() => power++}>+</button>
|
||||
```
|
||||
|
||||
## Bubbling events
|
||||
|
||||
Instead of doing `<button on:click>` to 'forward' the event from the element to the component, the component should accept an `onclick` callback prop:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { onclick, children } = $props();
|
||||
</script>
|
||||
|
||||
<button {onclick}>
|
||||
{@render children()}
|
||||
</button>
|
||||
```
|
||||
|
||||
Note that this also means you can 'spread' event handlers onto the element along with other props:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children, ...props } = $props();
|
||||
</script>
|
||||
|
||||
<button {...props}>
|
||||
{@render children()}
|
||||
</button>
|
||||
```
|
||||
|
||||
## Event modifiers
|
||||
|
||||
In Svelte 4, you can add event modifiers to handlers:
|
||||
|
||||
```svelte
|
||||
<button on:click|once|preventDefault={handler}>...</button>
|
||||
```
|
||||
|
||||
Modifiers are specific to `on:` and as such do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers.
|
||||
|
||||
Since event handlers are just functions, you can create your own wrappers as necessary:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
function once(fn) {
|
||||
return function (event) {
|
||||
if (fn) fn.call(this, event);
|
||||
fn = null;
|
||||
};
|
||||
}
|
||||
|
||||
function preventDefault(fn) {
|
||||
return function (event) {
|
||||
event.preventDefault();
|
||||
fn.call(this, event);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={once(preventDefault(handler))}>...</button>
|
||||
```
|
||||
|
||||
There are three modifiers — `capture`, `passive` and `nonpassive` — that can't be expressed as wrapper functions, since they need to be applied when the event handler is bound rather than when it runs.
|
||||
|
||||
For `capture`, we add the modifier to the event name:
|
||||
|
||||
```svelte
|
||||
<button onclickcapture={...}>...</button>
|
||||
```
|
||||
|
||||
Changing the [`passive`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) option of an event handler, meanwhile, is not something to be done lightly. If you have a use case for it — and you probably don't! - then you will need to use an action to apply the event handler yourself.
|
||||
|
||||
## Multiple event handlers
|
||||
|
||||
In Svelte 4, this is possible:
|
||||
|
||||
```svelte
|
||||
<button on:click={one} on:click={two}>...</button>
|
||||
```
|
||||
|
||||
This is something of an anti-pattern, since it impedes readability (if there are many attributes, it becomes harder to spot that there are two handlers unless they are right next to each other) and implies that the two handlers are independent, when in fact something like `event.stopImmediatePropagation()` inside `one` would prevent `two` from being called.
|
||||
|
||||
Duplicate attributes/properties on elements — which now includes event handlers — are not allowed. Instead, do this:
|
||||
|
||||
```svelte
|
||||
<button
|
||||
onclick={(e) => {
|
||||
one(e);
|
||||
two(e);
|
||||
}}
|
||||
>
|
||||
...
|
||||
</button>
|
||||
```
|
||||
|
||||
When spreading props, local event handlers must go _after_ the spread, or they risk being overwritten:
|
||||
|
||||
```svelte
|
||||
<button
|
||||
{...props}
|
||||
onclick={(e) => {
|
||||
doStuff(e);
|
||||
props.onclick?.(e);
|
||||
}}
|
||||
>
|
||||
...
|
||||
</button>
|
||||
```
|
||||
|
||||
## Why the change?
|
||||
|
||||
By deprecating `createEventDispatcher` and the `on:` directive in favour of callback props and normal element properties, we:
|
||||
|
||||
- reduce Svelte's learning curve
|
||||
- remove boilerplate, particularly around `createEventDispatcher`
|
||||
- remove the overhead of creating `CustomEvent` objects for events that may not even have listeners
|
||||
- add the ability to spread event handlers
|
||||
- add the ability to know which event handlers were provided to a component
|
||||
- add the ability to express whether a given event handler is required or optional
|
||||
- increase type safety (previously, it was effectively impossible for Svelte to guarantee that a component didn't emit a particular event)
|
@ -1,277 +0,0 @@
|
||||
---
|
||||
title: Imports
|
||||
---
|
||||
|
||||
As well as runes, Svelte 5 introduces a handful of new things you can import, alongside existing ones like `getContext`, `setContext` and `tick`.
|
||||
|
||||
## `svelte`
|
||||
|
||||
### `flushSync`
|
||||
|
||||
Forces any pending effects (including DOM updates) to be applied immediately, rather than in the future. This is mainly useful in a testing context — you'll rarely need it in application code.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { flushSync } from 'svelte';
|
||||
|
||||
let count = $state(0);
|
||||
let element;
|
||||
|
||||
function onclick() {
|
||||
flushSync(() => (count += 1));
|
||||
|
||||
// without `flushSync`, the DOM would be updated in the future
|
||||
console.log(element.textContent === String(count));
|
||||
}
|
||||
</script>
|
||||
|
||||
<span bind:this={element}>{count}</span>
|
||||
<button {onclick}>update</button>
|
||||
```
|
||||
|
||||
### `mount`
|
||||
|
||||
Instantiates a component and mounts it to the given target:
|
||||
|
||||
```js
|
||||
// @errors: 2322
|
||||
import { mount } from 'svelte';
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.querySelector('#app'),
|
||||
props: { some: 'property' }
|
||||
});
|
||||
```
|
||||
|
||||
Note that unlike calling `new App(...)` in Svelte 4, things like effects (including `onMount` callbacks, and action functions) will not run during `mount`. If you need to force pending effects to run (in the context of a test, for example) you can do so with `flushSync()`.
|
||||
|
||||
### `hydrate`
|
||||
|
||||
Like `mount`, but will reuse up any HTML rendered by Svelte's SSR output (from the [`render`](#svelte-server-render) function) inside the target and make it interactive:
|
||||
|
||||
```js
|
||||
// @errors: 2322
|
||||
import { hydrate } from 'svelte';
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = hydrate(App, {
|
||||
target: document.querySelector('#app'),
|
||||
props: { some: 'property' }
|
||||
});
|
||||
```
|
||||
|
||||
As with `mount`, effects will not run during `hydrate` — use `flushSync()` immediately afterwards if you need them to.
|
||||
|
||||
### `unmount`
|
||||
|
||||
Unmounts a component created with [`mount`](#svelte-mount) or [`hydrate`](#svelte-hydrate):
|
||||
|
||||
```js
|
||||
// @errors: 1109
|
||||
import { mount, unmount } from 'svelte';
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = mount(App, {...});
|
||||
|
||||
// later
|
||||
unmount(app);
|
||||
```
|
||||
|
||||
### `untrack`
|
||||
|
||||
To prevent something from being treated as an `$effect`/`$derived` dependency, use `untrack`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
let { a, b } = $props();
|
||||
|
||||
$effect(() => {
|
||||
// this will run when `a` changes,
|
||||
// but not when `b` changes
|
||||
console.log(a);
|
||||
console.log(untrack(() => b));
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### `createRawSnippet`
|
||||
|
||||
An advanced API designed for people building frameworks that integrate with Svelte, `createRawSnippet` allows you to create [snippets](/docs/snippets) programmatically for use with `{@render ...}` tags:
|
||||
|
||||
```js
|
||||
import { createRawSnippet } from 'svelte';
|
||||
|
||||
const greet = createRawSnippet((name) => {
|
||||
return {
|
||||
render: () => `
|
||||
<h1>Hello ${name()}!</h1>
|
||||
`,
|
||||
setup: (node) => {
|
||||
$effect(() => {
|
||||
node.textContent = `Hello ${name()}!`;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
The `render` function is called during server-side rendering, or during `mount` (but not during `hydrate`, because it already ran on the server), and must return HTML representing a single element.
|
||||
|
||||
The `setup` function is called during `mount` or `hydrate` with that same element as its sole argument. It is responsible for ensuring that the DOM is updated when the arguments change their value — in this example, when `name` changes:
|
||||
|
||||
```svelte
|
||||
{@render greet(name)}
|
||||
```
|
||||
|
||||
If `setup` returns a function, it will be called when the snippet is unmounted. If the snippet is fully static, you can omit the `setup` function altogether.
|
||||
|
||||
## `svelte/reactivity`
|
||||
|
||||
Svelte provides reactive `SvelteMap`, `SvelteSet`, `SvelteDate` and `SvelteURL` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts. [Demo:](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE32QwUrEMBBAf2XMpQrb9t7tFrx7UjxZYWM6NYFkEpJJ16X03yWK9OQeZ3iPecwqZmMxie5tFSQdik48hiAOgq-hDGlByygOIvkcVdn0SUUTeBhpZOOCjwwrvPxgr89PsMEcvYPqV2wjSsVmMXytjiMVR3lKDDlaOAHhZVfvK80cUte2-CVdsNgo79ogWVcPx5H6dj9M_V1dg9KSPjEBe2CNCZumgboeRuoNhczwYWjqFmkzntYcbROiZ6-83f5HtE9c3nADKUF_yEi9jnvQxVgLOUySEc464nwGSRMsRiEsGJO8mVeEbRAH4fxkZoOT6Dhm3N63b9_bGfOlAQAA)
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { SvelteURL } from 'svelte/reactivity';
|
||||
|
||||
const url = new SvelteURL('https://example.com/path');
|
||||
</script>
|
||||
|
||||
<!-- changes to these... -->
|
||||
<input bind:value={url.protocol} />
|
||||
<input bind:value={url.hostname} />
|
||||
<input bind:value={url.pathname} />
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- will update `href` and vice versa -->
|
||||
<input bind:value={url.href} />
|
||||
```
|
||||
|
||||
## `svelte/events`
|
||||
|
||||
Where possible, event handlers added with [attributes like `onclick`](/docs/event-handlers) use a technique called _event delegation_. It works by creating a single handler for each event type on the root DOM element, rather than creating a handler for each element, resulting in better performance and memory usage.
|
||||
|
||||
Delegated event handlers run after other event handlers. In other words, a handler added programmatically with [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) will run _before_ a handler added declaratively with `onclick`, regardless of their relative position in the DOM ([demo](/#H4sIAAAAAAAAE41Sy2rDMBD8lUUXJxDiu-sYeugt_YK6h8RaN6LyykgrQzH6965shxJooQc_RhrNzA6aVW8sBlW9zYouA6pKPY-jOij-GjMIE1pGwcFF3-WVOnTejNy01LIZRucZZnD06iIxJOi9G6BYjxVPmZQfiwzaTBkL2ti73R5ODcwLiftIHRtHcLuQtuhlc9tpuSyBbyZAuLloNfhIELBzpO8E-Q_O4tG6j13hIqO_y0BvPOpiv0bhtJ1Y3pLoeNH6ZULiswmMJLZFZ033WRzuAvstdMseOXqCh9SriMfBTfgPnZxg-aYM6_KnS6pFCK6GdJVHPc0C01JyfY0slUnHi-JpfgjwSzUycdgmfOjFEP3RS1qdhJ8dYMDFt1yNmxxU0jRyCwanTW9Qq4p9xPSevgHI3m43QAIAAA==)). It also means that calling `event.stopPropagation()` inside a declarative handler _won't_ prevent the programmatic handler (created inside an action, for example) from running.
|
||||
|
||||
To preserve the relative order, use `on` rather than `addEventListener` ([demo](/#H4sIAAAAAAAAE3VRy26DMBD8lZUvECkqdwpI_YB-QdJDgpfGqlkjex2pQv73rnmoStQeMB52dnZmmdVgLAZVn2ZFlxFVrd6mSR0Vf08ZhDtaRsHBRd_nL03ovZm4O9OZzTg5zzCDo3cXiSHB4N0IxdpWvD6RnuoV3pE4rLT8WGTQ5p6xoE20LA_QdjAvJB4i9WxE6nYhbdFLcaucuaqAbyZAuLloNfhIELB3pHeC3IOz-GLdZ1m4yOh3GRiMR10cViucto7l9MjRk9gvxdsRit6a_qs47q1rT8qvpvpdDjXChqshXWdT7SwwLVtrrpElnAguSu38EPCPEOItbF4eEhiifxKkdZLw8wQYcZlbrYO7bFTcdPJbR6fNYFCrmn3E9JF-AJZOg9MRAgAA)):
|
||||
|
||||
```js
|
||||
// @filename: index.ts
|
||||
const element: Element = null as any;
|
||||
// ---cut---
|
||||
import { on } from 'svelte/events';
|
||||
|
||||
const off = on(element, 'click', () => {
|
||||
console.log('element was clicked');
|
||||
});
|
||||
|
||||
// later, if we need to remove the event listener:
|
||||
off();
|
||||
```
|
||||
|
||||
`on` also accepts an optional fourth argument which matches the options argument for `addEventListener`.
|
||||
|
||||
## `svelte/server`
|
||||
|
||||
### `render`
|
||||
|
||||
Only available on the server and when compiling with the `server` option. Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app:
|
||||
|
||||
```js
|
||||
// @errors: 2724 2305 2307
|
||||
import { render } from 'svelte/server';
|
||||
import App from './App.svelte';
|
||||
|
||||
const result = render(App, {
|
||||
props: { some: 'property' }
|
||||
});
|
||||
```
|
||||
|
||||
If the `css` compiler option was set to `'injected'`, `<style>` elements will be included in the `head`.
|
||||
|
||||
## `svelte/store`
|
||||
|
||||
In addition to the existing store-related imports such as `writable`, `svelte/store` gains two new functions: `fromStore` and `toStore`. These allow you to easily use stores and rune-based state interchangeably, even outside `.svelte` files.
|
||||
|
||||
### `fromStore`
|
||||
|
||||
Takes a store and turns it into an object with a reactive (and readonly, if the store is not [writable](https://svelte.dev/docs/svelte-store#writable)) `current` property.
|
||||
|
||||
```js
|
||||
import { fromStore, get, writable } from 'svelte/store';
|
||||
|
||||
const store = writable(0);
|
||||
|
||||
const count = fromStore(store);
|
||||
|
||||
count.current; // 0;
|
||||
store.set(1);
|
||||
count.current; // 1
|
||||
|
||||
count.current += 1;
|
||||
get(store); // 2
|
||||
```
|
||||
|
||||
### `toStore`
|
||||
|
||||
Creates a store from a function that returns reactive state (and, optionally, a second function that sets the state):
|
||||
|
||||
```js
|
||||
import { toStore } from 'svelte/store';
|
||||
|
||||
let count = $state(0);
|
||||
|
||||
const store = toStore(
|
||||
() => count,
|
||||
(v) => (count = v)
|
||||
);
|
||||
|
||||
store.set(1);
|
||||
count; // 1
|
||||
```
|
||||
|
||||
## `svelte/elements`
|
||||
|
||||
Svelte provides built-in [DOM types](https://github.com/sveltejs/svelte/blob/master/packages/svelte/elements.d.ts). A common use case for DOM types is forwarding props to an HTML element. To properly type your props and get full intellisense, your props interface should extend the attributes type for your HTML element:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
username: string;
|
||||
}
|
||||
|
||||
let { username, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div {...rest}>
|
||||
Hi, {username}!
|
||||
</div>
|
||||
```
|
||||
|
||||
> You can use `ComponentProps<ImportedComponent>`, if you wish to forward props to a Svelte component.
|
||||
|
||||
Svelte provides a best-effort of all the HTML DOM types that exist. If an attribute is missing from our [type definitions](https://github.com/sveltejs/svelte/blob/master/packages/svelte/elements.d.ts), you are welcome to open an issue and/or a PR fixing it. For experimental attributes, you can augment the existing types locally by creating a `.d.ts` file:
|
||||
|
||||
```ts
|
||||
import { HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
declare module 'svelte/elements' {
|
||||
export interface SvelteHTMLElements {
|
||||
'custom-button': HTMLButtonAttributes;
|
||||
}
|
||||
|
||||
// allows for more granular control over what element to add the typings to
|
||||
export interface HTMLButtonAttributes {
|
||||
veryexperimentalattribute?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export {}; // ensure this is not an ambient module, else types will be overridden instead of augmented
|
||||
```
|
||||
|
||||
The `.d.ts` file must be included in your `tsconfig.json` file. If you are using the standard `"include": ["src/**/*"]`, this just means the file should be placed in the `src` directory.
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "API"
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
---
|
||||
title: Universal reactivity
|
||||
---
|
||||
|
||||
In Svelte 5, you can create reactive state anywhere in your app — not just at the top level of your components.
|
||||
|
||||
Suppose we have a component like this:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
function increment() {
|
||||
count += 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={increment}>
|
||||
clicks: {count}
|
||||
</button>
|
||||
```
|
||||
|
||||
We can encapsulate this logic in a function, so that it can be used in multiple places:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
+ function createCounter() {
|
||||
let count = $state(0);
|
||||
|
||||
function increment() {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
+ return {
|
||||
+ get count() { return count },
|
||||
+ increment
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
+ const counter = createCounter();
|
||||
</script>
|
||||
|
||||
-<button onclick={increment}>
|
||||
- clicks: {count}
|
||||
+<button onclick={counter.increment}>
|
||||
+ clicks: {counter.count}
|
||||
</button>
|
||||
```
|
||||
|
||||
> Note that we're using a [`get` property](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) in the returned object, so that `counter.count` always refers to the current value rather than the value at the time the `createCounter` function was called.
|
||||
>
|
||||
> As a corollary, `const { count, increment } = createCounter()` won't work. That's because in JavaScript, [destructured declarations](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) are evaluated at the time of destructuring — in other words, `count` will never update.
|
||||
|
||||
We can also extract that function out into a separate `.svelte.js` or `.svelte.ts` module...
|
||||
|
||||
```js
|
||||
export function createCounter() {
|
||||
let count = $state(0);
|
||||
|
||||
function increment() {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
get count() {
|
||||
return count;
|
||||
},
|
||||
increment
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
...and import it into our component:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
+ import { createCounter } from './counter.svelte.js';
|
||||
- function createCounter() {...}
|
||||
|
||||
const counter = createCounter();
|
||||
</script>
|
||||
|
||||
<button onclick={counter.increment}>
|
||||
clicks: {counter.count}
|
||||
</button>
|
||||
```
|
||||
|
||||
[See this example in the playground.](/#H4sIAAAAAAAAE2VQ0U7DMAz8FStC2iaqDl67dhLiMxgPI3NRRutUiYNAVf6dJG1TBk-W7bvznUfRqg6tqF5GQeceRSWehkEUgr-H2NhP7BhDb7UzMk5qK40a-HiiE6t-0IZhBGnwzPisHTEa8NAa3cOm3MtpUk4y5dVuDoEXmFKTZZjX0NwKbHcBVe_XQ1S_OWZNoEl2Sn404yKsKDB7JPbJUNraCvI-VR_VJoVjiNLri2oVXkTFxqEvcvJbt-sTrvb3A_ArhW4dSVbB0x_rMEYjHc7pQrY7ywGwfdjN2TMzm19Y8S-Rc9_AYwRH57EYZGdowbwv2istQ9L8MA19MdV8JimGpf__hFf_Ay1mGDQKAgAA)
|
||||
|
||||
## Stores equivalent
|
||||
|
||||
In Svelte 4, the way you'd do this is by creating a [custom store](https://svelte.dev/tutorial/svelte/custom-stores), perhaps like this:
|
||||
|
||||
```js
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export function createCounter() {
|
||||
const { subscribe, update } = writable(0);
|
||||
|
||||
function increment() {
|
||||
update((count) => count + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
increment
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Back in the component, we retrieve the store value by prefixing its name with `$`:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
import { createCounter } from './counter.js';
|
||||
|
||||
const counter = createCounter();
|
||||
</script>
|
||||
|
||||
<button onclick={counter.increment}>
|
||||
- clicks: {counter.count}
|
||||
+ clicks: {$counter}
|
||||
</button>
|
||||
```
|
||||
|
||||
The store approach has some significant drawbacks. A counter is just about the simplest custom store we could create, and yet we have to completely change how the code is written — importing `writable`, understanding its API, grabbing references to `subscribe` and `update`, changing the implementation of `increment` from `count += 1` to something far more cryptic, and prefixing the store name with a `$` to retrieve its value. That's a lot of stuff you need to understand.
|
||||
|
||||
With runes, we just copy the existing code into a new function.
|
||||
|
||||
## Gotchas
|
||||
|
||||
Reactivity doesn't magically cross function boundaries. In other words, replacing the `get` property with a regular property wouldn't work...
|
||||
|
||||
```diff
|
||||
export function createCounter() {
|
||||
let count = $state(0);
|
||||
|
||||
function increment() {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
- get count() { return count },
|
||||
+ count,
|
||||
increment
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
...because the value of `count` in the returned object would always be `0`. Using the `$state` rune doesn't change that fact — it simply means that when you _do_ read `count` (whether via a `get` property or a normal function) inside your template or inside an effect, Svelte knows what to update when `count` changes.
|
@ -1,79 +0,0 @@
|
||||
---
|
||||
title: Fine-grained reactivity
|
||||
---
|
||||
|
||||
In Svelte 4, reactivity centres on the _component_ and the top-level state declared therein. What this means is that in a situation like this...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let todos = [];
|
||||
|
||||
function remaining(todos) {
|
||||
console.log('recalculating');
|
||||
return todos.filter((todo) => !todo.done).length;
|
||||
}
|
||||
|
||||
function addTodo(event) {
|
||||
if (event.key !== 'Enter') return;
|
||||
|
||||
todos = [
|
||||
...todos,
|
||||
{
|
||||
done: false,
|
||||
text: event.target.value
|
||||
}
|
||||
];
|
||||
|
||||
event.target.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<input onkeydown={addTodo} />
|
||||
|
||||
{#each todos as todo}
|
||||
<div>
|
||||
<input bind:value={todo.text} />
|
||||
<input type="checkbox" bind:checked={todo.done} />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<p>{remaining(todos)} remaining</p>
|
||||
```
|
||||
|
||||
...editing any individual `todo` will invalidate the entire list. You can see this for yourself by [opening the playground](/#H4sIAAAAAAAAE2VSy27jMAz8FVV7cAIE8t21DfSwf7C3OgdVohOhCmXIdLaF4H9fPewE6N7I0ZAzpBj4aCzMvHkPHOUNeMPfpomfOH1PKZnvYAliPrvFq4S0s_Jmon7AgSwQI6fdzDr2fn6NUATHBRUZh8zDTRo0eDlkzpGF9DyQcjg7C8K6y6HyoKRVi5UUidXxtVA80OKx9BbRIYHPTVjXs5cUCO0QjsICXuiai9Yf6lLrP5F4gDsgPbTNyAoiPuGbvXQdq35j7F4dWdHchhjoMVdJBxJCZOy0A2EPBkpuGjZKO8PpiRJ8UcOKHEl_ARJ3aRfYGWsJzg_N_6nRQFXt87X1c_fYGpwWYg7jINr9xS5sE6-szoTwC6S6bv8j5xxkxVabe18EtyYfBnWT9bqQt5ucb32erHQS3cDVFdTnh_saeKnLOeitMm1ir2zrohTq5CR_UDv14eddrM9Laeupj8d2c9qMBjRvyC-wntd_ggUqOqcCAAA=), adding some todos, and watching the console in the bottom right. `remaining(todos)` is recalculated every time we edit the `text` of a todo, even though it can't possibly affect the result.
|
||||
|
||||
Worse, everything inside the `each` block needs to be checked for updates. When a list gets large enough, this behaviour has the potential to cause performance headaches.
|
||||
|
||||
With runes, it's easy to make reactivity _fine-grained_, meaning that things will only update when they need to:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- let todos = [];
|
||||
+ let todos = $state([]);
|
||||
|
||||
function remaining(todos) {
|
||||
console.log('recalculating');
|
||||
return todos.filter(todo => !todo.done).length;
|
||||
}
|
||||
|
||||
function addTodo(event) {
|
||||
if (event.key !== 'Enter') return;
|
||||
|
||||
- todos = [
|
||||
- ...todos,
|
||||
- {
|
||||
- done: false,
|
||||
- text: event.target.value
|
||||
- }
|
||||
- ];
|
||||
+ todos.push({
|
||||
+ done: false,
|
||||
+ text: event.target.value
|
||||
+ });
|
||||
|
||||
event.target.value = '';
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
In [this version of the app](/#H4sIAAAAAAAAE2VSy07EMAz8lRCQ2kqovZe2Egf-gBvlEBJ3N9qsUyXuAqr67-TRZSW4xfZ4Zmxn5ZM24Hn7tnIUZ-Atf55n_sjpe46Bv4AhCLG3i5Mx03np9EzDiCMZIEZWWc969uBJEJRv79VTKIXitKAkbZE5OAuNGg9lwlZsjeWRpEVvDdTGHsrCgRRGLkZQABaJI0Ac0OIwa9TBKYFLJKwf2F181MoiVLUBPNAxNW1_1IVSrwFYwgWQfrX1xHKmPsE3u-t7VrxgYC8qljX3IUbK2vPij-XeO1IUbdkkjIfHa47gi1qWOUm4A1B9EWaBXN-qX8L_kLC9oria75rbgrHTOC_ELAaXyn5iv-7jbKxJgPUehDzuRxA-PeIGqFP6MmTBneRDo2qTXr-m1UXHO88NFe_ej1weQZ4-7NfIc1-KQe2dcf5rZ9dkpbWJTtL2u3lY_x59u32DrpmH8KPOVulJg-ItuQW29-0HkdosMowCAAA=), editing the `text` of a todo won't cause unrelated things to be updated.
|
@ -1,297 +0,0 @@
|
||||
---
|
||||
title: Old vs new
|
||||
---
|
||||
|
||||
This page intends to give a broad overview of how code written using the new APIs looks compared to code not using them. You will see that for most simple tasks that only involve a single component it will actually not look much different. For more complex logic, they simplify things.
|
||||
|
||||
## Counter
|
||||
|
||||
The `$state`, `$derived` and `$effect` runes replace magic `let` declarations, `$: x = ...` and `$: { ... }`. Event handlers can be written as event attributes now, which in practise means just removing the colon.
|
||||
|
||||
- [Before](/#H4sIAAAAAAAAE0VP0Q6CMAz8lbqYAGqC-jiBxH_wTXzQUWRxbgQ6E7Ps34VN40vbu8u1V8daqXBk_OyYvj6RcXbse7Zh9O5nML5QEU54NHYQM1OMYpA9VbWuSSGBMFYTlLA9zMySQ2PsTeHERGUF-6B8VRdmki2kUa9gt81-dE1XhQOlyckY6OS9WyRZdJOf21SK_B9AFzdLZDQYzYWS4lG6NIOyiqfXax9SuoA85OBitrAlOqvptadpZCuxYZwGi_7iP__ps0sVAQAA)
|
||||
- [After](/#H4sIAAAAAAAACkVP0WrDMAz8FU0U6qyBdnvMksD-YW_LHlJbXs1cOzhyYRj_-3Dcsgch7nQ6nRJqY2nF7jOhm6-EHb4vC7bIv0sB640sE7a4-hhkYfpVBrPwOLmJLTFIHx3DALuVZyZxat4eE-Xj2VIZKQrmRkpU7TO8VlGpHWlNkoVoYBghbTQbDXftCC-n5kFPPFsKLPYf3sPFfF-e9ncjzqXlgvrjf0DXnyOzd-CdtEb-DKme2bwPh7w9kTaU4QipJs7FpC6O2OLVK6MNKew4RMpf-Q9GD6iFNAEAAA==)
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- let count = 0;
|
||||
- $: double = count * 2;
|
||||
|
||||
- $: {
|
||||
+ let count = $state(0);
|
||||
+ let double = $derived(count * 2);
|
||||
+ $effect(() => {
|
||||
if (count > 10) {
|
||||
alert('Too high!');
|
||||
}
|
||||
- }
|
||||
+ });
|
||||
</script>
|
||||
|
||||
-<button on:click={() => count++}>
|
||||
+<button onclick={() => count++}>
|
||||
{count} / {double}
|
||||
</button>
|
||||
```
|
||||
|
||||
## Tracking dependencies
|
||||
|
||||
In non-runes mode, dependencies of `$:` statements are tracked at _compile time_. `a` and `b` changing will only cause `sum` to be recalculated because the expression — `add(a, b)` — refers to those values.
|
||||
|
||||
In runes mode, dependencies are tracked at _run time_. `sum` will be recalculated whenever `a` or `b` change, whether they are passed in to the `add` function or accessed via closure. This results in more maintainable, refactorable code.
|
||||
|
||||
- [Before](/#H4sIAAAAAAAAE3WPwarDIBBFf2WQLlpSSNfWCP2Opgs1BuQlRnQsFPHfO0lpSxdvMcK5c4YZCxvdZBPj18K8mi3j7BICOzJ8hBXS3U5oidOSo1kTkUx0AWXve5wsgoIOTuc36Q_tOKQ8E6ph2Ksj6MMWrzVmb9At_tuCsvUwWsyRcmhAb3rtvWi_G73QGZEmF8_N5MxfV_YH6CSopqmSHtG-BPm_qldV_6pBFlVpadGVLi50eBVtkPTveRnc6OzAOMZs660-AQKQKZYyAQAA)
|
||||
- [After](/#H4sIAAAAAAAAE3WQ3WrDMAyFX8WYXqSkkF5njqHPsfTCPwqYJo6x5cIwfvfKKWMMthuBvnMkHVT44lZIfPws3KsN-MhvIfALx6_QmvSEFYH6tOdoGhHJRBdQzn7GFZApNrFTQoXQXc8f31T_SVPeGrcQ3RNsp6ztzodK-pK9Qbd7dlBWGpsxAuZIjPVMH2vq7MXwE8ELnRFpavejWZ15TIWGJ8lU31dJRQxvg_zfqptV_7YGWVSlo0VXSlwoeBVDkPSIbbducWD5iDFDvdcXqDJidUMBAAA=)
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- let a = 0;
|
||||
- let b = 0;
|
||||
- $: sum = add(a, b);
|
||||
+ let a = $state(0);
|
||||
+ let b = $state(0);
|
||||
+ let sum = $derived(add());
|
||||
|
||||
- function add(a, b) {
|
||||
+ function add() {
|
||||
return a + b;
|
||||
}
|
||||
</script>
|
||||
|
||||
-<button on:click={() => a++}>a++</button>
|
||||
-<button on:click={() => b++}>b++</button>
|
||||
+<button onclick={() => a++}>a++</button>
|
||||
+<button onclick={() => b++}>b++</button>
|
||||
<p>{a} + {b} = {sum}</p>
|
||||
```
|
||||
|
||||
## Untracking dependencies
|
||||
|
||||
Conversely, suppose you — for some reason — wanted to recalculate `sum` when `a` changes, but _not_ when `b` changes.
|
||||
|
||||
In non-runes mode, we 'hide' the dependency from the compiler by excluding it from the `$:` statement. In runes mode, we have a better and more explicit solution: [`untrack`](/docs/functions#untrack).
|
||||
|
||||
- [Before](/#H4sIAAAAAAAAE3WPwYrDIBCGX2WQHhJSyJ6tEfocmz2oMSBNjOi4sIjvvpO0u6WHHhz4_vmGGQub3WIT45-FebVaxtk1BHZm-BN2SN92QUucthzNnohkogsoRz_iYhEUDPBx-SP9TycOKa-Eapoa1R7Z_ubsDbrNP3IoRwOjxRwphA704dbRi_65ywudEWls89wsztyG0rQwSFBdVyUV0d8F-V7Vu6pf1SCLqrS06Eq3Fjq5ij5I-vG6TW52dmIcY7b1q_4CeJwNvCwBAAA=)
|
||||
- [After](/#H4sIAAAAAAAACnXOwWrFIBAF0F8Zhi4SDOStUyP0O1660DgBaWJExwdF_PeHKaUU2uWce7lMwc3tlHC6F_T6IJzwLQQckD9DO9KDdiYcMJ05rk1kWqMLrBa_8E4MGmZ4SayZulv_-q3mT035aG4pugfZTlvb9VfqF96yX9mdHi6F0mzhSJyjBw0CzDVTFy_Hnxe8NJn59HD6dXfrx1y6HmYFWoiqtBBy_MrVv03TmuZ3M6iiKwgopsIMJeWjyjEoHPA4rdscWZw4Zqrv9Qm1yJsGQQEAAA==)
|
||||
|
||||
```diff
|
||||
<script>
|
||||
+ import { untrack } from 'svelte';
|
||||
|
||||
- let a = 0;
|
||||
- let b = 0;
|
||||
- $: sum = add(a);
|
||||
+ let a = $state(0);
|
||||
+ let b = $state(0);
|
||||
+ let sum = $derived(add());
|
||||
|
||||
- function add(a) {
|
||||
- return a + b;
|
||||
+ function add() {
|
||||
+ return a + untrack(() => b);
|
||||
}
|
||||
</script>
|
||||
|
||||
-<button on:click={() => a++}>a++</button>
|
||||
-<button on:click={() => b++}>b++</button>
|
||||
+<button onclick={() => a++}>a++</button>
|
||||
+<button onclick={() => b++}>b++</button>
|
||||
<p>{a} + {b} = {sum}</p>
|
||||
```
|
||||
|
||||
## Simple component props
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- export let count = 0;
|
||||
+ let { count = 0 } = $props();
|
||||
</script>
|
||||
|
||||
{count}
|
||||
```
|
||||
|
||||
## Advanced component props
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- let classname = '';
|
||||
- export { classname as class };
|
||||
+ let { class: classname, ...others } = $props();
|
||||
</script>
|
||||
|
||||
<pre class={classname}>
|
||||
- {JSON.stringify($$restProps)}
|
||||
+ {JSON.stringify(others)}
|
||||
</pre>
|
||||
```
|
||||
|
||||
## Autoscroll
|
||||
|
||||
To implement a chat window that autoscrolls to the bottom when new messages appear (but only if you were _already_ scrolled to the bottom), we need to measure the DOM before we update it.
|
||||
|
||||
In Svelte 4, we do this with `beforeUpdate`, but this is a flawed approach — it fires before _every_ update, whether it's relevant or not. In the example below, we need to introduce checks like `updatingMessages` to make sure we don't mess with the scroll position when someone toggles dark mode.
|
||||
|
||||
With runes, we can use `$effect.pre`, which behaves the same as `$effect` but runs before the DOM is updated. As long as we explicitly reference `messages` inside the effect body, it will run whenever `messages` changes, but _not_ when `theme` changes.
|
||||
|
||||
`beforeUpdate`, and its equally troublesome counterpart `afterUpdate`, will be deprecated in Svelte 5.
|
||||
|
||||
- [Before](/#H4sIAAAAAAAAE31WXa_bNgz9K6yL1QmWOLlrC-w6H8MeBgwY9tY9NfdBtmlbiywZkpyPBfnvo2zLcZK28AWuRPGI5OGhkEuQc4EmiL9eAskqDOLg97oOZoE9125jDigs0t6oRqfOsjap5rXd7uTO8qpW2sIFEsyVxn_qjFmcAcstar-xPN3DFXKtKgi768IVgQku0ELj3Lgs_kZjWIEGNpAzYXDlHWyJFZI1zJjeh4O5uvl_DY8oUkVeVoFuJKYls-_CGYS25Aboj0EtWNqel0wWoBoLTGZgmdgDS9zW4Uz4NsrswPHoyutN4xInkylstnBxdmIhh8m7xzqmoNE2Wq46n1RJQzEbq4g-JQSl7e-HDx-GdaTy3KD9E3lRWvj5Zu9QX1QN20dj7zyHz8s-1S6lW7Cpz3RnXTcm04hIlfdFuO8p2mQ5-3a06cqjrn559bF_2NHOnRZ5I1PLlXQNyQT-hedMHeUEDyjtdMxsa4n2eIbNhlTwhyRthaOKOmYtniwF6pwt0wXa6MBEg0OibZec27gz_dk3UrZ6hB2LLYoiv521Yd8Gt-foTrfhiCDP0lC9VUUhcDLU49Xe_9943cNvEArHfAjxeBTovvXiNpFynfEDpIIZs9kFbg52QbeNHWZzebz32s7xHco3nJAJl1nshmhz8dYOQJDyZetnbb2gTWe-vEeWlrfpZMavr56ldb29eNt6UXvgwgFbp_WC0tl2RK25rGk6lYz3nUI2lzvBXGHhPZPGWmKUXFNBKqdaW259wl_aHbiqoVIZdpE60Nax6IOujT0LbFFxIVTCxCRR2XloUcYNvSbnGHKBp763jHoj59xiZWJI0Wm0P_m3MSS985xkasn-cFq20xTDy3J5KFcjgUTD69BHdcHIjz431z28IqlxGcPSfdFnrGDZn6gD6lyo45zyHAD-btczf-98nhQxHEvKfeUtOVkSejD3q-9X7JbzjGtsdUxlKdFU8qGsT78uaw848syWMXz85Waq2Gnem4mAn3prweq4q6Y3JEpnqMmnPoFRgmd3ySW0LLRqSKlwYHriCvJvUs2yjMaaoA-XzTXLeGMe45zmhv_XAno3Mj0xF7USuqNvnE9H343QHlq-eAgxpbTPNR9yzUkgLjwSR0NK4wKoxy-jDg-9vy8sUSToakzW-9fX13Em9Q8T6Z26uZhBN36XUYo5q7ggLXBZoub2Ofv7g6GCZfTxe034NCjiudXj7Omla0eTfo7QBPOcYxbE7qG-vl3_B1G-_i_JCAAA)
|
||||
- [After](/#H4sIAAAAAAAAE31WXa-jNhD9K7PsdknUQJLurtRLPqo-VKrU1327uQ8GBnBjbGSb5KZR_nvHgMlXtyIS9njO-MyZGZRzUHCBJkhez4FkNQZJ8HvTBLPAnhq3MQcUFmlvVKszZ1mbTPPGbndyZ3ndKG3hDJZne7hAoVUNYY8JV-RBPgIt2AprhA18MpZZnIQ50_twuvLHNRrDSjRXj9fwiCJTBLIKdCsxq5j9EM4gtBU3QD8GjWBZd14xWYJqLTCZg2ViDyx1W4cz4dv0hsiB49FRHkyfsCgws3GjcTKZwmYLZ2feWc9o1W8zJQ2Fb62i5JUQRNRHgs-fx3WsisKg_RN5WVn4-WrvUd9VA9tH4-AcwbfFQIpkLWByvWzqSe2sk3kyjUlOec_XPU-3TRaz_75tuvKoi19e3OvipSpamVmupJM2F_gXnnJ1lBM8oLQjHceys8R7PMFms4HwD2lRhzeEe-EsvluSrHe2TJdo4wMTLY48XKwPzm0KGm2r5ajFtRYU4TWOY7-ddWHfxhDP0QkQhnf5PWRnVVkKnIx8fZsOb5dR16nwG4TCCRdCMphWQ7z1_DoOcp3zA2SCGbPZBa5jd0G_TRxmc36Me-mG6A7l60XIlMs8ce2-OXtrDyBItdz6qVjPadObzx-RZdV1nJjx64tXad1sz962njceOHfAzmk9JzrbXqg1lw3NkZL7vgE257t-uMDcO6attSSokpmgFqVMO2U93e_dDlzOUKsc-3t6zNZp6K9cG3sS2KGSUqiUiUmq8tNYoJwbmvpTAoXA96GyjCojI26xNglk6DpwOPm7NdRYp4ia0JL94bTqRiGB5WJxqFY37RGPoz3c6i4jP3rcUA7wmhqNywQW7om_YQ2L4UQdUBdCHSPiOQJ8bFcxHzeK0jKBY0XcV95SkCWlD9t-9eOM3TLKucauiyktJdpaPqT19ddF4wFHntsqgS-_XE01e48GMwnw02AtWZP02QyGVOkcNfk072CU4PkduZSWpVYt9SkcmJ64hPwHpWF5ziVls3wIFmmW89Y83vMeGf5PBxjcyPSkXNy10J18t3x6-a6CDtBq6SGklNKeazFyLahB3PVIGo2UbhOgGi9vKjzW_j6xVFFD17difXx5ebll0vwvkcGpn4sZ9MN3vqFYsJoL6gUuK9TcPrO_PxgzWMRfflSEr2NHPJf6lj1957rRpH8CNMG84JgHidUtXt4u_wK21LXERAgAAA==)
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- import { beforeUpdate, afterUpdate, tick } from 'svelte';
|
||||
+ import { tick } from 'svelte';
|
||||
|
||||
- let updatingMessages = false;
|
||||
- let theme = 'dark';
|
||||
- let messages = [];
|
||||
+ let theme = $state('dark');
|
||||
+ let messages = $state([]);
|
||||
|
||||
let viewport;
|
||||
|
||||
- beforeUpdate(() => {
|
||||
+ $effect.pre(() => {
|
||||
- if (!updatingMessages) return;
|
||||
+ messages;
|
||||
const autoscroll = viewport && viewport.offsetHeight + viewport.scrollTop > viewport.scrollHeight - 50;
|
||||
|
||||
if (autoscroll) {
|
||||
tick().then(() => {
|
||||
viewport.scrollTo(0, viewport.scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
- updatingMessages = false;
|
||||
});
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 'Enter') {
|
||||
const text = event.target.value;
|
||||
if (!text) return;
|
||||
|
||||
- updatingMessages = true;
|
||||
messages = [...messages, text];
|
||||
event.target.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
toggleValue = !toggleValue;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class:dark={theme === 'dark'}>
|
||||
<div bind:this={viewport}>
|
||||
{#each messages as message}
|
||||
<p>{message}</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
- <input on:keydown={handleKeydown} />
|
||||
+ <input onkeydown={handleKeydown} />
|
||||
|
||||
- <button on:click={toggle}>
|
||||
+ <button onclick={toggle}>
|
||||
Toggle dark mode
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Forwarding events
|
||||
|
||||
Because [event handlers](event-handlers) are just regular attributes now, the "forwarding events" concept is replaced with just passing callback props. Before, you would have to mark every event that you want to forward separately. You can still do this with event attributes...
|
||||
|
||||
```diff
|
||||
<script>
|
||||
+ let { onclick, onkeydown, ...attributes } = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
- {...$$props}
|
||||
+ {...attributes}
|
||||
- on:click
|
||||
- on:keydown
|
||||
+ {onclick}
|
||||
+ {onkeydown}
|
||||
>a button</button>
|
||||
```
|
||||
|
||||
...but in practise what you probably _really_ want to do in these situations is forward _all_ events. This wasn't possible before, but it is now:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
+ let { ...props } = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
- {...$$props}
|
||||
- on:click
|
||||
- on:keydown
|
||||
+ {...props}
|
||||
>a button</button>
|
||||
```
|
||||
|
||||
- [Before](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACn1Py2rDMBD8lWUJOIEg3xXb0P5G1YMjr4morBXSuhCM_z1xlLaXktMys8xrwdF5yqg_Fgz9RKjxLUY8olzjBvI3eaE7zjwnuzFNtslF6Uww4qbISeB9FuEAY-IJKlUXqIq0OpnQ1H-a0JT3Js9y9dQatOw5aUg0GNx4Dtp6Z7_aZX-AtoPeU5J99eBoALkQnB8m1WE1oe7u9SYe3OhoQC1ppvX4u6akvRxUvGBRSu12MXHMK_xU6PpnVFOX-0_Y53oDgvtVGEYBAAA=)
|
||||
- [After](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo1PQWrEMAz8ihCFJLA492wSaL9R95A6CjV1bGMrhcX4793Eu9tDe-hJmpFGmkm4aEMRu9eEdloJO3z2Hk_IF7-D-EWG6Yqj24LamT6qoD2P0krWq3eB4WVjdhaW4FaoRFugKNLqLG3f_mhsX8a7PPLF0CBROeNCB4FmiTvvrDJafQ6pbmAYYTIUuK4OjmbgD4L340bVZGnb8epudbNeNM3Ycdgonx5hyrP_5jHEkEAI4YPzETIM8HS0dfMrRrEA6b6dx-lmq29L_cPYW_4GVdmFa3EBAAA=)
|
||||
|
||||
## Passing UI content to a component
|
||||
|
||||
Previously, you would pass UI content into components using slots. Svelte 5 provides a better mechanism for this, [snippets](snippets). In the simple case of passing something to the default slot, nothing has changed for the consumer:
|
||||
|
||||
```svelte
|
||||
<!-- same with both slots and snippets -->
|
||||
<script>
|
||||
import Button from './Button.svelte';
|
||||
</script>
|
||||
|
||||
<Button>click me</Button>
|
||||
```
|
||||
|
||||
Inside `Button.svelte`, use `@render` instead of the `<slot>` tag. The default content is passed as the `children` prop:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
+ let { children } = $props();
|
||||
</script>
|
||||
|
||||
<button>
|
||||
- <slot />
|
||||
+ {@render children()}
|
||||
</button>
|
||||
```
|
||||
|
||||
When passing props back up to the consumer, snippets make things easier to reason about, removing the need to deal with the confusing semantics of the `let:`-directive:
|
||||
|
||||
```diff
|
||||
<!-- provider -->
|
||||
<script>
|
||||
+ let { children } = $props();
|
||||
</script>
|
||||
|
||||
<button>
|
||||
- <slot prop="some value" />
|
||||
+ {@render children("some value")}
|
||||
</button>
|
||||
```
|
||||
|
||||
```diff
|
||||
<!-- consumer -->
|
||||
<script>
|
||||
import Button from './Button.svelte';
|
||||
</script>
|
||||
|
||||
- <Button let:prop>click {prop}</Button>
|
||||
+ <Button>
|
||||
+ {#snippet children(prop)}
|
||||
+ click {prop}
|
||||
+ {/snippet}
|
||||
+ </Button>
|
||||
```
|
||||
|
||||
Combined with event attributes, this reduces the number of concepts to learn — everything related to the component boundary can now be expressed through props.
|
||||
|
||||
- [Before](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACn2PzYrCQBCEX6XpSxTE3GeTgPoYzh5i0sHB-WOmIywh776ZjCgieOsqqvrrnnBQmiKK84S2NYQCD97jDvnPJxHvpJkWHd0YuuRUsQvKcyOtZGW8CwzHkdlZGIIzUOzLLPe5WvxIW5Wvjq0eaWdFp1V3q6fNFuoGWk2BN8UpedQDXwkua7LYzqCJhQ_Or1TJaxGm5MxpfV7ZLGca16tBUY-Cw0jz7vlVjnx97PJ-2MqqonYMCVTLJWoI7q0eSSKUTSLnzjJ-sn_nfxmfF-FdAQAA)
|
||||
- [After](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo2PzW6DMBCEX2XlVgIkBHcKUdo-RumBwqJYNbZlL5Eqy-9e_9DkkEtv3vHMzn6OrVygZd2HY3LakHXsVWtWM_rRcbBXFIRhtmo3c1R6Oxuu6TTKkfimlSF424mUhNWoDYqmzWOTo8XLKPv2npH94VZyFnz-HlxZwXCCSaChsniPGi5AF4SvZCwqn7rck5VcaySYL1wsBmWpjdKVj58jpWXgopQU1x52H_tz5ylwbGrhK8eFdWR29PUNO1v-Sy7CHe52SQ1N08RqCx4GeE7PsnpAz0Tg_twH2TmsWNDcwcZQuiFcJ7HjyKqEkLMh8Ajx6X8BPkQdmscBAAA=)
|
||||
|
||||
> When using custom elements, you should still use `<slot />` like before. In a future version, when Svelte removes its internal version of slots, it will leave those slots as-is, i.e. output a regular DOM tag instead of transforming it.
|
@ -1,7 +0,0 @@
|
||||
---
|
||||
title: More examples
|
||||
---
|
||||
|
||||
Check out [component-party.dev](https://component-party.dev/?f=svelte4,svelte5) to see side-by-side comparisons of some common patterns.
|
||||
|
||||
We'll add more examples over time, but if there's something in particular you'd like to see then hop over to the `#svelte-5-rc` channel of the [Svelte Discord](https://svelte.dev/chat).
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Examples"
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
---
|
||||
title: Frequently asked questions
|
||||
---
|
||||
|
||||
## Background and motivations
|
||||
|
||||
### What is this?
|
||||
|
||||
You're on the Svelte 5 preview site! If you don't know what Svelte is but somehow ended up here
|
||||
anyway, we suggest visiting [svelte.dev](https://svelte.dev) first to get familiar.
|
||||
|
||||
### What's special about Svelte 5?
|
||||
|
||||
Svelte 5 is a ground-up rewrite of the framework, designed to make your apps faster, smaller, and more robust.
|
||||
|
||||
It introduces <em>runes</em>, a powerful set of primitives for controlling reactivity inside your Svelte components and — for the first time — inside `.svelte.js` and `.svelte.ts` modules. You can learn about runes by reading the [Introducing runes](https://svelte.dev/blog/runes) blog post and watching the accompanying video, and by reading the preliminary [docs](/docs) on this site.
|
||||
|
||||
### Doesn't this make Svelte harder to learn?
|
||||
|
||||
Au contraire! Svelte today involves certain mental gymnastics:
|
||||
|
||||
- `let x` declares reactive state, but only at the top level of a component
|
||||
- `export let x` declares a prop, but only inside a component. `export const y = ...`, meanwhile, means something totally different
|
||||
- In addition to `export let`, you have to learn `$$props` and `$$restProps`
|
||||
- `$:` might be declaring a reactive binding, or running side-effects; like `let x`, it only works at the top level of a component. When these statements re-run is dependent on rules that are hard to understand
|
||||
- In general, code behaves differently inside and outside components, making refactoring difficult and requiring frequent context-switching
|
||||
|
||||
Runes, by contrast, are explicit, predictable and refactorable.
|
||||
|
||||
### Why can't we keep the old syntax?
|
||||
|
||||
Beyond the complexities listed above, the current design imposes some unfortunate limitations:
|
||||
|
||||
- There's no way to indicate which variables should _not_ be considered reactive. This becomes problematic when applying Svelte rules outside the top level of a component (for example in `.js` files)
|
||||
- The `$:` syntax doesn't play well with TypeScript. For example, you can't declare the type of `theme` in a statement like this — it's a syntax error:
|
||||
```ts
|
||||
// @errors: 2362 2363 2304 1005
|
||||
// @filename: ambient.d.ts
|
||||
declare global {
|
||||
const dark: boolean;
|
||||
}
|
||||
export {};
|
||||
// @filename: index.ts
|
||||
// ---cut---
|
||||
$: theme: 'light' | 'dark' = dark ? 'dark' : 'light';
|
||||
```
|
||||
But with runes, it works just fine:
|
||||
```ts
|
||||
// @filename: ambient.d.ts
|
||||
declare global {
|
||||
const dark: boolean;
|
||||
}
|
||||
export {};
|
||||
// @filename: index.ts
|
||||
// ---cut---
|
||||
let theme: 'light' | 'dark' = $derived(
|
||||
dark ? 'dark' : 'light'
|
||||
);
|
||||
```
|
||||
- Updating values inside `$:` statements can cause [confusing behaviour](https://github.com/sveltejs/svelte/issues/6732) and [impossible to resolve bugs](https://github.com/sveltejs/svelte/issues/4933) and the statements may run in an [unexpected order](https://github.com/sveltejs/svelte/issues/4516)
|
||||
- `$: {...}` doesn't let you return a cleanup function the way that [`$effect`](runes#$effect) does
|
||||
- Typing props is unduly cumbersome when you want to share interfaces between multiple components
|
||||
- Prefixing store names with `$` to access their values works in a `.svelte` file, but cannot work in `.js` and `.ts` without causing linting and typechecking errors. Having a unified approach to reactive state solves this problem
|
||||
|
||||
## Breaking changes and migration
|
||||
|
||||
### Is it a breaking change?
|
||||
|
||||
We're striving to make Svelte 5 a drop-in replacement for Svelte 4, and to that end we've ported over the entire test suite. The new features are opt-in, and you can mix-and-match the new stuff with the old stuff within an app (though not within a component — in 'runes mode', certain features are deliberately disabled).
|
||||
|
||||
Having said that, the underlying mechanisms are totally different. It's inevitable that some of you will hit edge cases, which is why this is a major version (5.0) rather than a minor (4.x).
|
||||
|
||||
### No but really, am I going to have to rewrite everything?
|
||||
|
||||
Eventually, you'll have to make some changes — most of which we hope to automate. We don't want to end up in a situation where people feel like they have to juggle knowledge of a bunch of different ways of doing things.
|
||||
|
||||
Our current plan is that some or all of the features that runes make unnecessary like `let`-style reactivity, `$:`, `$$props` and `$$restProps` will be deprecated in Svelte 6 and removed in Svelte 7. But don't worry — that won't happen for some time, and we'll provide automatic migration tooling to do as much of the change as possible. There are no plans to deprecate `onMount` or stores at the current time.
|
||||
|
||||
### Which things are disabled in runes mode?
|
||||
|
||||
When you opt into runes mode, you can no longer use the features that runes replace:
|
||||
|
||||
- `$state` replaces top-level `let` declarations implicitly creating reactive state
|
||||
- `$derived` replaces `$: x = ...`
|
||||
- `$effect` replaces `$: {'{ ... }'}`
|
||||
- `$props` replaces `export let`, `$$props` and `$$restProps`
|
||||
|
||||
All other features, including stores, are still fully supported in runes mode.
|
||||
|
||||
### Which things will be deprecated in Svelte 5?
|
||||
|
||||
`beforeUpdate` and `afterUpdate` are deprecated — use `$effect.pre` and `$effect` instead, as these are more conservative about when they run code. Everything else will remain.
|
||||
|
||||
### Prettier formatting is broken, what gives?
|
||||
|
||||
`svelte-lsp` ships with a stable version of Svelte to support the largest number of people out of the box. To make the language server compatible with Svelte 5 you will need to install the latest versions of `prettier` and `prettier-plugin-svelte` in your project and create (or update) a `.prettierrc` file:
|
||||
|
||||
```sh
|
||||
npm i --save-dev prettier-plugin-svelte prettier
|
||||
```
|
||||
|
||||
```json
|
||||
/// file: .prettierrc
|
||||
{
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": { "parser": "svelte" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule and future plans
|
||||
|
||||
### When is it coming out?
|
||||
|
||||
When it's done. The goal is sometime in 2024.
|
||||
|
||||
### Should I prepare my code for Svelte 5?
|
||||
|
||||
No. You can do the migration towards runes incrementally when Svelte 5 comes out.
|
||||
|
||||
### When can I `npm install` the Svelte 5 preview?
|
||||
|
||||
Right now!
|
||||
|
||||
```bash
|
||||
npm install svelte@next
|
||||
```
|
||||
|
||||
You can also opt into Svelte 5 when creating a new SvelteKit project:
|
||||
|
||||
```bash
|
||||
npx sv create
|
||||
```
|
||||
|
||||
### What's left to do?
|
||||
|
||||
A great many things. Transitions, for example, are not fully implemented. We also haven't fully solved all aspects of things like server-side rendering. We're getting there!
|
||||
|
||||
### Will feature X be part of 5.0?
|
||||
|
||||
If you have to ask, then probably not. Aside from runes, 5.0 is mostly about taking everything we've learned over the last few years (including from other frameworks — thanks friends!) and making Svelte the leanest and most powerful framework out there.
|
||||
|
||||
We know that some of you are very keen on certain feature ideas, and we are too. We have some big ideas for 5.1 and beyond.
|
||||
|
||||
## Discussion, contributing, and help
|
||||
|
||||
### I want to help. How do I contribute?
|
||||
|
||||
We appreciate your enthusiasm! We welcome issues on the [sveltejs/svelte](https://github.com/sveltejs/svelte) repo. Pull requests are a little dicier right now since many things are in flux, so we recommended starting with an issue.
|
||||
|
||||
### How can I share feedback or cool examples of what this enables?
|
||||
|
||||
You can use the `#svelte-5-rc` channel on the [Discord server](https://svelte.dev/chat) or the tag `#svelte-5` on social media.
|
||||
|
||||
### My question wasn't answered. What gives?
|
||||
|
||||
It must not have been asked frequently enough. To fix that, stop by the `#svelte-5-rc` channel of the [Discord server](https://svelte.dev/chat).
|
@ -1,436 +0,0 @@
|
||||
---
|
||||
title: Breaking changes
|
||||
---
|
||||
|
||||
While Svelte 5 is a complete rewrite, we have done our best to ensure that most codebases can upgrade with a minimum of hassle. That said, there are a few small breaking changes which may require action on your part. They are listed here.
|
||||
|
||||
## Components are no longer classes
|
||||
|
||||
In Svelte 3 and 4, components are classes. In Svelte 5 they are functions and should be instantiated differently. If you need to manually instantiate components, you should use `mount` or `hydrate` (imported from `svelte`) instead. If you see this error using SvelteKit, try updating to the latest version of SvelteKit first, which adds support for Svelte 5. If you're using Svelte without SvelteKit, you'll likely have a `main.js` file (or similar) which you need to adjust:
|
||||
|
||||
```diff
|
||||
+ import { mount } from 'svelte';
|
||||
import App from './App.svelte'
|
||||
|
||||
- const app = new App({ target: document.getElementById("app") });
|
||||
+ const app = mount(App, { target: document.getElementById("app") });
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
`mount` and `hydrate` have the exact same API. The difference is that `hydrate` will pick up the Svelte's server-rendered HTML inside its target and hydrate it. Both return an object with the exports of the component and potentially property accessors (if compiled with `accessors: true`). They do not come with the `$on`, `$set` and `$destroy` methods you may know from the class component API. These are its replacements:
|
||||
|
||||
For `$on`, instead of listening to events, pass them via the `events` property on the options argument.
|
||||
|
||||
```diff
|
||||
+ import { mount } from 'svelte';
|
||||
import App from './App.svelte'
|
||||
|
||||
- const app = new App({ target: document.getElementById("app") });
|
||||
- app.$on('event', callback);
|
||||
+ const app = mount(App, { target: document.getElementById("app"), events: { event: callback } });
|
||||
```
|
||||
|
||||
> Note that using `events` is discouraged — instead, [use callbacks](https://svelte-5-preview.vercel.app/docs/event-handlers)
|
||||
|
||||
For `$set`, use `$state` instead to create a reactive property object and manipulate it. If you're doing this inside a `.js` or `.ts` file, adjust the ending to include `.svelte`, i.e. `.svelte.js` or `.svelte.ts`.
|
||||
|
||||
```diff
|
||||
+ import { mount } from 'svelte';
|
||||
import App from './App.svelte'
|
||||
|
||||
- const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
|
||||
- app.$set('event', { foo: 'baz' });
|
||||
+ const props = $state({ foo: 'bar' });
|
||||
+ const app = mount(App, { target: document.getElementById("app"), props });
|
||||
+ props.foo = 'baz';
|
||||
```
|
||||
|
||||
For `$destroy`, use `unmount` instead.
|
||||
|
||||
```diff
|
||||
+ import { mount, unmount } from 'svelte';
|
||||
import App from './App.svelte'
|
||||
|
||||
- const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
|
||||
- app.$destroy();
|
||||
+ const app = mount(App, { target: document.getElementById("app") });
|
||||
+ unmount(app);
|
||||
```
|
||||
|
||||
As a stop-gap-solution, you can also use `createClassComponent` or `asClassComponent` (imported from `svelte/legacy`) instead to keep the same API known from Svelte 4 after instantiating.
|
||||
|
||||
```diff
|
||||
+ import { createClassComponent } from 'svelte/legacy';
|
||||
import App from './App.svelte'
|
||||
|
||||
- const app = new App({ target: document.getElementById("app") });
|
||||
+ const app = createClassComponent({ component: App, target: document.getElementById("app") });
|
||||
|
||||
export default app;
|
||||
```
|
||||
|
||||
If this component is not under your control, you can use the `compatibility.componentApi` compiler option for auto-applied backwards compatibility, which means code using `new Component(...)` keeps working without adjustments (note that this adds a bit of overhead to each component). This will also add `$set` and `$on` methods for all component instances you get through `bind:this`.
|
||||
|
||||
```js
|
||||
/// svelte.config.js
|
||||
export default {
|
||||
compilerOptions: {
|
||||
compatibility: {
|
||||
componentApi: 4
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Note that `mount` and `hydrate` are _not_ synchronous, so things like `onMount` won't have been called by the time the function returns and the pending block of promises will not have been rendered yet (because `#await` waits a microtask to wait for a potentially immediately-resolved promise). If you need that guarantee, call `flushSync` (import from `'svelte'`) after calling `mount/hydrate`.
|
||||
|
||||
### Server API changes
|
||||
|
||||
Similarly, components no longer have a `render` method when compiled for server side rendering. Instead, pass the function to `render` from `svelte/server`:
|
||||
|
||||
```diff
|
||||
+ import { render } from 'svelte/server';
|
||||
import App from './App.svelte';
|
||||
|
||||
- const { html, head } = App.render({ message: 'hello' });
|
||||
+ const { html, head } = render(App, { props: { message: 'hello' } });
|
||||
```
|
||||
|
||||
In Svelte 4, rendering a component to a string also returned the CSS of all components. In Svelte 5, this is no longer the case by default because most of the time you're using a tooling chain that takes care of it in other ways (like SvelteKit). If you need CSS to be returned from `render`, you can set the `css` compiler option to `'injected'` and it will add `<style>` elements to the `head`.
|
||||
|
||||
### Component typing changes
|
||||
|
||||
The change from classes towards functions is also reflected in the typings: `SvelteComponent`, the base class from Svelte 4, is deprecated in favour of the new `Component` type which defines the function shape of a Svelte component. To manually define a component shape in a `d.ts` file:
|
||||
|
||||
```ts
|
||||
import type { Component } from 'svelte';
|
||||
export declare const MyComponent: Component<{
|
||||
foo: string;
|
||||
}>;
|
||||
```
|
||||
|
||||
To declare that a component of a certain type is required:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
import {
|
||||
ComponentA,
|
||||
ComponentB
|
||||
} from 'component-library';
|
||||
|
||||
let component: Component<{ foo: string }> = $state(
|
||||
Math.random() ? ComponentA : ComponentB
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:component this={component} foo="bar" />
|
||||
```
|
||||
|
||||
The two utility types `ComponentEvents` and `ComponentType` are also deprecated. `ComponentEvents` is obsolete because events are defined as callback props now, and `ComponentType` is obsolete because the new `Component` type is the component type already (e.g. `ComponentType<SvelteComponent<{ prop: string }>>` == `Component<{ prop: string }>`).
|
||||
|
||||
### bind:this changes
|
||||
|
||||
Because components are no longer classes, using `bind:this` no longer returns a class instance with `$set`, `$on` and `$destroy` methods on it. It only returns the instance exports (`export function/const`) and, if you're using the `accessors` option, a getter/setter-pair for each property.
|
||||
|
||||
## Whitespace handling changed
|
||||
|
||||
Previously, Svelte employed a very complicated algorithm to determine if whitespace should be kept or not. Svelte 5 simplifies this which makes it easier to reason about as a developer. The rules are:
|
||||
|
||||
- Whitespace between nodes is collapsed to one whitespace
|
||||
- Whitespace at the start and end of a tag is removed completely
|
||||
- Certain exceptions apply such as keeping whitespace inside `pre` tags
|
||||
|
||||
As before, you can disable whitespace trimming by setting the `preserveWhitespace` option in your compiler settings or on a per-component basis in `<svelte:options>`.
|
||||
|
||||
## Modern browser required
|
||||
|
||||
Svelte 5 requires a modern browser (in other words, not Internet Explorer) for various reasons:
|
||||
|
||||
- it uses [`Proxies`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
|
||||
- elements with `clientWidth`/`clientHeight`/`offsetWidth`/`offsetHeight` bindings use a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) rather than a convoluted `<iframe>` hack
|
||||
- `<input type="range" bind:value={...} />` only uses an `input` event listener, rather than also listening for `change` events as a fallback
|
||||
|
||||
The `legacy` compiler option, which generated bulkier but IE-friendly code, no longer exists.
|
||||
|
||||
## Changes to compiler options
|
||||
|
||||
- The `false`/`true` (already deprecated previously) and the `"none"` values were removed as valid values from the `css` option
|
||||
- The `legacy` option was repurposed
|
||||
- The `hydratable` option has been removed. Svelte components are always hydratable now
|
||||
- The `enableSourcemap` option has been removed. Source maps are always generated now, tooling can choose to ignore it
|
||||
- The `tag` option was removed. Use `<svelte:options customElement="tag-name" />` inside the component instead
|
||||
- The `loopGuardTimeout`, `format`, `sveltePath`, `errorMode` and `varsReport` options were removed
|
||||
|
||||
## The `children` prop is reserved
|
||||
|
||||
Content inside component tags becomes a [snippet prop](/docs/snippets) called `children`. You cannot have a separate prop by that name.
|
||||
|
||||
## Dot notation indicates a component
|
||||
|
||||
In Svelte 4, `<foo.bar>` would create an element with a tag name of `"foo.bar"`. In Svelte 5, `foo.bar` is treated as a component instead. This is particularly useful inside `each` blocks:
|
||||
|
||||
```svelte
|
||||
{#each items as item}
|
||||
<item.component {...item.props} />
|
||||
{/each}
|
||||
```
|
||||
|
||||
## Breaking changes in runes mode
|
||||
|
||||
Some breaking changes only apply once your component is in runes mode.
|
||||
|
||||
### Bindings to component exports are not allowed
|
||||
|
||||
Exports from runes mode components cannot be bound to directly. For example, having `export const foo = ...` in component `A` and then doing `<A bind:foo />` causes an error. Use `bind:this` instead — `<A bind:this={a} />` — and access the export as `a.foo`. This change makes things easier to reason about, as it enforces a clear separation between props and exports.
|
||||
|
||||
### Bindings need to be explicitly defined using `$bindable()`
|
||||
|
||||
In Svelte 4 syntax, every property (declared via `export let`) is bindable, meaning you can `bind:` to it. In runes mode, properties are not bindable by default: you need to denote bindable props with the [`$bindable`](/docs/runes#$bindable) rune.
|
||||
|
||||
If a bindable property has a default value (e.g. `let { foo = $bindable('bar') } = $props();`), you need to pass a non-`undefined` value to that property if you're binding to it. This prevents ambiguous behavior — the parent and child must have the same value — and results in better performance (in Svelte 4, the default value was reflected back to the parent, resulting in wasteful additional render cycles).
|
||||
|
||||
### `accessors` option is ignored
|
||||
|
||||
Setting the `accessors` option to `true` makes properties of a component directly accessible on the component instance. In runes mode, properties are never accessible on the component instance. You can use component exports instead if you need to expose them.
|
||||
|
||||
### `immutable` option is ignored
|
||||
|
||||
Setting the `immutable` option has no effect in runes mode. This concept is replaced by how `$state` and its variations work.
|
||||
|
||||
### Classes are no longer "auto-reactive"
|
||||
|
||||
In Svelte 4, doing the following triggered reactivity:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let foo = new Foo();
|
||||
</script>
|
||||
|
||||
<button on:click={() => (foo.value = 1)}>{foo.value}</button
|
||||
>
|
||||
```
|
||||
|
||||
This is because the Svelte compiler treated the assignment to `foo.value` as an instruction to update anything that referenced `foo`. In Svelte 5, reactivity is determined at runtime rather than compile time, so you should define `value` as a reactive `$state` field on the `Foo` class. Wrapping `new Foo()` with `$state(...)` will have no effect — only vanilla objects and arrays are made deeply reactive.
|
||||
|
||||
### `<svelte:component>` is no longer necessary
|
||||
|
||||
In Svelte 4, components are _static_ — if you render `<Thing>`, and the value of `Thing` changes, [nothing happens](https://svelte.dev/repl/7f1fa24f0ab44c1089dcbb03568f8dfa?version=4.2.18). To make it dynamic you must use `<svelte:component>`.
|
||||
|
||||
This is [no longer true in Svelte 5](/#H4sIAAAAAAAAE4WQwU7DMAyGX8VESANpXe8lq9Q8AzfGobQujZQmWeJOQlXenaQB1sM0bnG-379_e2GDVOhZ9bYw3U7IKtZYy_aMvmwq_AUVYay9mV2XfrjvnLRUn_SJ5GSNI2hgcGaC3aFsDrlh97LB4g-LLY4ChQSvo9SfcIRHTy3h03NEvLzO0Nyjwo7gQ-q-urRqxuOy9oQ1AjeWpNHwQ5pQN7zMf7e4CLXY8Dhpdc-THooCaESP0DoEPM8ydqEmKIqkzUnL9MxrVJ2JG-qkoFH631xREg82mV4OEntWkZsx7K_3vXtdm_LbuwbiHwNx2-A9fANfmchv7QEAAA==):
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import A from './A.svelte';
|
||||
import B from './B.svelte';
|
||||
|
||||
let Thing = $state();
|
||||
</script>
|
||||
|
||||
<select bind:value={Thing}>
|
||||
<option value={A}>A</option>
|
||||
<option value={B}>B</option>
|
||||
</select>
|
||||
|
||||
<!-- these are equivalent -->
|
||||
<Thing />
|
||||
<svelte:component this={Thing} />
|
||||
```
|
||||
|
||||
### Touch and wheel events are passive
|
||||
|
||||
When using `onwheel`, `onmousewheel`, `ontouchstart` and `ontouchmove` event attributes, the handlers are [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) to align with browser defaults. This greatly improves responsiveness by allowing the browser to scroll the document immediately, rather than waiting to see if the event handler calls `event.preventDefault()`.
|
||||
|
||||
In the very rare cases that you need to prevent these event defaults, you should use [`on`](https://svelte-5-preview.vercel.app/docs/imports#svelte-events) instead (for example inside an action).
|
||||
|
||||
### Attribute/prop syntax is stricter
|
||||
|
||||
In Svelte 4, complex attribute values needn't be quoted:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```svelte
|
||||
<Component prop=this{is}valid />
|
||||
```
|
||||
|
||||
This is a footgun. In runes mode, if you want to concatenate stuff you must wrap the value in quotes:
|
||||
|
||||
```svelte
|
||||
<Component prop="this{is}valid" />
|
||||
```
|
||||
|
||||
Note that Svelte 5 will also warn if you have a single expression wrapped in quotes, like `answer="{42}"` — in Svelte 6, that will cause the value to be converted to a string, rather than passed as a number.
|
||||
|
||||
### HTML structure is stricter
|
||||
|
||||
In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server side rendering it. For example you could write this...
|
||||
|
||||
```svelte
|
||||
<table>
|
||||
<tr>
|
||||
<td>hi</td>
|
||||
</tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
... and the browser would auto-insert a `<tbody>` element:
|
||||
|
||||
```svelte
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>hi</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
Svelte 5 is more strict about the HTML structure and will throw a compiler error in cases where the browser would repair the DOM.
|
||||
|
||||
## Other breaking changes
|
||||
|
||||
### Stricter `@const` assignment validation
|
||||
|
||||
Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed.
|
||||
|
||||
### :is(...), :where(...), :has(...) and :not(...) are scoped
|
||||
|
||||
Previously, Svelte did not analyse selectors inside `:is(...)`, `:where(...)`, `:has(...)` and `:not(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:where(...)/:has(...)/:not(...)` selectors.
|
||||
|
||||
When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors:
|
||||
|
||||
```diff
|
||||
- main {
|
||||
+ main :global {
|
||||
@apply bg-blue-100 dark:bg-blue-900
|
||||
}
|
||||
```
|
||||
|
||||
### CSS hash position no longer deterministic
|
||||
|
||||
Previously Svelte would always insert the CSS hash last. This is no longer guaranteed in Svelte 5. This is only breaking if you [have very weird css selectors](https://stackoverflow.com/questions/15670631/does-the-order-of-classes-listed-on-an-item-affect-the-css).
|
||||
|
||||
### Scoped CSS uses :where(...)
|
||||
|
||||
To avoid issues caused by unpredictable specificity changes, scoped CSS selectors now use `:where(.svelte-xyz123)` selector modifiers alongside `.svelte-xyz123` (where `xyz123` is, as previously, a hash of the `<style>` contents). You can read more detail [here](https://github.com/sveltejs/svelte/pull/10443).
|
||||
|
||||
In the event that you need to support ancient browsers that don't implement `:where`, you can manually alter the emitted CSS, at the cost of unpredictable specificity changes:
|
||||
|
||||
```js
|
||||
// @errors: 2552
|
||||
css = css.replace(/:where\((.+?)\)/, '$1');
|
||||
```
|
||||
|
||||
### Error/warning codes have been renamed
|
||||
|
||||
Error and warning codes have been renamed. Previously they used dashes to separate the words, they now use underscores (e.g. foo-bar becomes foo_bar). Additionally, a handful of codes have been reworded slightly.
|
||||
|
||||
### Reduced number of namespaces
|
||||
|
||||
The number of valid namespaces you can pass to the compiler option `namespace` has been reduced to `html` (the default), `mathml` and `svg`.
|
||||
|
||||
The `foreign` namespace was only useful for Svelte Native, which we're planning to support differently in a 5.x minor.
|
||||
|
||||
### beforeUpdate/afterUpdate changes
|
||||
|
||||
`beforeUpdate` no longer runs twice on initial render if it modifies a variable referenced in the template.
|
||||
|
||||
`afterUpdate` callbacks in a parent component will now run after `afterUpdate` callbacks in any child components.
|
||||
|
||||
Both functions are disallowed in runes mode — use `$effect.pre(...)` and `$effect(...)` instead.
|
||||
|
||||
### `contenteditable` behavior change
|
||||
|
||||
If you have a `contenteditable` node with a corresponding binding _and_ a reactive value inside it (example: `<div contenteditable=true bind:textContent>count is {count}</div>`), then the value inside the contenteditable will not be updated by updates to `count` because the binding takes full control over the content immediately and it should only be updated through it.
|
||||
|
||||
### `oneventname` attributes no longer accept string values
|
||||
|
||||
In Svelte 4, it was possible to specify event attributes on HTML elements as a string:
|
||||
|
||||
```svelte
|
||||
<button onclick="alert('hello')">...</button>
|
||||
```
|
||||
|
||||
This is not recommended, and is no longer possible in Svelte 5, where properties like `onclick` replace `on:click` as the mechanism for adding [event handlers](/docs/event-handlers).
|
||||
|
||||
### `null` and `undefined` become the empty string
|
||||
|
||||
In Svelte 4, `null` and `undefined` were printed as the corresponding string. In 99 out of 100 cases you want this to become the empty string instead, which is also what most other frameworks out there do. Therefore, in Svelte 5, `null` and `undefined` become the empty string.
|
||||
|
||||
### `bind:files` values can only be `null`, `undefined` or `FileList`
|
||||
|
||||
`bind:files` is now a two-way binding. As such, when setting a value, it needs to be either falsy (`null` or `undefined`) or of type `FileList`.
|
||||
|
||||
### Bindings now react to form resets
|
||||
|
||||
Previously, bindings did not take into account `reset` event of forms, and therefore values could get out of sync with the DOM. Svelte 5 fixes this by placing a `reset` listener on the document and invoking bindings where necessary.
|
||||
|
||||
### `walk` not longer exported
|
||||
|
||||
`svelte/compiler` reexported `walk` from `estree-walker` for convenience. This is no longer true in Svelte 5, import it directly from that package instead in case you need it.
|
||||
|
||||
### Content inside `svelte:options` is forbidden
|
||||
|
||||
In Svelte 4 you could have content inside a `<svelte:options />` tag. It was ignored, but you could write something in there. In Svelte 5, content inside that tag is a compiler error.
|
||||
|
||||
### `<slot>` elements in declarative shadow roots are preserved
|
||||
|
||||
Svelte 4 replaced the `<slot />` tag in all places with its own version of slots. Svelte 5 preserves them in the case they are a child of a `<template shadowrootmode="...">` element.
|
||||
|
||||
### `<svelte:element>` tag must be an expression
|
||||
|
||||
In Svelte 4, `<svelte:element this="div">` is valid code. This makes little sense — you should just do `<div>`. In the vanishingly rare case that you _do_ need to use a literal value for some reason, you can do this:
|
||||
|
||||
```diff
|
||||
- <svelte:element this="div">
|
||||
+ <svelte:element this={"div"}>
|
||||
```
|
||||
|
||||
Note that whereas Svelte 4 would treat `<svelte:element this="input">` (for example) identically to `<input>` for the purposes of determining which `bind:` directives could be applied, Svelte 5 does not.
|
||||
|
||||
### `mount` plays transitions by default
|
||||
|
||||
The `mount` function used to render a component tree plays transitions by default unless the `intro` option is set to `false`. This is different from legacy class components which, when manually instantiated, didn't play transitions by default.
|
||||
|
||||
### `<img src={...}>` and `{@html ...}` hydration mismatches are not repaired
|
||||
|
||||
In Svelte 4, if the value of a `src` attribute or `{@html ...}` tag differ between server and client (a.k.a. a hydration mismatch), the mismatch is repaired. This is very costly: setting a `src` attribute (even if it evaluates to the same thing) causes images and iframes to be reloaded, and reinserting a large blob of HTML is slow.
|
||||
|
||||
Since these mismatches are extremely rare, Svelte 5 assumes that the values are unchanged, but in development will warn you if they are not. To force an update you can do something like this:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { markup, src } = $props();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// stash the values...
|
||||
const initial = { markup, src };
|
||||
|
||||
// unset them...
|
||||
markup = src = undefined;
|
||||
|
||||
$effect(() => {
|
||||
// ...and reset after we've mounted
|
||||
markup = initial.markup;
|
||||
src = initial.src;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{@html markup}
|
||||
<img {src} />
|
||||
```
|
||||
|
||||
### Hydration works differently
|
||||
|
||||
Svelte 5 makes use of comments during server side rendering which are used for more robust and efficient hydration on the client. As such, you shouldn't remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
|
||||
|
||||
### `onevent` attributes are delegated
|
||||
|
||||
Event attributes replace event directives: Instead of `on:click={handler}` you write `onclick={handler}`. For backwards compatibility the `on:event` syntax is still supported and behaves the same as in Svelte 4. Some of the `onevent` attributes however are delegated, which means you need to take care to not stop event propagation on those manually, as they then might never reach the listener for this event type at the root.
|
||||
|
||||
### `--style-props` uses a different element
|
||||
|
||||
Svelte 5 uses an extra `<svelte-css-wrapper>` element instead of a `<div>` to wrap the component when using CSS custom properties.
|
||||
|
||||
<!-- TODO in final docs, add link to corresponding section for more details -->
|
@ -1,130 +0,0 @@
|
||||
---
|
||||
title: Deprecations
|
||||
---
|
||||
|
||||
Aside from the [breaking changes](/docs/breaking-changes) listed on the previous page, Svelte 5 should be a drop-in replacement for Svelte 4. That said, there are some features that we will remove in a future major version of Svelte, and we encourage you to update your apps now to avoid breaking changes in future.
|
||||
|
||||
## beforeUpdate and afterUpdate
|
||||
|
||||
`beforeUpdate(fn)` schedules the `fn` callback to run immediately before any changes happen inside the current component. `afterUpdate(fn)` schedules it to run after any changes have taken effect.
|
||||
|
||||
These functions run indiscriminately when _anything_ changes. By using `$effect.pre` and `$effect` instead, we can ensure that work only happens when the things we care about have changed. The difference is visible in this example — [with `afterUpdate`](/#H4sIAAAAAAAAE21STW-DMAz9K140CSpVtJddUmDaj5i0aezAwKBI-VJi6CqU_74AY-WwiyPbz37PdibWCYme8Y-J6Voh4-zFWnZkdLOz40eUhNH3ZnDNHMl944SlstIVCWWNI5ig7gjdq21rQgjQOaMgWUuTSwRGqESCxhjXeijg0VNEphN8czgf4RYthMNlwxEqi66mweEd_HTeARzq9p5KsixL1uyGsA7HCNh1-tWxU5qmByhKmJY6aoz2RmImTZ8mbtBa6H4_10ZAqxUdpHudD0WxkB62fhVtKvewclX2DEmPRDPFtXYKXQL8Hop7kjG08dH_w8REmJ9lcfnpfhadr6vnV6FbcwWjuTKDR2VGLKYUl6n_brEcAbNGCtT0thxj897jLQOc1p5C2yFuPn6LomKu1j1WDL4iAx9rOcTGO3kBYk1uy2lZQchPtoxfSJlWdAJbxskNGD7DD-pLlz59AgAA), the callback runs on every `mousemove` event, whereas [with `$effect`](/#H4sIAAAAAAAAE21SwW6EIBD9lSnZRDfZuHvphapN_6JN7cHqaEgQCIxuG8O_F7VUDw0JZOY93gxvmFknJDrG32em6gEZZy_GsAujb7MEbkJJGGKnR9ssmdw1VhgqK1WRRIJGa9s6KODkqCZMZ_jicLvAd9jBn58ij3AwaGsaLe7kx9uBYFG1O5RkWZZsaGQYi1MgHJQWOIAn7DpsKE3PUJQwr3eo0cppiZnUfZrYUSmhevhlRmHadtFBeuzvoSjWYueoVVHs7kgrt46eIemRaJG_13ZAmwDfU8EfGVKxHv3_iAD45VgNy6-7xyrfRsDvQrX6DlrxQY8OBz1hMae4vvhvBqv5mDVSoKLXdQgxegMf1nXTFMqMwfEw46JitlY9Vgw-QwU-1XIMwof2PIQ7uSnn1QKfX00Z_sOgW9EJbBknO6L_8D9aLfICSgIAAA==), the function only runs when `temperature` changes:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- import { afterUpdate } from 'svelte';
|
||||
|
||||
let coords = $state({ x: 0, y: 0 });
|
||||
let temperature = $state(50);
|
||||
let trend = $state('...');
|
||||
|
||||
let prev = temperature;
|
||||
|
||||
- afterUpdate(() => {
|
||||
- console.log('running afterUpdate');
|
||||
+ $effect(() => {
|
||||
+ console.log('running $effect');
|
||||
|
||||
if (temperature !== prev) {
|
||||
trend = temperature > prev ? 'getting warmer' : 'getting cooler';
|
||||
prev = temperature;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:mousemove={(e) => coords = { x: e.clientX, y: e.clientY } } />
|
||||
|
||||
<input type="range" bind:value={temperature} >
|
||||
<p>{trend}</p>
|
||||
```
|
||||
|
||||
Note that using `$effect` and `$effect.pre` will put you in [runes mode](/docs/runes) — be sure to update your props and state accordingly.
|
||||
|
||||
## `createEventDispatcher`
|
||||
|
||||
`createEventDispatcher` returns a function from which you can dispatch custom events. The usage is somewhat boilerplate-y, but it was encouraged in Svelte 4 due to consistency with how you listen to dom events (via `on:click` for example).
|
||||
|
||||
Svelte 5 introduces [event attributes](/docs/event-handlers) which deprecate event directives (`onclick` instead of `on:click`), and as such we also encourage you to use callback properties for events instead:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- import { createEventDispatcher } from 'svelte';
|
||||
- const dispatch = createEventDispatcher();
|
||||
+ let { greet } = $props();
|
||||
|
||||
- function greet() {
|
||||
- dispatch('greet');
|
||||
- }
|
||||
</script>
|
||||
|
||||
<button
|
||||
- on:click={greet}
|
||||
+ onclick={greet}
|
||||
>greet</button>
|
||||
```
|
||||
|
||||
When authoring custom elements, use the new [host rune](/docs/runes#$host) to dispatch events (among other things):
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- import { createEventDispatcher } from 'svelte';
|
||||
- const dispatch = createEventDispatcher();
|
||||
|
||||
function greet() {
|
||||
- dispatch('greet');
|
||||
+ $host().dispatchEvent(new CustomEvent('greet'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
- on:click={greet}
|
||||
+ onclick={greet}
|
||||
>greet</button>
|
||||
```
|
||||
|
||||
Note that using `$props` and `$host` will put you in [runes mode](/docs/runes) — be sure to update your props and state accordingly.
|
||||
|
||||
## `<svelte:component>` in runes mode
|
||||
|
||||
In previous versions of Svelte, the component constructor was fixed when the component was rendered. In other words, if you wanted `<X>` to re-render when `X` changed, you would either have to use `<svelte:component this={X}>` or put the component inside a `{#key X}...{/key}` block.
|
||||
|
||||
In Svelte 5 this is no longer true — if `X` changes, `<X>` re-renders.
|
||||
|
||||
In some cases `<object.property>` syntax can be used as a replacement; a lowercased variable with property access is recognized as a component in Svelte 5.
|
||||
|
||||
For complex component resolution logic, an intermediary, capitalized variable may be necessary. E.g. in places where `@const` can be used:
|
||||
|
||||
```diff
|
||||
{#each items as item}
|
||||
- <svelte:component this={item.condition ? Y : Z} />
|
||||
+ {@const Component = item.condition ? Y : Z}
|
||||
+ <Component />
|
||||
{/each}
|
||||
```
|
||||
|
||||
A derived value may be used in other contexts:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
...
|
||||
let condition = $state(false);
|
||||
+ const Component = $derived(condition ? Y : Z);
|
||||
</script>
|
||||
- <svelte:component this={condition ? Y : Z} />
|
||||
+ <Component />
|
||||
```
|
||||
|
||||
## `immutable`
|
||||
|
||||
The `immutable` compiler option is deprecated. Use runes mode instead, where all state is immutable (which means that assigning to `object.property` won't cause updates for anything that is observing `object` itself, or a different property of it).
|
||||
|
||||
## `context="module"`
|
||||
|
||||
`context="module"` is deprecated, use the new `module` attribute instead:
|
||||
|
||||
```diff
|
||||
- <script context="module">
|
||||
+ <script module>
|
||||
...
|
||||
</script>
|
||||
```
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Appendix"
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
redirect(308, '/docs/imports');
|
||||
}
|
@ -1,203 +0,0 @@
|
||||
import { base as app_base } from '$app/paths';
|
||||
import {
|
||||
escape,
|
||||
extractFrontmatter,
|
||||
markedTransform,
|
||||
normalizeSlugify,
|
||||
removeMarkdown,
|
||||
renderContentMarkdown
|
||||
} from '@sveltejs/site-kit/markdown';
|
||||
|
||||
/**
|
||||
* @param {import('./types').DocsData} docs_data
|
||||
* @param {string} slug
|
||||
*/
|
||||
export async function get_parsed_docs(docs_data, slug) {
|
||||
for (const { pages } of docs_data) {
|
||||
for (const page of pages) {
|
||||
if (page.slug === slug) {
|
||||
return {
|
||||
...page,
|
||||
content: await render_content(page.file, page.content)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return {Promise<import('./types').DocsData>} */
|
||||
export async function get_docs_data(base = './src/routes/docs/content') {
|
||||
const { readdir, readFile } = await import('node:fs/promises');
|
||||
|
||||
/** @type {import('./types').DocsData} */
|
||||
const docs_data = [];
|
||||
|
||||
for (const category_dir of await readdir(base)) {
|
||||
const match = /\d{2}-(.+)/.exec(category_dir);
|
||||
if (!match) continue;
|
||||
|
||||
const category_slug = match[1];
|
||||
|
||||
// Read the meta.json
|
||||
const { title: category_title, draft = 'false' } = JSON.parse(
|
||||
await readFile(`${base}/${category_dir}/meta.json`, 'utf-8')
|
||||
);
|
||||
|
||||
if (draft === 'true') continue;
|
||||
|
||||
/** @type {import('./types').Category} */
|
||||
const category = {
|
||||
title: category_title,
|
||||
slug: category_slug,
|
||||
pages: []
|
||||
};
|
||||
|
||||
for (const filename of await readdir(`${base}/${category_dir}`)) {
|
||||
if (filename === 'meta.json') continue;
|
||||
const match = /\d{2}-(.+)/.exec(filename);
|
||||
if (!match) continue;
|
||||
|
||||
const page_slug = match[1].replace('.md', '');
|
||||
|
||||
const page_data = extractFrontmatter(
|
||||
await readFile(`${base}/${category_dir}/${filename}`, 'utf-8')
|
||||
);
|
||||
|
||||
if (page_data.metadata.draft === 'true') continue;
|
||||
|
||||
const page_title = page_data.metadata.title;
|
||||
const page_content = page_data.body;
|
||||
|
||||
category.pages.push({
|
||||
title: page_title,
|
||||
slug: page_slug,
|
||||
content: page_content,
|
||||
category: category_title,
|
||||
sections: await get_sections(page_content),
|
||||
path: `${app_base}/docs/${page_slug}`,
|
||||
file: `${category_dir}/${filename}`
|
||||
});
|
||||
}
|
||||
|
||||
docs_data.push(category);
|
||||
}
|
||||
|
||||
return docs_data;
|
||||
}
|
||||
|
||||
/** @param {import('./types').DocsData} docs_data */
|
||||
export function get_docs_list(docs_data) {
|
||||
return docs_data.map((category) => ({
|
||||
title: category.title,
|
||||
pages: category.pages.map((page) => ({
|
||||
title: page.title,
|
||||
path: page.path
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
const titled = async (str) =>
|
||||
removeMarkdown(
|
||||
escape(await markedTransform(str, { paragraph: (txt) => txt }))
|
||||
.replace(/<\/?code>/g, '')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/, '&')
|
||||
.replace(/<(\/)?(em|b|strong|code)>/g, '')
|
||||
);
|
||||
|
||||
/** @param {string} markdown */
|
||||
export async function get_sections(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
const root = /** @type {import('./types').Section} */ ({
|
||||
title: 'Root',
|
||||
slug: 'root',
|
||||
sections: [],
|
||||
breadcrumbs: [''],
|
||||
text: ''
|
||||
});
|
||||
let currentNodes = [root];
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^(#{2,4})\s(.*)/);
|
||||
if (match) {
|
||||
const level = match[1].length - 2;
|
||||
const text = await titled(match[2]);
|
||||
const slug = normalizeSlugify(text);
|
||||
|
||||
// Prepare new node
|
||||
/** @type {import('./types').Section} */
|
||||
const newNode = {
|
||||
title: text,
|
||||
slug,
|
||||
sections: [],
|
||||
breadcrumbs: [...currentNodes[level].breadcrumbs, text],
|
||||
text: ''
|
||||
};
|
||||
|
||||
// Add the new node to the tree
|
||||
currentNodes[level].sections.push(newNode);
|
||||
|
||||
// Prepare for potential children of the new node
|
||||
currentNodes = currentNodes.slice(0, level + 1);
|
||||
currentNodes.push(newNode);
|
||||
} else if (line.trim() !== '') {
|
||||
// Add non-heading line to the text of the current section
|
||||
currentNodes[currentNodes.length - 1].text += line + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return root.sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {string} body
|
||||
* @returns
|
||||
*/
|
||||
const render_content = (filename, body) =>
|
||||
renderContentMarkdown(filename, body, {
|
||||
cacheCodeSnippets: true,
|
||||
|
||||
twoslashBanner: (filename, source) => {
|
||||
const injected = [
|
||||
`// @filename: runes.d.ts`,
|
||||
`declare function $props(): any`,
|
||||
`declare function $state<T>(initial: T): T`,
|
||||
`declare function $derived<T>(value: T): T`,
|
||||
`declare const $effect: ((callback: () => void | (() => void)) => void) & { pre: (callback: () => void | (() => void)) => void };`
|
||||
];
|
||||
|
||||
if (/(svelte)/.test(source) || filename.includes('typescript')) {
|
||||
injected.push(`// @filename: ambient.d.ts`, `/// <reference types="svelte" />`);
|
||||
}
|
||||
|
||||
if (filename.includes('svelte-compiler')) {
|
||||
injected.push('// @esModuleInterop');
|
||||
}
|
||||
|
||||
if (filename.includes('svelte.md')) {
|
||||
injected.push('// @errors: 2304');
|
||||
}
|
||||
|
||||
// Actions JSDoc examples are invalid. Too many errors, edge cases
|
||||
if (filename.includes('svelte-action')) {
|
||||
injected.push('// @noErrors');
|
||||
}
|
||||
|
||||
if (filename.includes('typescript')) {
|
||||
injected.push('// @errors: 2304');
|
||||
}
|
||||
|
||||
// Tutorials
|
||||
if (filename.startsWith('tutorial')) {
|
||||
injected.push('// @noErrors');
|
||||
}
|
||||
|
||||
return injected.join('\n');
|
||||
}
|
||||
});
|
@ -1,26 +0,0 @@
|
||||
export type DocsData = Category[];
|
||||
|
||||
export interface Section {
|
||||
title: string;
|
||||
slug: string;
|
||||
// Currently, we are only going with 2 level headings, so this will be undefined. In future, we may want to support 3 levels, in which case this will be a list of sections
|
||||
sections?: Section[];
|
||||
breadcrumbs: string[];
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type Category = {
|
||||
title: string;
|
||||
slug: string;
|
||||
pages: Page[];
|
||||
};
|
||||
|
||||
export type Page = {
|
||||
title: string;
|
||||
category: string;
|
||||
slug: string;
|
||||
file: string;
|
||||
path: string;
|
||||
content: string;
|
||||
sections: Section[];
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
redirect(308, '/docs/faq');
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/** @param {string} input */
|
||||
export async function compress_and_encode_text(input) {
|
||||
const reader = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip')).getReader();
|
||||
let buffer = '';
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
reader.releaseLock();
|
||||
return btoa(buffer).replaceAll('+', '-').replaceAll('/', '_');
|
||||
} else {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
// decoding as utf-8 will make btoa reject the string
|
||||
buffer += String.fromCharCode(value[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} input */
|
||||
export async function decode_and_decompress_text(input) {
|
||||
const decoded = atob(input.replaceAll('-', '+').replaceAll('_', '/'));
|
||||
// putting it directly into the blob gives a corrupted file
|
||||
const u8 = new Uint8Array(decoded.length);
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
u8[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
const stream = new Blob([u8]).stream().pipeThrough(new DecompressionStream('gzip'));
|
||||
return new Response(stream).text();
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { get_docs_data, get_docs_list } from '../docs/render';
|
||||
|
||||
export const GET = async () => {
|
||||
const docs_list = get_docs_list(await get_docs_data());
|
||||
const processed_docs_list = docs_list.map(({ title, pages }) => ({
|
||||
title,
|
||||
sections: pages.map(({ title, path }) => ({ title, path }))
|
||||
}));
|
||||
|
||||
return json([
|
||||
{
|
||||
title: 'Docs',
|
||||
prefix: 'docs',
|
||||
pathname: '/docs/introduction',
|
||||
sections: [
|
||||
{
|
||||
title: 'DOCS',
|
||||
sections: processed_docs_list
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
prefix: 'status',
|
||||
pathname: '/status'
|
||||
}
|
||||
]);
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
import results from './results.json';
|
||||
|
||||
export async function load() {
|
||||
let total = 0;
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
let skipped = 0;
|
||||
|
||||
const suites = results.testResults
|
||||
.map((suite) => {
|
||||
let name = suite.name.split('/').at(-2);
|
||||
|
||||
if (name === 'runtime-browser') {
|
||||
// special case
|
||||
name = suite.assertionResults[0].ancestorTitles[1];
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
tests: suite.assertionResults.map((test) => {
|
||||
total += 1;
|
||||
if (test.status === 'passed') passed += 1;
|
||||
if (test.status === 'failed') failed += 1;
|
||||
if (test.status === 'skipped') skipped += 1;
|
||||
return {
|
||||
title: test.title,
|
||||
status: test.status
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (a.name < b.name ? -1 : +1));
|
||||
|
||||
return {
|
||||
nav_title: 'Status',
|
||||
results: {
|
||||
suites,
|
||||
total,
|
||||
passed,
|
||||
failed,
|
||||
skipped
|
||||
}
|
||||
};
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import results from '../results.json';
|
||||
|
||||
export function GET() {
|
||||
return json(results);
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import compiler_js from '../../../../../../packages/svelte/compiler/index.js?url';
|
||||
import package_json from '../../../../../../packages/svelte/package.json?url';
|
||||
import { read } from '$app/server';
|
||||
|
||||
const files = import.meta.glob('../../../../../../packages/svelte/src/**/*.js', {
|
||||
eager: true,
|
||||
query: '?url',
|
||||
import: 'default'
|
||||
});
|
||||
|
||||
const prefix = '../../../../../../packages/svelte/';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export function entries() {
|
||||
const entries = Object.keys(files).map((path) => ({ path: path.replace(prefix, '') }));
|
||||
entries.push({ path: 'compiler/index.js' }, { path: 'package.json' });
|
||||
return entries;
|
||||
}
|
||||
|
||||
// service worker requests files under this path to load the compiler and runtime
|
||||
export async function GET({ params }) {
|
||||
let file = '';
|
||||
|
||||
if (params.path === 'compiler/index.js') {
|
||||
file = compiler_js;
|
||||
} else if (params.path === 'package.json') {
|
||||
file = package_json;
|
||||
} else {
|
||||
file = /** @type {string} */ (files[prefix + params.path]);
|
||||
|
||||
// remove query string added by Vite when changing source code locally
|
||||
file = file.split('?')[0];
|
||||
}
|
||||
|
||||
return read(file);
|
||||
}
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,26 +0,0 @@
|
||||
import adapter from '@sveltejs/adapter-vercel';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
export default {
|
||||
compilerOptions: {
|
||||
compatibility: {
|
||||
// site-kit manually instantiates components inside an action
|
||||
componentApi: 4
|
||||
}
|
||||
},
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
runtime: 'nodejs18.x'
|
||||
}),
|
||||
|
||||
prerender: {
|
||||
handleMissingId(details) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
vitePlugin: {
|
||||
inspector: false
|
||||
}
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"$schema": "http://openapi.vercel.sh/vercel.json",
|
||||
"outputDirectory": "sites/svelte-5-preview/.vercel",
|
||||
"buildCommand": "cd ../../ && pnpm build && (pnpm test-output || true) && pnpm preview-site"
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
plugins: [sveltekit()],
|
||||
resolve: {
|
||||
dedupe: ['@codemirror/state', '@codemirror/language', '@codemirror/view']
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@sveltejs/site-kit', '@sveltejs/kit', 'svelte']
|
||||
},
|
||||
ssr: { noExternal: ['@sveltejs/site-kit', '@sveltejs/kit', 'svelte'] },
|
||||
server: {
|
||||
fs: {
|
||||
strict: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
Loading…
Reference in new issue