From 48bd5101255d7678f9b4fab7673a7065f4b50368 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 9 Nov 2023 20:22:20 +0100 Subject: [PATCH] Implements prototype patching hot-reload --- scripts/debugger-scripts-api.d.ts | 4 +- scripts/hot-reload-injected-script.js | 8 +++- src/vs/base/common/hotReload.ts | 41 +++++++++++++++++-- .../bracketPairsImpl.ts | 2 + .../common/model/tokenizationTextModelPart.ts | 2 + 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/scripts/debugger-scripts-api.d.ts b/scripts/debugger-scripts-api.d.ts index 149912bc04f..0a06ad925e0 100644 --- a/scripts/debugger-scripts-api.d.ts +++ b/scripts/debugger-scripts-api.d.ts @@ -15,8 +15,8 @@ interface IDisposable { dispose(): void; } -interface GlobalThisAddition extends globalThis { - $hotReload_applyNewExports?(oldExports: Record): AcceptNewExportsFn | undefined; +interface GlobalThisAddition { + $hotReload_applyNewExports?(args: { oldExports: Record; newSrc: string }): AcceptNewExportsFn | undefined; } type AcceptNewExportsFn = (newExports: Record) => boolean; diff --git a/scripts/hot-reload-injected-script.js b/scripts/hot-reload-injected-script.js index c6311f3b9c9..8d082f1a139 100644 --- a/scripts/hot-reload-injected-script.js +++ b/scripts/hot-reload-injected-script.js @@ -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. diff --git a/src/vs/base/common/hotReload.ts b/src/vs/base/common/hotReload.ts index 17724907937..3544ff2c514 100644 --- a/src/vs/base/common/hotReload.ts +++ b/src/vs/base/common/hotReload.ts @@ -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) => AcceptNewExportsHandler | undefined; +export type HotReloadHandler = (args: { oldExports: Record; newSrc: string }) => AcceptNewExportsHandler | undefined; export type AcceptNewExportsHandler = (newExports: Record) => boolean; function registerGlobalHotReloadHandler() { @@ -50,10 +49,44 @@ function registerGlobalHotReloadHandler() { return hotReloadHandlers; } -let hotReloadHandlers: Set<(oldExports: Record) => AcceptNewExportsFn | undefined> | undefined = undefined; +let hotReloadHandlers: Set<(args: { oldExports: Record; newSrc: string }) => AcceptNewExportsFn | undefined> | undefined = undefined; interface GlobalThisAddition { - $hotReload_applyNewExports?(oldExports: Record): AcceptNewExportsFn | undefined; + $hotReload_applyNewExports?(args: { oldExports: Record; newSrc: string }): AcceptNewExportsFn | undefined; } + type AcceptNewExportsFn = (newExports: Record) => 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; + }; + }); +} diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts index 470a16f009c..4df6b83148d 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts @@ -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>()); diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index 54476c26f20..27658b9db64 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -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);