Skip to content

Commit d3407b6

Browse files
authored
Diagnostics service (#428)
* set up diagnostics service * remove diagnostics from adapters * tidy up
1 parent e9aab3f commit d3407b6

File tree

10 files changed

+125
-103
lines changed

10 files changed

+125
-103
lines changed

apps/svelte.dev/scripts/create-tutorial-zip/common/svelte.config.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,6 @@ const config = {
55
// Don't do this in your own apps unless you know what you're doing!
66
// See https://kit.svelte.dev/docs/configuration#csrf for more info.
77
csrf: false
8-
},
9-
10-
vitePlugin: {
11-
// This enables compile-time warnings to be
12-
// visible in the learn.svelte.dev editor
13-
onwarn: (warning, defaultHandler) => {
14-
console.log('svelte:warnings:%s', JSON.stringify(warning));
15-
defaultHandler(warning);
16-
}
178
}
189
};
1910

apps/svelte.dev/src/lib/tutorial/adapters/rollup/index.svelte.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@ import Bundler from '@sveltejs/repl/bundler';
33
import * as yootils from 'yootils';
44
import type { Adapter } from '$lib/tutorial';
55
import type { File, Item } from 'editor';
6-
import type { CompileError, Warning } from 'svelte/compiler';
76

87
/** Rollup bundler singleton */
98
let bundler: Bundler;
109

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

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

4542
async function compile() {
46-
const result = await bundler.bundle(
43+
state.bundle = await bundler.bundle(
4744
[...current_stubs.values()]
4845
// TODO we can probably remove all the SvelteKit specific stuff from the tutorial content once this settles down
4946
.filter((f): f is File => f.name.startsWith('/src/lib/') && f.type === 'file')
@@ -53,23 +50,6 @@ export async function create(): Promise<Adapter> {
5350
type: f.name.split('.').pop() ?? 'svelte'
5451
}))
5552
);
56-
57-
state.bundle = result;
58-
59-
// TODO this approach is insufficient — we need to get diagnostics for
60-
// individual files, not just the bundle as a whole
61-
state.errors = {};
62-
state.warnings = {};
63-
64-
if (result.error) {
65-
const file = '/src/lib/' + result.error.filename;
66-
state.errors[file] = result.error;
67-
}
68-
69-
for (const warning of result?.warnings ?? []) {
70-
const file = '/src/lib/' + warning.filename;
71-
(state.warnings[file] ??= []).push(warning);
72-
}
7353
}
7454

7555
const q = yootils.queue(1);

apps/svelte.dev/src/lib/tutorial/adapters/webcontainer/index.svelte.ts

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { escape_html } from '../../../utils/escape.js';
88
import { ready } from '../common/index.js';
99
import type { Adapter } from '$lib/tutorial';
1010
import type { Item, File } from 'editor';
11-
import type { CompileError, Warning } from 'svelte/compiler';
1211

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

2926
export async function create(): Promise<Adapter> {
@@ -49,40 +46,14 @@ export async function create(): Promise<Adapter> {
4946
}
5047
});
5148

52-
let warnings: Record<string, import('svelte/compiler').Warning[]> = {};
53-
let timeout: any;
54-
55-
function schedule_to_update_warning(msec: number) {
56-
clearTimeout(timeout);
57-
timeout = setTimeout(() => (state.warnings = { ...warnings }), msec);
58-
}
59-
6049
const log_stream = () =>
6150
new WritableStream({
6251
write(chunk) {
6352
if (chunk === '\x1B[1;1H') {
6453
// clear screen
6554
state.logs = [];
6655
} else if (chunk?.startsWith('svelte:warnings:')) {
67-
const warn: Warning = JSON.parse(chunk.slice(16));
68-
const filename = (warn.filename!.startsWith('/') ? warn.filename : '/' + warn.filename)!;
69-
const current = warnings[filename];
70-
71-
if (!current) {
72-
warnings[filename] = [warn];
73-
// the exact same warning may be given multiple times in a row
74-
} else if (
75-
!current.some(
76-
(s) =>
77-
s.code === warn.code &&
78-
s.position![0] === warn.position![0] &&
79-
s.position![1] === warn.position![1]
80-
)
81-
) {
82-
current.push(warn);
83-
}
84-
85-
schedule_to_update_warning(100);
56+
// TODO when does this happen?
8657
} else {
8758
const log = converter.toHtml(escape_html(chunk)).replace(/\n/g, '<br>');
8859
state.logs = [...state.logs, log];
@@ -181,17 +152,6 @@ export async function create(): Promise<Adapter> {
181152
...force_delete
182153
];
183154

184-
// initialize warnings of written files
185-
to_write
186-
.filter((stub) => stub.type === 'file' && warnings[stub.name])
187-
.forEach((stub) => (warnings[stub.name] = []));
188-
// remove warnings of deleted files
189-
to_delete
190-
.filter((stubname) => warnings[stubname])
191-
.forEach((stubname) => delete warnings[stubname]);
192-
193-
state.warnings = { ...warnings };
194-
195155
current_stubs = stubs_to_map(stubs);
196156

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

259219
tree[basename] = to_file(file);
260220

261-
// initialize warnings of this file
262-
warnings[file.name] = [];
263-
schedule_to_update_warning(100);
264-
265221
await vm.mount(root);
266222

267223
if (will_restart) await wait_for_restart_vite();

apps/svelte.dev/src/routes/tutorial/[...slug]/+page.svelte

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,6 @@
316316
<section slot="b" class="editor-container">
317317
<Editor
318318
bind:this={editor}
319-
errors={adapter.adapter_state.errors}
320-
warnings={adapter.adapter_state.warnings}
321319
{workspace}
322320
onchange={async (file, contents) => {
323321
skip_set_files = true;

apps/svelte.dev/src/routes/tutorial/[...slug]/adapter.svelte.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ export const adapter_state = new (class {
3131
status: 'initialising'
3232
}
3333
);
34-
35-
/** Diagnostics */
36-
errors = $derived((use_rollup ? rollup_state.errors : wc_state.errors) || {});
37-
warnings = $derived((use_rollup ? rollup_state.warnings : wc_state.warnings) || {});
3834
})();
3935

4036
if (browser) {

packages/editor/src/lib/Editor.svelte

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,15 @@
1414
import { autocomplete_for_svelte } from '@sveltejs/site-kit/codemirror';
1515
import type { Diagnostic } from '@codemirror/lint';
1616
import { Workspace, type Item, type File } from './Workspace.svelte.js';
17-
import type { CompileError, Warning } from 'svelte/compiler';
1817
import './codemirror.css';
1918
2019
interface Props {
21-
errors: Record<string, CompileError | null>;
22-
warnings: Record<string, Warning[]>;
2320
workspace: Workspace;
2421
onchange: (file: File, contents: string) => void;
2522
autocomplete_filter?: (file: File) => boolean;
2623
}
2724
28-
let { errors, warnings, workspace, onchange, autocomplete_filter = () => true }: Props = $props();
25+
let { workspace, onchange, autocomplete_filter = () => true }: Props = $props();
2926
3027
let container: HTMLDivElement;
3128
@@ -171,25 +168,25 @@
171168
172169
const diagnostics: Diagnostic[] = [];
173170
174-
const error = null; // TODO should be `errors[workspace.selected_name]` but it's currently a Rollup plugin error...
175-
const current_warnings = warnings[workspace.selected_name] || [];
171+
const error = workspace.diagnostics[workspace.selected_name]?.error;
172+
const current_warnings = workspace.diagnostics[workspace.selected_name]?.warnings ?? [];
176173
177174
if (error) {
178-
// diagnostics.push({
179-
// severity: 'error',
180-
// from: error.position![0],
181-
// to: error.position![1],
182-
// message: error.message,
183-
// renderMessage: () => {
184-
// // TODO expose error codes, so we can link to docs in future
185-
// const span = document.createElement('span');
186-
// span.innerHTML = `${error.message
187-
// .replace(/&/g, '&amp;')
188-
// .replace(/</g, '&lt;')
189-
// .replace(/`(.+?)`/g, `<code>$1</code>`)}`;
190-
// return span;
191-
// }
192-
// });
175+
diagnostics.push({
176+
severity: 'error',
177+
from: error.position![0],
178+
to: error.position![1],
179+
message: error.message,
180+
renderMessage: () => {
181+
const span = document.createElement('span');
182+
span.innerHTML = `${error.message
183+
.replace(/&/g, '&amp;')
184+
.replace(/</g, '&lt;')
185+
.replace(/`(.+?)`/g, `<code>$1</code>`)} <strong>(${error.code})</strong>`;
186+
187+
return span;
188+
}
189+
});
193190
}
194191
195192
for (const warning of current_warnings) {

packages/editor/src/lib/Workspace.svelte.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { CompileError, Warning } from 'svelte/compiler';
2+
import { get_diagnostics } from './diagnostics';
3+
14
export interface File {
25
type: 'file';
36
name: string;
@@ -14,11 +17,18 @@ export interface Directory {
1417

1518
export type Item = File | Directory;
1619

20+
export interface Diagnostics {
21+
error: CompileError | null;
22+
warnings: Warning[];
23+
}
24+
1725
export class Workspace {
1826
files = $state.raw<Item[]>([]);
1927
creating = $state.raw<{ parent: string; type: 'file' | 'directory' } | null>(null);
2028
selected_name = $state<string | null>(null);
2129

30+
diagnostics = $state<Record<string, Diagnostics>>({});
31+
2232
#onupdate: (file: File) => void;
2333
#onreset: (items: Item[]) => void;
2434

@@ -37,6 +47,21 @@ export class Workspace {
3747
this.selected_name = selected_name;
3848
this.#onupdate = onupdate;
3949
this.#onreset = onreset;
50+
51+
this.#reset_diagnostics();
52+
}
53+
54+
#reset_diagnostics() {
55+
this.diagnostics = {};
56+
57+
for (const file of this.files) {
58+
if (file.type !== 'file') continue;
59+
if (!/\.svelte(\.|$)/.test(file.name)) continue;
60+
61+
get_diagnostics(file).then((diagnostics) => {
62+
this.diagnostics[file.name] = diagnostics;
63+
});
64+
}
4065
}
4166

4267
get selected_file() {
@@ -55,6 +80,10 @@ export class Workspace {
5580
return old;
5681
});
5782

83+
get_diagnostics(file).then((diagnostics) => {
84+
this.diagnostics[file.name] = diagnostics;
85+
});
86+
5887
this.#onupdate(file);
5988
}
6089

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

6998
this.#onreset(new_files);
99+
this.#reset_diagnostics();
70100
}
71101
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { BROWSER } from 'esm-env';
2+
import DiagnosticsWorker from './worker?worker';
3+
import type { Diagnostics, File } from '../Workspace.svelte';
4+
5+
const callbacks = new Map<number, (diagnostics: Diagnostics) => void>();
6+
7+
let worker: Worker;
8+
9+
let uid = 1;
10+
11+
if (BROWSER) {
12+
worker = new DiagnosticsWorker();
13+
14+
worker.addEventListener('message', (event) => {
15+
const callback = callbacks.get(event.data.id);
16+
17+
if (callback) {
18+
callback(event.data.payload);
19+
callbacks.delete(event.data.id);
20+
}
21+
});
22+
}
23+
24+
export function get_diagnostics(file: File): Promise<Diagnostics> {
25+
if (!BROWSER) {
26+
// TODO this is a bit janky
27+
return Promise.resolve({
28+
error: null,
29+
warnings: []
30+
});
31+
}
32+
33+
let id = uid++;
34+
35+
worker.postMessage({ id, file });
36+
37+
return new Promise((fulfil) => {
38+
callbacks.set(id, fulfil);
39+
});
40+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { compile, compileModule } from 'svelte/compiler';
2+
import type { File } from '../Workspace.svelte';
3+
4+
// TODO need to handle Svelte 3/4 for playground
5+
6+
addEventListener('message', (event) => {
7+
const { id, file } = event.data as { id: number; file: File };
8+
9+
const fn = file.name.endsWith('.svelte') ? compile : compileModule;
10+
11+
try {
12+
const result = fn(file.contents, {
13+
filename: file.name
14+
});
15+
16+
postMessage({
17+
id,
18+
payload: {
19+
error: null,
20+
// @ts-expect-error https://github.com/sveltejs/svelte/issues/13628
21+
warnings: result.warnings.map((w) => ({ message: w.message, ...w }))
22+
}
23+
});
24+
} catch (e) {
25+
postMessage({
26+
id,
27+
payload: {
28+
// @ts-expect-error
29+
error: { message: e.message, ...e },
30+
warnings: []
31+
}
32+
});
33+
}
34+
});

packages/editor/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"skipLibCheck": true,
1010
"sourceMap": true,
1111
"strict": true,
12-
"module": "NodeNext",
12+
"module": "preserve",
1313
"moduleResolution": "bundler"
1414
}
1515
}

0 commit comments

Comments
 (0)