Skip to content

Commit b8a3395

Browse files
authored
Implements hot reloading of js for the monaco playground server. (microsoft#185555)
1 parent b975405 commit b8a3395

File tree

1 file changed

+93
-37
lines changed

1 file changed

+93
-37
lines changed

scripts/playground-server.ts

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ function main() {
2323
const editorMainBundle = new CachedBundle('vs/editor/editor.main', moduleIdMapper);
2424
fileServer.overrideFileContent(editorMainBundle.entryModulePath, () => editorMainBundle.bundle());
2525

26-
const hotReloadJsCode = getHotReloadCode(new URL('/file-changes', server.url));
2726
const loaderPath = path.join(rootDir, 'out/vs/loader.js');
2827
fileServer.overrideFileContent(loaderPath, async () =>
29-
Buffer.from(new TextEncoder().encode(`${await fsPromise.readFile(loaderPath, 'utf8')}\n${hotReloadJsCode}`))
28+
Buffer.from(new TextEncoder().encode(makeLoaderJsHotReloadable(await fsPromise.readFile(loaderPath, 'utf8'), new URL('/file-changes', server.url))))
3029
);
3130

3231
const watcher = DirWatcher.watchRecursively(moduleIdMapper.rootDir);
@@ -35,7 +34,7 @@ function main() {
3534
editorMainBundle.bundle();
3635
console.log(`${new Date().toLocaleTimeString()}, file change: ${path}`);
3736
});
38-
server.use('/file-changes', handleGetFileChangesRequest(watcher, fileServer));
37+
server.use('/file-changes', handleGetFileChangesRequest(watcher, fileServer, moduleIdMapper));
3938

4039
console.log(`Server listening on ${server.url}`);
4140
}
@@ -169,6 +168,12 @@ function getContentType(filePath: string): string {
169168
return 'image/png';
170169
case '.jpg':
171170
return 'image/jpg';
171+
case '.svg':
172+
return 'image/svg+xml';
173+
case '.html':
174+
return 'text/html';
175+
case '.wasm':
176+
return 'application/wasm';
172177
default:
173178
return 'text/plain';
174179
}
@@ -215,56 +220,107 @@ class DirWatcher {
215220
}
216221
}
217222

218-
function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer): ChainableRequestHandler {
223+
function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer, moduleIdMapper: SimpleModuleIdPathMapper): ChainableRequestHandler {
219224
return async (req, res) => {
220225
res.writeHead(200, { 'Content-Type': 'text/plain' });
221226
const d = watcher.onDidChange(fsPath => {
222227
const path = fileServer.filePathToUrlPath(fsPath);
223228
if (path) {
224-
res.write(JSON.stringify({ changedPath: path }) + '\n');
229+
res.write(JSON.stringify({ changedPath: path, moduleId: moduleIdMapper.getModuleId(fsPath) }) + '\n');
225230
}
226231
});
227232
res.on('close', () => d.dispose());
228233
};
229234
}
235+
function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): string {
236+
loaderJsCode = loaderJsCode.replace(
237+
/constructor\(env, scriptLoader, defineFunc, requireFunc, loaderAvailableTimestamp = 0\) {/,
238+
'$&globalThis.$$globalModuleManager = this;'
239+
);
230240

231-
function getHotReloadCode(fileChangesUrl: URL): string {
232-
const additionalJsCode = `
233-
function $watchChanges() {
234-
console.log("Connecting to server to watch for changes...");
235-
fetch(${JSON.stringify(fileChangesUrl)})
236-
.then(async request => {
237-
const reader = request.body.getReader();
238-
let buffer = '';
239-
while (true) {
240-
const { done, value } = await reader.read();
241-
if (done) { break; }
242-
buffer += new TextDecoder().decode(value);
243-
const lines = buffer.split('\\n');
244-
buffer = lines.pop();
245-
for (const line of lines) {
246-
const data = JSON.parse(line);
247-
if (data.changedPath.endsWith('.css')) {
248-
console.log('css changed', data.changedPath);
249-
const styleSheet = [...document.querySelectorAll("link[rel='stylesheet']")].find(l => new URL(l.href, document.location.href).pathname.endsWith(data.changedPath));
250-
if (styleSheet) {
251-
styleSheet.href = styleSheet.href.replace(/\\?.*/, '') + '?' + Date.now();
241+
const $$globalModuleManager: any = undefined;
242+
243+
// This code will be appended to loader.js
244+
function $watchChanges(fileChangesUrl: string) {
245+
let reloadFn;
246+
if (globalThis.$sendMessageToParent) {
247+
reloadFn = () => globalThis.$sendMessageToParent({ kind: 'reload' });
248+
} else if (typeof window !== 'undefined') {
249+
reloadFn = () => window.location.reload();
250+
} else {
251+
reloadFn = () => { };
252+
}
253+
254+
console.log('Connecting to server to watch for changes...');
255+
(fetch as any)(fileChangesUrl)
256+
.then(async request => {
257+
const reader = request.body.getReader();
258+
let buffer = '';
259+
while (true) {
260+
const { done, value } = await reader.read();
261+
if (done) { break; }
262+
buffer += new TextDecoder().decode(value);
263+
const lines = buffer.split('\n');
264+
buffer = lines.pop()!;
265+
for (const line of lines) {
266+
const data = JSON.parse(line);
267+
let handled = false;
268+
if (data.changedPath.endsWith('.css')) {
269+
console.log('css changed', data.changedPath);
270+
const styleSheet = [...document.querySelectorAll(`link[rel='stylesheet']`)].find((l: any) => new URL(l.href, document.location.href).pathname.endsWith(data.changedPath)) as any;
271+
if (styleSheet) {
272+
styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now();
273+
}
274+
handled = true;
275+
} else if (data.changedPath.endsWith('.js') && data.moduleId) {
276+
console.log('js changed', data.changedPath);
277+
const moduleId = $$globalModuleManager._moduleIdProvider.getModuleId(data.moduleId);
278+
if ($$globalModuleManager._modules2[moduleId]) {
279+
const srcUrl = $$globalModuleManager._config.moduleIdToPaths(data.moduleId);
280+
const newSrc = await (await fetch(srcUrl)).text();
281+
(new Function('define', newSrc))(function (deps, callback) {
282+
const oldModule = $$globalModuleManager._modules2[moduleId];
283+
delete $$globalModuleManager._modules2[moduleId];
284+
285+
$$globalModuleManager.defineModule(data.moduleId, deps, callback);
286+
const newModule = $$globalModuleManager._modules2[moduleId];
287+
const oldExports = { ...oldModule.exports };
288+
289+
Object.assign(oldModule.exports, newModule.exports);
290+
newModule.exports = oldModule.exports;
291+
292+
handled = true;
293+
294+
for (const cb of [...globalThis.$hotReload_deprecateExports]) {
295+
cb(oldExports, newModule.exports);
296+
}
297+
298+
if (handled) {
299+
console.log('hot reloaded', data.moduleId);
300+
}
301+
});
302+
}
252303
}
253-
} else {
254-
$sendMessageToParent({ kind: "reload" });
304+
305+
if (!handled) { reloadFn(); }
255306
}
256307
}
257-
}
258-
})
259-
.catch(err => {
260-
console.error(err);
261-
setTimeout($watchChanges, 1000);
262-
});
308+
}).catch(err => {
309+
console.error(err);
310+
setTimeout(() => $watchChanges(fileChangesUrl), 1000);
311+
});
263312

264-
}
265-
$watchChanges();
313+
}
314+
315+
const additionalJsCode = `
316+
(${(function () {
317+
globalThis.$hotReload_deprecateExports = new Set<(oldExports: any, newExports: any) => void>();
318+
}).toString()})();
319+
${$watchChanges.toString()}
320+
$watchChanges(${JSON.stringify(fileChangesUrl)});
266321
`;
267-
return additionalJsCode;
322+
323+
return `${loaderJsCode}\n${additionalJsCode}`;
268324
}
269325

270326
// #endregion

0 commit comments

Comments
 (0)