Skip to content

Commit 3d761e1

Browse files
committed
[fix] more robust monaco integration
- Move logic into monaco file - Better reuse of existing models + fix solve/reset - Fix initial select
1 parent f2cc2d8 commit 3d761e1

File tree

5 files changed

+172
-115
lines changed

5 files changed

+172
-115
lines changed

content/tutorial/02-sveltekit/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"depth": 0,
66
"name": "project"
77
},
8-
"focus": "/src/routes/index.svelte"
8+
"focus": "/src/routes/+page.svelte"
99
}

content/tutorial/04-advanced-sveltekit/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"depth": 0,
66
"name": "project"
77
},
8-
"focus": "/src/routes/index.svelte"
8+
"focus": "/src/routes/+page.svelte"
99
}

src/lib/client/adapters/webcontainer/index.js

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { load } from '@webcontainer/api';
22
import base64 from 'base64-js';
33
import { ready } from '../common/index.js';
44

5+
/** @type {import('@webcontainer/api').WebContainer} */
6+
let vm;
7+
58
/**
69
* @param {import('$lib/types').Stub[]} stubs
710
* @returns {Promise<import('$lib/types').Adapter>}
@@ -21,14 +24,13 @@ export async function create(stubs) {
2124
throw new Error('WebContainers are not supported by Safari');
2225
}
2326

24-
/** @type {import('@webcontainer/api').WebContainer} */
25-
let vm;
26-
2727
const base = await new Promise(async (fulfil, reject) => {
2828
setTimeout(() => {
2929
reject(new Error('Timed out starting WebContainer'));
3030
}, 15000);
3131

32+
// There can only be one instance, else it throws an error - guard against this case
33+
// if there was an error later on or a timeout and the user tries again
3234
if (!vm) {
3335
console.log('loading webcontainer');
3436

@@ -84,15 +86,31 @@ export async function create(stubs) {
8486
);
8587
});
8688

87-
let current = stubs;
89+
/**
90+
* Paths of the currently loaded file stubs
91+
*/
92+
let current = new Set(stubs.filter((stub) => stub.type === 'file').map((stub) => stub.name));
93+
/**
94+
* Keeps track of the latest create/reset to ensure things are not processed in parallel.
95+
* (if this turns out to be insufficient, we can use a queue)
96+
*/
97+
let running = base;
8898

8999
return {
90100
base,
91101

92-
/** @param {import('$lib/types').Stub[]} stubs */
102+
/**
103+
* Deletes old files and adds new ones
104+
* @param {import('$lib/types').Stub[]} stubs
105+
*/
93106
async reset(stubs) {
94-
const old = new Set(current.filter((stub) => stub.type === 'file').map((stub) => stub.name));
95-
current = stubs;
107+
await running;
108+
/** @type {Function} */
109+
let resolve = () => {};
110+
running = new Promise((fulfil) => (resolve = fulfil));
111+
112+
const old = current;
113+
current = new Set(stubs.filter((stub) => stub.type === 'file').map((stub) => stub.name));
96114

97115
for (const stub of stubs) {
98116
if (stub.type === 'file') {
@@ -106,16 +124,21 @@ export async function create(stubs) {
106124
const promise = new Promise((fulfil, reject) => {
107125
const error_unsub = vm.on('error', (error) => {
108126
error_unsub();
127+
resolve();
109128
reject(new Error(error.message));
110129
});
111130

112131
const ready_unsub = vm.on('server-ready', (port, base) => {
113132
ready_unsub();
114133
console.log(`server ready on port ${port} at ${performance.now()}: ${base}`);
134+
resolve();
115135
fulfil(undefined);
116136
});
117137

118-
setTimeout(() => reject(new Error('Timed out resetting WebContainer')), 10000);
138+
setTimeout(() => {
139+
resolve();
140+
reject(new Error('Timed out resetting WebContainer'));
141+
}, 10000);
119142
});
120143

121144
for (const file of old) {
@@ -142,10 +165,17 @@ export async function create(stubs) {
142165
await promise;
143166

144167
await new Promise((f) => setTimeout(f, 200)); // wait for chokidar
168+
169+
resolve();
145170
},
146171

147-
/** @param {import('$lib/types').FileStub[]} stubs */
172+
/**
173+
* Loads new files but keeps the old ones
174+
* @param {import('$lib/types').FileStub[]} stubs
175+
*/
148176
async update(stubs) {
177+
await running;
178+
149179
/** @type {import('@webcontainer/api').FileSystemTree} */
150180
const root = {};
151181

@@ -178,6 +208,8 @@ export async function create(stubs) {
178208

179209
async destroy() {
180210
vm.teardown();
211+
// @ts-ignore
212+
vm = null;
181213
}
182214
};
183215
}

src/lib/client/monaco/monaco.js

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { browser } from '$app/environment';
1+
import { browser, dev } from '$app/environment';
22
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
33
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
44
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker';
@@ -9,6 +9,12 @@ import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
99
let monaco;
1010

1111
if (browser) {
12+
if (dev && !/chrome/i.test(navigator.userAgent)) {
13+
throw new Error(
14+
'The code editor requires Chrome during development, as it uses module workers'
15+
);
16+
}
17+
1218
// @ts-expect-error
1319
self.MonacoEnvironment = {
1420
/**
@@ -37,4 +43,101 @@ if (browser) {
3743
monaco = await import('monaco-editor');
3844
}
3945

40-
export { monaco };
46+
/**
47+
* file extension -> monaco language
48+
* @type {Record<string, string>}
49+
* */
50+
const types = {
51+
js: 'javascript',
52+
ts: 'typescript',
53+
svelte: 'html' // TODO
54+
};
55+
56+
/**
57+
* URL -> model
58+
* @type {Map<string, import('monaco-editor').editor.ITextModel>}
59+
* */
60+
const models = new Map();
61+
62+
let notify_adapter = true;
63+
64+
/**
65+
*
66+
* @param {import('$lib/types').Stub[]} stubs
67+
* @param {() => import('$lib/types').Adapter} adapter
68+
* @param {boolean} [notify]
69+
*/
70+
function update_files(stubs, adapter, notify = true) {
71+
notify_adapter = notify;
72+
for (const stub of stubs) {
73+
if (stub.type === 'directory') {
74+
continue;
75+
}
76+
77+
const model = models.get(stub.name);
78+
79+
if (model) {
80+
const value = model.getValue();
81+
82+
if (stub.contents !== value) {
83+
model.pushEditOperations(
84+
[],
85+
[
86+
{
87+
range: model.getFullModelRange(),
88+
text: stub.contents
89+
}
90+
],
91+
() => null
92+
);
93+
}
94+
} else {
95+
create_file(stub, adapter);
96+
}
97+
}
98+
99+
for (const [name, model] of models) {
100+
if (!stubs.some((stub) => stub.name === name)) {
101+
model.dispose();
102+
models.delete(name);
103+
}
104+
}
105+
notify = true;
106+
}
107+
108+
/**
109+
* @param {import('$lib/types').FileStub} stub
110+
* @param {() => import('$lib/types').Adapter} adapter
111+
*/
112+
function create_file(stub, adapter) {
113+
// deep-copy stub so we can mutate it and not create a memory leak
114+
stub = JSON.parse(JSON.stringify(stub));
115+
116+
const type = /** @type {string} */ (stub.basename.split('.').pop());
117+
118+
const model = monaco.editor.createModel(
119+
stub.contents,
120+
types[type] || type,
121+
new monaco.Uri().with({ path: stub.name })
122+
);
123+
124+
model.updateOptions({ tabSize: 2 });
125+
126+
model.onDidChangeContent(() => {
127+
const contents = model.getValue();
128+
129+
if (notify_adapter) {
130+
stub.contents = contents;
131+
adapter()?.update([stub]);
132+
}
133+
});
134+
135+
models.set(stub.name, model);
136+
}
137+
138+
/** @param {string} name */
139+
function get_model(name) {
140+
return models.get(name);
141+
}
142+
143+
export { monaco, get_model, create_file, update_files };

0 commit comments

Comments
 (0)