Implements prototype patching hot-reload

This commit is contained in:
Henning Dieterichs 2023-11-09 20:22:20 +01:00 committed by Henning Dieterichs
parent acff02431f
commit 48bd510125
5 changed files with 49 additions and 8 deletions

View file

@ -15,8 +15,8 @@ interface IDisposable {
dispose(): void;
}
interface GlobalThisAddition extends globalThis {
$hotReload_applyNewExports?(oldExports: Record<string, unknown>): AcceptNewExportsFn | undefined;
interface GlobalThisAddition {
$hotReload_applyNewExports?(args: { oldExports: Record<string, unknown>; newSrc: string }): AcceptNewExportsFn | undefined;
}
type AcceptNewExportsFn = (newExports: Record<string, unknown>) => boolean;

View file

@ -85,7 +85,7 @@ module.exports.run = async function (debugSession) {
// A frozen copy of the previous exports
const oldExports = Object.freeze({ ...oldModule.exports });
const reloadFn = g.$hotReload_applyNewExports?.(oldExports);
const reloadFn = g.$hotReload_applyNewExports?.({ oldExports, newSrc });
if (!reloadFn) {
console.log(debugSessionName, 'ignoring js change, as module does not support hot-reload', relativePath);
@ -94,7 +94,11 @@ module.exports.run = async function (debugSession) {
return;
}
const newScript = new Function('define', newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality.
// Eval maintains source maps
function newScript(/* this parameter is used by newSrc */ define) {
// eslint-disable-next-line no-eval
eval(newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality.
}
newScript(/* define */ function (deps, callback) {
// Evaluating the new code was successful.

View file

@ -14,7 +14,6 @@ export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable
return { dispose() { } };
} else {
const handlers = registerGlobalHotReloadHandler();
handlers.add(handler);
return {
dispose() { handlers.delete(handler); }
@ -28,7 +27,7 @@ export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable
*
* If no handler can apply the new exports, the module will not be reloaded.
*/
export type HotReloadHandler = (oldExports: Record<string, unknown>) => AcceptNewExportsHandler | undefined;
export type HotReloadHandler = (args: { oldExports: Record<string, unknown>; newSrc: string }) => AcceptNewExportsHandler | undefined;
export type AcceptNewExportsHandler = (newExports: Record<string, unknown>) => boolean;
function registerGlobalHotReloadHandler() {
@ -50,10 +49,44 @@ function registerGlobalHotReloadHandler() {
return hotReloadHandlers;
}
let hotReloadHandlers: Set<(oldExports: Record<string, unknown>) => AcceptNewExportsFn | undefined> | undefined = undefined;
let hotReloadHandlers: Set<(args: { oldExports: Record<string, unknown>; newSrc: string }) => AcceptNewExportsFn | undefined> | undefined = undefined;
interface GlobalThisAddition {
$hotReload_applyNewExports?(oldExports: Record<string, unknown>): AcceptNewExportsFn | undefined;
$hotReload_applyNewExports?(args: { oldExports: Record<string, unknown>; newSrc: string }): AcceptNewExportsFn | undefined;
}
type AcceptNewExportsFn = (newExports: Record<string, unknown>) => boolean;
if (isHotReloadEnabled()) {
// This code does not run in production.
registerHotReloadHandler(({ oldExports, newSrc }) => {
// Don't match its own source code
if (newSrc.indexOf('/* ' + 'hot-reload:patch-prototype-methods */') === -1) {
return undefined;
}
return newExports => {
for (const key in newExports) {
const exportedItem = newExports[key];
console.log(`[hot-reload] Patching prototype methods of '${key}'`, { exportedItem });
if (typeof exportedItem === 'function' && exportedItem.prototype) {
const oldExportedItem = oldExports[key];
if (oldExportedItem) {
for (const prop of Object.getOwnPropertyNames(exportedItem.prototype)) {
const descriptor = Object.getOwnPropertyDescriptor(exportedItem.prototype, prop)!;
const oldDescriptor = Object.getOwnPropertyDescriptor((oldExportedItem as any).prototype, prop);
if (descriptor?.value?.toString() !== oldDescriptor?.value?.toString()) {
console.log(`[hot-reload] Patching prototype method '${key}.${prop}'`);
}
Object.defineProperty((oldExportedItem as any).prototype, prop, descriptor);
}
newExports[key] = oldExportedItem;
}
}
}
return true;
};
});
}

View file

@ -18,6 +18,8 @@ import { BracketInfo, BracketPairInfo, BracketPairWithMinIndentationInfo, IBrack
import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent } from 'vs/editor/common/textModelEvents';
import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
/* hot-reload:patch-prototype-methods */
export class BracketPairsTextModelPart extends Disposable implements IBracketPairsTextModelPart {
private readonly bracketPairsTree = this._register(new MutableDisposable<IReference<BracketPairsTree>>());

View file

@ -32,6 +32,8 @@ import { LineTokens } from 'vs/editor/common/tokens/lineTokens';
import { SparseMultilineTokens } from 'vs/editor/common/tokens/sparseMultilineTokens';
import { SparseTokensStore } from 'vs/editor/common/tokens/sparseTokensStore';
/* hot-reload:patch-prototype-methods */
export class TokenizationTextModelPart extends TextModelPart implements ITokenizationTextModelPart {
private readonly _semanticTokens: SparseTokensStore = new SparseTokensStore(this._languageService.languageIdCodec);