Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,6 @@ const config = {
// Don't do this in your own apps unless you know what you're doing!
// See https://kit.svelte.dev/docs/configuration#csrf for more info.
csrf: false
},

vitePlugin: {
// This enables compile-time warnings to be
// visible in the learn.svelte.dev editor
onwarn: (warning, defaultHandler) => {
console.log('svelte:warnings:%s', JSON.stringify(warning));
defaultHandler(warning);
}
}
};

Expand Down
22 changes: 1 addition & 21 deletions apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ import Bundler from '@sveltejs/repl/bundler';
import * as yootils from 'yootils';
import type { Adapter } from '$lib/tutorial';
import type { File, Item } from 'editor';
import type { CompileError, Warning } from 'svelte/compiler';

/** Rollup bundler singleton */
let bundler: Bundler;

export const state = new (class RollupState {
progress = $state.raw({ value: 0, text: 'initialising' });
bundle = $state.raw<any>(null);
errors = $state.raw<Record<string, CompileError | null>>();
warnings = $state.raw<Record<string, Warning[]>>({});
})();

/**
Expand Down Expand Up @@ -43,7 +40,7 @@ export async function create(): Promise<Adapter> {
let current_stubs = stubs_to_map([]);

async function compile() {
const result = await bundler.bundle(
state.bundle = await bundler.bundle(
[...current_stubs.values()]
// TODO we can probably remove all the SvelteKit specific stuff from the tutorial content once this settles down
.filter((f): f is File => f.name.startsWith('/src/lib/') && f.type === 'file')
Expand All @@ -53,23 +50,6 @@ export async function create(): Promise<Adapter> {
type: f.name.split('.').pop() ?? 'svelte'
}))
);

state.bundle = result;

// TODO this approach is insufficient — we need to get diagnostics for
// individual files, not just the bundle as a whole
state.errors = {};
state.warnings = {};

if (result.error) {
const file = '/src/lib/' + result.error.filename;
state.errors[file] = result.error;
}

for (const warning of result?.warnings ?? []) {
const file = '/src/lib/' + warning.filename;
(state.warnings[file] ??= []).push(warning);
}
}

const q = yootils.queue(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { escape_html } from '../../../utils/escape.js';
import { ready } from '../common/index.js';
import type { Adapter } from '$lib/tutorial';
import type { Item, File } from 'editor';
import type { CompileError, Warning } from 'svelte/compiler';

const converter = new AnsiToHtml({
fg: 'var(--sk-text-3)'
Expand All @@ -22,8 +21,6 @@ export const state = new (class WCState {
base = $state.raw<string | null>(null);
error = $state.raw<Error | null>(null);
logs = $state.raw<string[]>([]);
errors = $state.raw<Record<string, CompileError | null>>({});
warnings = $state.raw<Record<string, Warning[]>>({});
})();

export async function create(): Promise<Adapter> {
Expand All @@ -49,40 +46,14 @@ export async function create(): Promise<Adapter> {
}
});

let warnings: Record<string, import('svelte/compiler').Warning[]> = {};
let timeout: any;

function schedule_to_update_warning(msec: number) {
clearTimeout(timeout);
timeout = setTimeout(() => (state.warnings = { ...warnings }), msec);
}

const log_stream = () =>
new WritableStream({
write(chunk) {
if (chunk === '\x1B[1;1H') {
// clear screen
state.logs = [];
} else if (chunk?.startsWith('svelte:warnings:')) {
const warn: Warning = JSON.parse(chunk.slice(16));
const filename = (warn.filename!.startsWith('/') ? warn.filename : '/' + warn.filename)!;
const current = warnings[filename];

if (!current) {
warnings[filename] = [warn];
// the exact same warning may be given multiple times in a row
} else if (
!current.some(
(s) =>
s.code === warn.code &&
s.position![0] === warn.position![0] &&
s.position![1] === warn.position![1]
)
) {
current.push(warn);
}

schedule_to_update_warning(100);
// TODO when does this happen?
} else {
const log = converter.toHtml(escape_html(chunk)).replace(/\n/g, '<br>');
state.logs = [...state.logs, log];
Expand Down Expand Up @@ -181,17 +152,6 @@ export async function create(): Promise<Adapter> {
...force_delete
];

// initialize warnings of written files
to_write
.filter((stub) => stub.type === 'file' && warnings[stub.name])
.forEach((stub) => (warnings[stub.name] = []));
// remove warnings of deleted files
to_delete
.filter((stubname) => warnings[stubname])
.forEach((stubname) => delete warnings[stubname]);

state.warnings = { ...warnings };

current_stubs = stubs_to_map(stubs);

// For some reason, server-ready is fired again when the vite dev server is restarted.
Expand Down Expand Up @@ -258,10 +218,6 @@ export async function create(): Promise<Adapter> {

tree[basename] = to_file(file);

// initialize warnings of this file
warnings[file.name] = [];
schedule_to_update_warning(100);

await vm.mount(root);

if (will_restart) await wait_for_restart_vite();
Expand Down
2 changes: 0 additions & 2 deletions apps/svelte.dev/src/routes/tutorial/[...slug]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,6 @@
<section slot="b" class="editor-container">
<Editor
bind:this={editor}
errors={adapter.adapter_state.errors}
warnings={adapter.adapter_state.warnings}
{workspace}
onchange={async (file, contents) => {
skip_set_files = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ export const adapter_state = new (class {
status: 'initialising'
}
);

/** Diagnostics */
errors = $derived((use_rollup ? rollup_state.errors : wc_state.errors) || {});
warnings = $derived((use_rollup ? rollup_state.warnings : wc_state.warnings) || {});
})();

if (browser) {
Expand Down
39 changes: 18 additions & 21 deletions packages/editor/src/lib/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,15 @@
import { autocomplete_for_svelte } from '@sveltejs/site-kit/codemirror';
import type { Diagnostic } from '@codemirror/lint';
import { Workspace, type Item, type File } from './Workspace.svelte.js';
import type { CompileError, Warning } from 'svelte/compiler';
import './codemirror.css';

interface Props {
errors: Record<string, CompileError | null>;
warnings: Record<string, Warning[]>;
workspace: Workspace;
onchange: (file: File, contents: string) => void;
autocomplete_filter?: (file: File) => boolean;
}

let { errors, warnings, workspace, onchange, autocomplete_filter = () => true }: Props = $props();
let { workspace, onchange, autocomplete_filter = () => true }: Props = $props();

let container: HTMLDivElement;

Expand Down Expand Up @@ -171,25 +168,25 @@

const diagnostics: Diagnostic[] = [];

const error = null; // TODO should be `errors[workspace.selected_name]` but it's currently a Rollup plugin error...
const current_warnings = warnings[workspace.selected_name] || [];
const error = workspace.diagnostics[workspace.selected_name]?.error;
const current_warnings = workspace.diagnostics[workspace.selected_name]?.warnings ?? [];

if (error) {
// diagnostics.push({
// 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, '&amp;')
// .replace(/</g, '&lt;')
// .replace(/`(.+?)`/g, `<code>$1</code>`)}`;
// return span;
// }
// });
diagnostics.push({
severity: 'error',
from: error.position![0],
to: error.position![1],
message: error.message,
renderMessage: () => {
const span = document.createElement('span');
span.innerHTML = `${error.message
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/`(.+?)`/g, `<code>$1</code>`)} <strong>(${error.code})</strong>`;

return span;
}
});
}

for (const warning of current_warnings) {
Expand Down
30 changes: 30 additions & 0 deletions packages/editor/src/lib/Workspace.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { CompileError, Warning } from 'svelte/compiler';
import { get_diagnostics } from './diagnostics';

export interface File {
type: 'file';
name: string;
Expand All @@ -14,11 +17,18 @@ export interface Directory {

export type Item = File | Directory;

export interface Diagnostics {
error: CompileError | null;
warnings: Warning[];
}

export class Workspace {
files = $state.raw<Item[]>([]);
creating = $state.raw<{ parent: string; type: 'file' | 'directory' } | null>(null);
selected_name = $state<string | null>(null);

diagnostics = $state<Record<string, Diagnostics>>({});

#onupdate: (file: File) => void;
#onreset: (items: Item[]) => void;

Expand All @@ -37,6 +47,21 @@ export class Workspace {
this.selected_name = selected_name;
this.#onupdate = onupdate;
this.#onreset = onreset;

this.#reset_diagnostics();
}

#reset_diagnostics() {
this.diagnostics = {};

for (const file of this.files) {
if (file.type !== 'file') continue;
if (!/\.svelte(\.|$)/.test(file.name)) continue;

get_diagnostics(file).then((diagnostics) => {
this.diagnostics[file.name] = diagnostics;
});
}
}

get selected_file() {
Expand All @@ -55,6 +80,10 @@ export class Workspace {
return old;
});

get_diagnostics(file).then((diagnostics) => {
this.diagnostics[file.name] = diagnostics;
});

this.#onupdate(file);
}

Expand All @@ -67,5 +96,6 @@ export class Workspace {
this.files = new_files;

this.#onreset(new_files);
this.#reset_diagnostics();
}
}
40 changes: 40 additions & 0 deletions packages/editor/src/lib/diagnostics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { BROWSER } from 'esm-env';
import DiagnosticsWorker from './worker?worker';
import type { Diagnostics, File } from '../Workspace.svelte';

const callbacks = new Map<number, (diagnostics: Diagnostics) => void>();

let worker: Worker;

let uid = 1;

if (BROWSER) {
worker = new DiagnosticsWorker();

worker.addEventListener('message', (event) => {
const callback = callbacks.get(event.data.id);

if (callback) {
callback(event.data.payload);
callbacks.delete(event.data.id);
}
});
}

export function get_diagnostics(file: File): Promise<Diagnostics> {
if (!BROWSER) {
// TODO this is a bit janky
return Promise.resolve({
error: null,
warnings: []
});
}

let id = uid++;

worker.postMessage({ id, file });

return new Promise((fulfil) => {
callbacks.set(id, fulfil);
});
}
34 changes: 34 additions & 0 deletions packages/editor/src/lib/diagnostics/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { compile, compileModule } from 'svelte/compiler';
import type { File } from '../Workspace.svelte';

// TODO need to handle Svelte 3/4 for playground

addEventListener('message', (event) => {
const { id, file } = event.data as { id: number; file: File };

const fn = file.name.endsWith('.svelte') ? compile : compileModule;

try {
const result = fn(file.contents, {
filename: file.name
});

postMessage({
id,
payload: {
error: null,
// @ts-expect-error https://github.com/sveltejs/svelte/issues/13628
warnings: result.warnings.map((w) => ({ message: w.message, ...w }))
}
});
} catch (e) {
postMessage({
id,
payload: {
// @ts-expect-error
error: { message: e.message, ...e },
warnings: []
}
});
}
});
2 changes: 1 addition & 1 deletion packages/editor/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"module": "NodeNext",
"module": "preserve",
"moduleResolution": "bundler"
}
}
Loading