port some to svelte5

pull/12817/head
Ottomated 2 years ago
parent 9ba370f305
commit 4ff7f8cada

@ -3,43 +3,34 @@
</script> </script>
<script> <script>
/** @import { Lang } from './types' */
import { historyField } from '@codemirror/commands'; import { historyField } from '@codemirror/commands';
import { EditorState, Range, StateEffect, StateEffectType, StateField } from '@codemirror/state'; import { EditorState, Range, StateEffect, StateEffectType, StateField } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view'; import { Decoration, EditorView } from '@codemirror/view';
import { codemirror, withCodemirrorInstance } from '@neocodemirror/svelte'; import { codemirror, withCodemirrorInstance } from '@neocodemirror/svelte';
import { svelteLanguage } from '@replit/codemirror-lang-svelte'; import { svelteLanguage } from '@replit/codemirror-lang-svelte';
import { javascriptLanguage } from '@codemirror/lang-javascript'; import { javascriptLanguage } from '@codemirror/lang-javascript';
import { createEventDispatcher, tick } from 'svelte'; import { tick } from 'svelte';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { get_repl_context } from '$lib/context.js'; import { get_repl_context } from '$lib/context.js';
import Message from './Message.svelte'; import Message from './Message.svelte';
import { svelteTheme } from './theme.js'; import { svelteTheme } from './theme.js';
import { autocomplete } from './autocomplete.js'; import { autocomplete } from './autocomplete.js';
/** @type {import('@codemirror/lint').LintSource | undefined} */ /** @type {{ diagnostics: import('@codemirror/lint').LintSource | undefined, readonly?: boolean, tab?: boolean, onchange: (value: string) => void }} */
export let diagnostics = undefined; const { diagnostics = undefined, readonly = false, tab = true, onchange } = $props();
export let readonly = false; let code = $state('');
export let tab = true;
/** @type {ReturnType<typeof createEventDispatcher<{ change: { value: string } }>>} */ /** @type {Lang} */
const dispatch = createEventDispatcher(); let lang = $state('svelte');
let code = ''; /** @param {{ code: string; lang: Lang }} options */
/** @type {import('./types').Lang} */
let lang = 'svelte';
/**
* @param {{ code: string; lang: import('./types').Lang }} options
*/
export async function set(options) { export async function set(options) {
update(options); update(options);
} }
/** /** @param {{ code?: string; lang?: Lang }} options */
* @param {{ code?: string; lang?: import('./types').Lang }} options
*/
export async function update(options) { export async function update(options) {
await isReady; await isReady;
@ -64,16 +55,14 @@
} }
} }
/** /** @param {number} pos */
* @param {number} pos
*/
export function setCursor(pos) { export function setCursor(pos) {
cursor_pos = pos; cursor_pos = pos;
} }
/** @type {(...val: any) => void} */ /** @type {(...val: any) => void} */
let fulfil_module_editor_ready; let fulfill_module_editor_ready;
export const isReady = new Promise((f) => (fulfil_module_editor_ready = f)); export const isReady = new Promise((f) => (fulfill_module_editor_ready = f));
export function resize() { export function resize() {
$cmInstance.view?.requestMeasure(); $cmInstance.view?.requestMeasure();
@ -87,9 +76,7 @@
return $cmInstance.view?.state.toJSON({ history: historyField }); return $cmInstance.view?.state.toJSON({ history: historyField });
} }
/** /** @param {any} state */
* @param {any} state
*/
export function setEditorState(state) { export function setEditorState(state) {
if (!$cmInstance.view) return; if (!$cmInstance.view) return;
@ -174,18 +161,24 @@
/** @type {import('@codemirror/state').Extension[]} */ /** @type {import('@codemirror/state').Extension[]} */
let extensions = []; let extensions = [];
let cursor_pos = 0; let cursor_pos = $state(0);
$: if ($cmInstance.view) { $effect(() => {
fulfil_module_editor_ready(); if ($cmInstance.view) {
} fulfill_module_editor_ready();
}
});
$: if ($cmInstance.view && w && h) resize(); $effect(() => {
if ($cmInstance.view && w && h) resize();
});
$: if (marked) { $effect(() => {
unmarkText(); if (marked) {
marked = false; unmarkText();
} marked = false;
}
});
const watcher = EditorView.updateListener.of((viewUpdate) => { const watcher = EditorView.updateListener.of((viewUpdate) => {
if (viewUpdate.selectionSet) { if (viewUpdate.selectionSet) {
@ -206,6 +199,7 @@
}); });
</script> </script>
<!-- svelte-ignore attribute_illegal_colon - codemirror's custom events contain colons -->
<div <div
class="codemirror-container" class="codemirror-container"
use:codemirror={{ use:codemirror={{
@ -230,9 +224,9 @@
extensions: [svelte_rune_completions, js_rune_completions, watcher], extensions: [svelte_rune_completions, js_rune_completions, watcher],
instanceStore: cmInstance instanceStore: cmInstance
}} }}
on:codemirror:textChange={({ detail: value }) => { oncodemirror:textChange={(/** @type {{ detail: string; }} */ event) => {
code = value; code = event.detail;
dispatch('change', { value: code }); onchange?.(code);
}} }}
> >
{#if !$cmInstance.view} {#if !$cmInstance.view}

@ -1,21 +1,15 @@
<script> <script>
/** @import { File } from '$lib/types' */
import { get_repl_context } from '$lib/context.js'; import { get_repl_context } from '$lib/context.js';
import { get_full_filename } from '$lib/utils.js'; import { get_full_filename } from '$lib/utils.js';
import { createEventDispatcher, tick } from 'svelte'; import { tick } from 'svelte';
import RunesInfo from './RunesInfo.svelte'; import RunesInfo from './RunesInfo.svelte';
import Migrate from './Migrate.svelte'; import Migrate from './Migrate.svelte';
/** @type {boolean} */ /** @typedef {{ files: File[]; diff: File }} ChangeEvent */
export let show_modified;
/** @type {boolean} */ /** @type {{ show_modified: boolean; runes: boolean, onremove?: (ev: ChangeEvent) => void, onadd?: (ev: ChangeEvent) => void }} */
export let runes; const { show_modified, runes, onremove, onadd } = $props();
/** @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 { const {
files, files,
@ -28,9 +22,9 @@
} = get_repl_context(); } = get_repl_context();
/** @type {string | null} */ /** @type {string | null} */
let editing_name = null; let editing_name = $state(null);
let input_value = ''; let input_value = $state('');
/** @param {string} filename */ /** @param {string} filename */
function select_file(filename) { function select_file(filename) {
@ -40,7 +34,7 @@
} }
} }
/** @param {import('$lib/types').File} file */ /** @param {File} file */
function edit_tab(file) { function edit_tab(file) {
if ($selected_name === get_full_filename(file)) { if ($selected_name === get_full_filename(file)) {
editing_name = get_full_filename(file); editing_name = get_full_filename(file);
@ -125,7 +119,7 @@
$files = $files.filter((file) => get_full_filename(file) !== filename); $files = $files.filter((file) => get_full_filename(file) !== filename);
dispatch('remove', { files: $files, diff: file }); onremove?.({ files: $files, diff: file });
EDITOR_STATE_MAP.delete(get_full_filename(file)); EDITOR_STATE_MAP.delete(get_full_filename(file));
@ -160,12 +154,12 @@
rebundle(); rebundle();
dispatch('add', { files: $files, diff: file }); onadd?.({ files: $files, diff: file });
$files = $files; $files = $files;
} }
/** @param {import('$lib/types').File} editing */ /** @param {File} editing */
function is_file_name_used(editing) { function is_file_name_used(editing) {
return $files.find( return $files.find(
(file) => JSON.stringify(file) !== JSON.stringify($selected) && file.name === editing.name (file) => JSON.stringify(file) !== JSON.stringify($selected) && file.name === editing.name
@ -177,7 +171,7 @@
let from = null; let from = null;
/** @type {string | null} */ /** @type {string | null} */
let over = null; let over = $state(null);
/** @param {DragEvent & { currentTarget: HTMLDivElement }} event */ /** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
function dragStart(event) { function dragStart(event) {
@ -190,10 +184,13 @@
/** @param {DragEvent & { currentTarget: HTMLDivElement }} event */ /** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
function dragOver(event) { function dragOver(event) {
event.preventDefault();
over = event.currentTarget.id; over = event.currentTarget.id;
} }
function dragEnd() { /** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
function dragEnd(event) {
event.preventDefault();
if (from && over) { if (from && over) {
const from_index = $files.findIndex((file) => file.name === from); const from_index = $files.findIndex((file) => file.name === from);
const to_index = $files.findIndex((file) => file.name === over); const to_index = $files.findIndex((file) => file.name === over);
@ -210,8 +207,8 @@
</script> </script>
<div class="component-selector"> <div class="component-selector">
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="file-tabs" on:dblclick={add_new}> <div class="file-tabs" ondblclick={add_new}>
{#each $files as file, index (file.name)} {#each $files as file, index (file.name)}
{@const filename = get_full_filename(file)} {@const filename = get_full_filename(file)}
<div <div
@ -222,14 +219,14 @@
class:active={filename === $selected_name} class:active={filename === $selected_name}
class:draggable={filename !== editing_name && index !== 0} class:draggable={filename !== editing_name && index !== 0}
class:drag-over={over === file.name} class:drag-over={over === file.name}
on:click={() => select_file(filename)} onclick={() => select_file(filename)}
on:keyup={(e) => e.key === ' ' && select_file(filename)} onkeyup={(e) => e.key === ' ' && select_file(filename)}
on:dblclick|stopPropagation={() => {}} ondblclick={(ev) => ev.stopPropagation()}
draggable={filename !== editing_name} draggable={filename !== editing_name}
on:dragstart={dragStart} ondragstart={dragStart}
on:dragover|preventDefault={dragOver} ondragover={dragOver}
on:dragleave={dragLeave} ondragleave={dragLeave}
on:drop|preventDefault={dragEnd} ondrop={dragEnd}
> >
<i class="drag-handle"></i> <i class="drag-handle"></i>
{#if file.name === 'App' && filename !== editing_name} {#if file.name === 'App' && filename !== editing_name}
@ -244,14 +241,14 @@
{input_value + (/\./.test(input_value) ? '' : `.${editing_file.type}`)} {input_value + (/\./.test(input_value) ? '' : `.${editing_file.type}`)}
</span> </span>
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y_autofocus -->
<input <input
autofocus autofocus
spellcheck={false} spellcheck={false}
bind:value={input_value} bind:value={input_value}
on:focus={select_input} onfocus={select_input}
on:blur={close_edit} onblur={close_edit}
on:keydown={(e) => { onkeydown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
if (!is_file_name_used(editing_file)) { if (!is_file_name_used(editing_file)) {
@ -263,21 +260,21 @@
/> />
{/if} {/if}
{:else} {:else}
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
class="editable" class="editable"
title="edit component name" title="edit component name"
on:click={() => edit_tab(file)} onclick={() => edit_tab(file)}
on:keyup={(e) => e.key === ' ' && edit_tab(file)} onkeyup={(e) => e.key === ' ' && edit_tab(file)}
> >
{file.name}.{file.type}{#if show_modified && file.modified}*{/if} {file.name}.{file.type}{#if show_modified && file.modified}*{/if}
</div> </div>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<span <span
class="remove" class="remove"
on:click={() => remove(filename)} onclick={() => remove(filename)}
on:keyup={(e) => e.key === ' ' && remove(filename)} onkeyup={(e) => e.key === ' ' && remove(filename)}
> >
<svg width="12" height="12" viewBox="0 0 24 24"> <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="18" y1="6" x2="6" y2="18" />
@ -289,7 +286,7 @@
{/each} {/each}
</div> </div>
<button class="add-new" on:click={add_new} title="add new component"> <button class="add-new" onclick={add_new} title="add new component">
<svg width="12" height="12" viewBox="0 0 24 24"> <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="12" y1="5" x2="12" y2="19" />
<line stroke="#999" x1="5" y1="12" x2="19" y2="12" /> <line stroke="#999" x1="5" y1="12" x2="19" y2="12" />

@ -5,7 +5,7 @@
</script> </script>
<div class="container"> <div class="container">
<button on:click={migrate} title="Migrate this component towards the new syntax">migrate</button> <button onclick={migrate} title="Migrate this component towards the new syntax">migrate</button>
</div> </div>
<style> <style>

@ -2,14 +2,8 @@
import { get_repl_context } from '$lib/context.js'; import { get_repl_context } from '$lib/context.js';
import CodeMirror from '../CodeMirror.svelte'; import CodeMirror from '../CodeMirror.svelte';
/** @type {boolean} */ /** @type {{ error: any; warnings: any[] }} */
export let autocomplete; const { error, warnings } = $props();
/** @type {any} */ // TODO
export let error;
/** @type {any[]} */ // TODO
export let warnings;
export function focus() { export function focus() {
$module_editor?.focus(); $module_editor?.focus();
@ -62,7 +56,7 @@
return []; return [];
}} }}
on:change={handle_change} onchange={handle_change}
/> />
</div> </div>
</div> </div>

@ -1,22 +1,22 @@
<script> <script>
import { get_repl_context } from '$lib/context.js'; import { get_repl_context } from '$lib/context.js';
/** @type {boolean} */ /** @type {{ runes: boolean }} */
export let runes; const { runes } = $props();
let open = false; let open = $state(false);
const { selected_name } = get_repl_context(); const { selected_name } = get_repl_context();
</script> </script>
<svelte:window <svelte:window
on:keydown={(e) => { onkeydown={(e) => {
if (e.key === 'Escape') open = false; if (e.key === 'Escape') open = false;
}} }}
/> />
<div class="container"> <div class="container">
<button class:active={runes} class:open on:click={() => (open = !open)}> <button class:active={runes} class:open onclick={() => (open = !open)}>
<svg viewBox="0 0 24 24"> <svg viewBox="0 0 24 24">
<path d="M9.4,1H19l-5.9,7.7h8L8.3,23L11,12.6H3.5L9.4,1z" /> <path d="M9.4,1H19l-5.9,7.7h8L8.3,23L11,12.6H3.5L9.4,1z" />
</svg> </svg>
@ -25,9 +25,10 @@
</button> </button>
{#if open} {#if open}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions <!-- a11y is handled by the <svelte:window> above -->
(This is taken care of by the <svelte:window> above) --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="modal-backdrop" on:click={() => (open = false)}></div> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="modal-backdrop" onclick={() => (open = false)}></div>
<div class="popup"> <div class="popup">
{#if $selected_name.endsWith('.svelte.js')} {#if $selected_name.endsWith('.svelte.js')}
<p> <p>

@ -1,10 +1,10 @@
<script> <script>
export let checked = false; /** @type {{ checked: boolean }} */
let { checked = $bindable(false) } = $props();
import Checkbox from './Checkbox.svelte'; import Checkbox from './Checkbox.svelte';
</script> </script>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="input-output-toggle"> <label class="input-output-toggle">
<span class:active={!checked} style="text-align: right">input</span> <span class:active={!checked} style="text-align: right">input</span>
<span style="display:grid; place-items: center"> <span style="display:grid; place-items: center">

@ -2,16 +2,14 @@
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { get_repl_context } from './context.js'; import { get_repl_context } from './context.js';
/** @type {'info' | 'warning' | 'error'} */ /** @type {{
export let kind = 'info'; * kind: 'info' | 'warning' | 'error',
* details?: import('./types').MessageDetails | undefined,
/** @type {import('./types').MessageDetails | undefined} */ * filename?: string | undefined,
export let details = undefined; * truncate?: boolean,
* children: import('svelte').Snippet,
/** @type {string | undefined} */ * }} */
export let filename = undefined; const { kind = 'info', details = undefined, filename = undefined, truncate = false, children } = $props();
export let truncate = false;
const { go_to_warning_pos } = get_repl_context(); const { go_to_warning_pos } = get_repl_context();
@ -33,13 +31,13 @@
{#if details} {#if details}
<button <button
class:navigable={details.filename} class:navigable={details.filename}
on:click={() => go_to_warning_pos(details)} onclick={() => go_to_warning_pos(details)}
on:keyup={(e) => e.key === ' ' && go_to_warning_pos(details)} onkeyup={(e) => e.key === ' ' && go_to_warning_pos(details)}
> >
{message(details)} {message(details)}
</button> </button>
{:else} {:else}
<slot /> {@render children()}
{/if} {/if}
</div> </div>

@ -1,9 +1,9 @@
<script> <script>
/** @import { File, ReplContext } from './types' */
import { EditorState } from '@codemirror/state'; import { EditorState } from '@codemirror/state';
import { SplitPane } from '@rich_harris/svelte-split-pane'; import { SplitPane } from '@rich_harris/svelte-split-pane';
import { BROWSER } from 'esm-env'; import { BROWSER } from 'esm-env';
import { createEventDispatcher } from 'svelte'; import { derived as derivedStore, writable } from 'svelte/store';
import { derived, writable } from 'svelte/store';
import Bundler from './Bundler.js'; import Bundler from './Bundler.js';
import ComponentSelector from './Input/ComponentSelector.svelte'; import ComponentSelector from './Input/ComponentSelector.svelte';
import ModuleEditor from './Input/ModuleEditor.svelte'; import ModuleEditor from './Input/ModuleEditor.svelte';
@ -13,23 +13,44 @@
import { get_full_filename } from './utils.js'; import { get_full_filename } from './utils.js';
import Compiler from './Output/Compiler.js'; import Compiler from './Output/Compiler.js';
export let packagesUrl = 'https://unpkg.com'; /** @type {{
export let svelteUrl = `${BROWSER ? location.origin : ''}/svelte`; * packagesUrl?: string;
export let embedded = false; * svelteUrl?: string;
/** @type {'columns' | 'rows'} */ * embedded?: boolean;
export let orientation = 'columns'; * orientation?: 'columns' | 'rows';
export let relaxed = false; * relaxed?: boolean;
export let fixed = false; * fixed?: boolean;
export let fixedPos = 50; * fixedPos?: number;
export let injectedJS = ''; * injectedJS?: string;
export let injectedCSS = ''; * injectedCSS?: string;
/** @type {'light' | 'dark'} */ * previewTheme?: 'light' | 'dark';
export let previewTheme = 'light'; * showModified?: boolean;
export let showModified = false; * showAst?: boolean;
export let showAst = false; * autocomplete?: boolean;
export let autocomplete = true; * onchange?: (files: File[]) => void;
* onadd?: (event: { files: File[]; diff: File }) => void;
let runes = false; * onremove?: (event: { files: File[]; diff: File }) => void;
* }} */
let {
packagesUrl = 'https://unpkg.com',
svelteUrl = `${BROWSER ? location.origin : ''}/svelte`,
embedded = false,
orientation = 'columns',
relaxed = false,
fixed = false,
fixedPos = 50,
injectedJS = '',
injectedCSS = '',
previewTheme = 'light',
showModified = false,
showAst = false,
autocomplete = true,
onchange,
onadd,
onremove
} = $props();
let runes = $state(false);
export function toJSON() { export function toJSON() {
return { return {
@ -39,7 +60,7 @@
} }
/** /**
* @param {{ files: import('./types').File[], css?: string }} data * @param {{ files: File[], css?: string }} data
*/ */
export async function set(data) { export async function set(data) {
$files = data.files; $files = data.files;
@ -60,20 +81,13 @@
// after having loaded the files externally // after having loaded the files externally
populate_editor_state(); populate_editor_state();
dispatch('change', { files: $files }); onchange?.($files);
} }
export function markSaved() { export function markSaved() {
$files = $files.map((val) => ({ ...val, modified: false })); $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} */ /** @type {import('svelte/compiler').CompileOptions} */
const DEFAULT_COMPILE_OPTIONS = { const DEFAULT_COMPILE_OPTIONS = {
generate: 'client', generate: 'client',
@ -92,7 +106,7 @@
const selected_name = writable('App.svelte'); const selected_name = writable('App.svelte');
/** @type {ReplContext['selected']} */ /** @type {ReplContext['selected']} */
const selected = derived([files, selected_name], ([$files, $selected_name]) => { const selected = derivedStore([files, selected_name], ([$files, $selected_name]) => {
return ( return (
$files.find((val) => get_full_filename(val) === $selected_name) ?? { $files.find((val) => get_full_filename(val) === $selected_name) ?? {
name: '', name: '',
@ -154,7 +168,7 @@
$bundling = new Promise((resolve) => { $bundling = new Promise((resolve) => {
resolver = resolve; resolver = resolve;
}); });
const result = await $bundler?.bundle($files); const result = await $bundler?.bundle($state.snapshot($files));
if (result && token === current_token) $bundle = result; if (result && token === current_token) $bundle = result;
resolver(); resolver();
} }
@ -162,7 +176,7 @@
async function migrate() { async function migrate() {
if (!compiler || $selected?.type !== 'svelte') return; if (!compiler || $selected?.type !== 'svelte') return;
const result = await compiler.migrate($selected); const result = await compiler.migrate($state.snapshot($selected));
if (result.error) { if (result.error) {
// TODO show somehow // TODO show somehow
return; return;
@ -182,9 +196,7 @@
let is_select_changing = false; let is_select_changing = false;
/** /** @param {string} filename */
* @param {string} filename
*/
async function handle_select(filename) { async function handle_select(filename) {
is_select_changing = true; is_select_changing = true;
@ -203,16 +215,14 @@
is_select_changing = false; is_select_changing = false;
} }
/** /** @param {string} value */
* @param {CustomEvent<{ value: string }>} event async function handle_change(value) {
*/
async function handle_change(event) {
if (is_select_changing) return; if (is_select_changing) return;
files.update(($files) => { files.update(($files) => {
const file = { ...$selected }; const file = { ...$selected };
file.source = event.detail.value; file.source = value;
file.modified = true; file.modified = true;
const idx = $files.findIndex((val) => get_full_filename(val) === $selected_name); const idx = $files.findIndex((val) => get_full_filename(val) === $selected_name);
@ -227,9 +237,7 @@
EDITOR_STATE_MAP.set(get_full_filename($selected), $module_editor?.getEditorState()); EDITOR_STATE_MAP.set(get_full_filename($selected), $module_editor?.getEditorState());
dispatch('change', { onchange?.($files);
files: $files
});
rebundle(); rebundle();
} }
@ -268,35 +276,42 @@
const compiler = BROWSER ? new Compiler(svelteUrl) : null; const compiler = BROWSER ? new Compiler(svelteUrl) : null;
/** @type {import('./workers/workers').CompileMessageData | null} */ /** @type {import('./workers/workers').CompileMessageData | null} */
let compiled = null; let compiled = $state(null);
$inspect(compiled);
/** /**
* @param {import('./types').File | null} $selected * @param {File | null} $selected
* @param {import('svelte/compiler').CompileOptions} $compile_options * @param {import('svelte/compiler').CompileOptions} $compile_options
*/ */
async function recompile($selected, $compile_options) { async function recompile($selected, $compile_options) {
if (!compiler || !$selected) return; if (!compiler || !$selected) return;
if ($selected.type === 'svelte' || $selected.type === 'js') { if ($selected.type === 'svelte' || $selected.type === 'js') {
compiled = await compiler.compile($selected, $compile_options, true); compiled = await compiler.compile($state.snapshot($selected), $compile_options, true);
runes = compiled.result.metadata?.runes ?? false; runes = compiled.result.metadata?.runes ?? false;
} else { } else {
runes = false; runes = false;
} }
} }
$: recompile($selected, $compile_options); $effect(() => {
recompile($selected, $compile_options);
});
$: mobile = width < 540; let width = $state(0);
$: $toggleable = mobile && orientation === 'columns'; const mobile = $derived(width < 540);
let width = 0; $effect(() => {
let show_output = false; toggleable.set(mobile && orientation === 'columns');
});
let show_output = $state(false);
/** @type {string | null} */ /** @type {string | null} */
let status = null; let status = $state(null);
let status_visible = false; let status_visible = $state(false);
/** @type {NodeJS.Timeout | undefined} */ /** @type {NodeJS.Timeout | undefined} */
let status_timeout = undefined; let status_timeout = undefined;
@ -336,7 +351,7 @@
} }
</script> </script>
<svelte:window on:beforeunload={before_unload} /> <svelte:window onbeforeunload={before_unload} />
<div class="container" class:toggleable={$toggleable} bind:clientWidth={width}> <div class="container" class:toggleable={$toggleable} bind:clientWidth={width}>
<div class="viewport" class:output={show_output}> <div class="viewport" class:output={show_output}>
@ -349,12 +364,8 @@
max="-4.1rem" max="-4.1rem"
> >
<section slot="a"> <section slot="a">
<ComponentSelector show_modified={showModified} {runes} on:add on:remove /> <ComponentSelector show_modified={showModified} {runes} {onadd} {onremove} />
<ModuleEditor <ModuleEditor error={compiled?.result.error} warnings={compiled?.result.warnings ?? []} />
{autocomplete}
error={compiled?.result.error}
warnings={compiled?.result.warnings ?? []}
/>
</section> </section>
<section slot="b" style="height: 100%;"> <section slot="b" style="height: 100%;">

@ -134,10 +134,11 @@ const options = runes.map(({ snippet, test }, i) => ({
/** /**
* @param {import('@codemirror/autocomplete').CompletionContext} context * @param {import('@codemirror/autocomplete').CompletionContext} context
* @param {import('./types.js').File} selected * @param {import('./types.js').File | null} selected
* @param {import('./types.js').File[]} files * @param {import('./types.js').File[]} files
*/ */
export function autocomplete(context, selected, files) { export function autocomplete(context, selected, files) {
if (!selected) return false;
let node = syntaxTree(context.state).resolveInner(context.pos, -1); let node = syntaxTree(context.state).resolveInner(context.pos, -1);
if (node.name === 'String' && node.parent?.name === 'ImportDeclaration') { if (node.name === 'String' && node.parent?.name === 'ImportDeclaration') {

@ -2,12 +2,12 @@ import { getContext, setContext } from 'svelte';
const key = Symbol('repl'); const key = Symbol('repl');
/** @returns {import("./types").ReplContext} */ /** @returns {import('./types').ReplContext} */
export function get_repl_context() { export function get_repl_context() {
return getContext(key); return getContext(key);
} }
/** @param {import("./types").ReplContext} value */ /** @param {import('./types').ReplContext} value */
export function set_repl_context(value) { export function set_repl_context(value) {
setContext(key, value); setContext(key, value);
} }

@ -67,11 +67,7 @@ export type ReplContext = {
rebundle(): Promise<void>; rebundle(): Promise<void>;
migrate(): Promise<void>; migrate(): Promise<void>;
handle_select(filename: string): Promise<void>; handle_select(filename: string): Promise<void>;
handle_change( handle_change(value: string): Promise<void>;
event: CustomEvent<{
value: string;
}>
): Promise<void>;
go_to_warning_pos(item?: MessageDetails): Promise<void>; go_to_warning_pos(item?: MessageDetails): Promise<void>;
clear_state(): void; clear_state(): void;
}; };

@ -19,7 +19,7 @@ const ready = new Promise((f) => {
self.addEventListener( self.addEventListener(
'message', 'message',
/** @param {MessageEvent<import("../workers").CompileMessageData>} event */ /** @param {MessageEvent<import('../workers').CompileMessageData>} event */
async (event) => { async (event) => {
switch (event.data.type) { switch (event.data.type) {
case 'init': case 'init':
@ -55,7 +55,7 @@ const common_options = {
css: false css: false
}; };
/** @param {import("../workers").CompileMessageData} param0 */ /** @param {import('../workers').CompileMessageData} param0 */
function compile({ id, source, options, return_ast }) { function compile({ id, source, options, return_ast }) {
try { try {
const css = `/* Select a component to see compiled CSS */`; const css = `/* Select a component to see compiled CSS */`;
@ -132,7 +132,7 @@ function compile({ id, source, options, return_ast }) {
} }
} }
/** @param {import("../workers").MigrateMessageData} param0 */ /** @param {import('../workers').MigrateMessageData} param0 */
function migrate({ id, source }) { function migrate({ id, source }) {
try { try {
const result = svelte.migrate(source); const result = svelte.migrate(source);

@ -1,4 +1,4 @@
import type { CompileOptions, File } from '../types'; import type { CompileOptions, File, Warning } from '../types';
export type CompileMessageData = { export type CompileMessageData = {
id: number; id: number;
@ -15,6 +15,11 @@ export type CompileMessageData = {
metadata?: { metadata?: {
runes: boolean; runes: boolean;
}; };
error: {
message: string;
position: [number, number];
} | null;
warnings: Warning[];
}; };
}; };

Loading…
Cancel
Save