Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements hot reloading of js for the monaco playground server. #185555

Merged
merged 1 commit into from
Jun 19, 2023
Merged
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
130 changes: 93 additions & 37 deletions scripts/playground-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ function main() {
const editorMainBundle = new CachedBundle('vs/editor/editor.main', moduleIdMapper);
fileServer.overrideFileContent(editorMainBundle.entryModulePath, () => editorMainBundle.bundle());

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

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

console.log(`Server listening on ${server.url}`);
}
Expand Down Expand Up @@ -169,6 +168,12 @@ function getContentType(filePath: string): string {
return 'image/png';
case '.jpg':
return 'image/jpg';
case '.svg':
return 'image/svg+xml';
case '.html':
return 'text/html';
case '.wasm':
return 'application/wasm';
default:
return 'text/plain';
}
Expand Down Expand Up @@ -215,56 +220,107 @@ class DirWatcher {
}
}

function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer): ChainableRequestHandler {
function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer, moduleIdMapper: SimpleModuleIdPathMapper): ChainableRequestHandler {
return async (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
const d = watcher.onDidChange(fsPath => {
const path = fileServer.filePathToUrlPath(fsPath);
if (path) {
res.write(JSON.stringify({ changedPath: path }) + '\n');
res.write(JSON.stringify({ changedPath: path, moduleId: moduleIdMapper.getModuleId(fsPath) }) + '\n');
}
});
res.on('close', () => d.dispose());
};
}
function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): string {
loaderJsCode = loaderJsCode.replace(
/constructor\(env, scriptLoader, defineFunc, requireFunc, loaderAvailableTimestamp = 0\) {/,
'$&globalThis.$$globalModuleManager = this;'
);

function getHotReloadCode(fileChangesUrl: URL): string {
const additionalJsCode = `
function $watchChanges() {
console.log("Connecting to server to watch for changes...");
fetch(${JSON.stringify(fileChangesUrl)})
.then(async request => {
const reader = request.body.getReader();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) { break; }
buffer += new TextDecoder().decode(value);
const lines = buffer.split('\\n');
buffer = lines.pop();
for (const line of lines) {
const data = JSON.parse(line);
if (data.changedPath.endsWith('.css')) {
console.log('css changed', data.changedPath);
const styleSheet = [...document.querySelectorAll("link[rel='stylesheet']")].find(l => new URL(l.href, document.location.href).pathname.endsWith(data.changedPath));
if (styleSheet) {
styleSheet.href = styleSheet.href.replace(/\\?.*/, '') + '?' + Date.now();
const $$globalModuleManager: any = undefined;

// This code will be appended to loader.js
function $watchChanges(fileChangesUrl: string) {
let reloadFn;
if (globalThis.$sendMessageToParent) {
reloadFn = () => globalThis.$sendMessageToParent({ kind: 'reload' });
} else if (typeof window !== 'undefined') {
reloadFn = () => window.location.reload();
} else {
reloadFn = () => { };
}

console.log('Connecting to server to watch for changes...');
(fetch as any)(fileChangesUrl)
.then(async request => {
const reader = request.body.getReader();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) { break; }
buffer += new TextDecoder().decode(value);
const lines = buffer.split('\n');
buffer = lines.pop()!;
for (const line of lines) {
const data = JSON.parse(line);
let handled = false;
if (data.changedPath.endsWith('.css')) {
console.log('css changed', data.changedPath);
const styleSheet = [...document.querySelectorAll(`link[rel='stylesheet']`)].find((l: any) => new URL(l.href, document.location.href).pathname.endsWith(data.changedPath)) as any;
if (styleSheet) {
styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now();
}
handled = true;
} else if (data.changedPath.endsWith('.js') && data.moduleId) {
console.log('js changed', data.changedPath);
const moduleId = $$globalModuleManager._moduleIdProvider.getModuleId(data.moduleId);
if ($$globalModuleManager._modules2[moduleId]) {
const srcUrl = $$globalModuleManager._config.moduleIdToPaths(data.moduleId);
const newSrc = await (await fetch(srcUrl)).text();
(new Function('define', newSrc))(function (deps, callback) {
const oldModule = $$globalModuleManager._modules2[moduleId];
delete $$globalModuleManager._modules2[moduleId];

$$globalModuleManager.defineModule(data.moduleId, deps, callback);
const newModule = $$globalModuleManager._modules2[moduleId];
const oldExports = { ...oldModule.exports };

Object.assign(oldModule.exports, newModule.exports);
newModule.exports = oldModule.exports;

handled = true;

for (const cb of [...globalThis.$hotReload_deprecateExports]) {
cb(oldExports, newModule.exports);
}

if (handled) {
console.log('hot reloaded', data.moduleId);
}
});
}
}
} else {
$sendMessageToParent({ kind: "reload" });

if (!handled) { reloadFn(); }
}
}
}
})
.catch(err => {
console.error(err);
setTimeout($watchChanges, 1000);
});
}).catch(err => {
console.error(err);
setTimeout(() => $watchChanges(fileChangesUrl), 1000);
});

}
$watchChanges();
}

const additionalJsCode = `
(${(function () {
globalThis.$hotReload_deprecateExports = new Set<(oldExports: any, newExports: any) => void>();
}).toString()})();
${$watchChanges.toString()}
$watchChanges(${JSON.stringify(fileChangesUrl)});
`;
return additionalJsCode;

return `${loaderJsCode}\n${additionalJsCode}`;
}

// #endregion
Expand Down
Loading