monaco-editor icon indicating copy to clipboard operation
monaco-editor copied to clipboard

[Bug] Using workers with the deprecated AMD build

Open spahnke opened this issue 2 months ago • 1 comments

Reproducible in vscode.dev or in VS Code Desktop?

  • [x] Not reproducible in vscode.dev or VS Code Desktop

Reproducible in the monaco editor playground?

Monaco Editor Playground Link

No response

Monaco Editor Playground Code


Reproduction Steps

No response

Actual (Problematic) Behavior

No response

Expected Behavior

No response

Additional Context

Let me preface this with I understand that you want to move forward to ESM builds, and I understand deprecation of the AMD build, and I'm thankful that is still included for now. However, I can't just switch to ESM with everything implied by that especially bundler integration into a bigger project etc. without allocating a long and dedicated period of time to do so which is just not possible right now or the near future 🙁 Furthermore, as long as AMD is still included I'd like to at least keep up-to-date especially with regards to the TS version which hasn't been updated for a long while now. So I'd like to ask for some help here. This is for Monaco version 0.54.0 because I want to get at least this sorted out before the even more breaking-changes coming in 0.55.0 regarding language integration.

My integration of the Monaco editor relies on a worker running ESLint, that was built as an AMD module and then loaded using monaco.editor.createWebWorker, and makes use of the mirror models. The changelog states "Custom AMD workers don't work anymore out of the box.". I was able to at least figure out that I can compile the worker code as a normal script, create the Worker myself and then call the method like

const worker = new Worker(window.location.origin + "/worker/eslint-worker.js");
this.webWorker = monaco.editor.createWebWorker<IEsLintWorker>({
	worker,
	// moduleId: window.location.origin + "/worker/eslint-worker",
	// label: this.owner,
	// createData: { config: this.createEsLintCompatibleConfig() } as IWorkerCreateData
});
return this.webWorker.getProxy();

where the commented lines were the API used before. However, I can't figure out what I need to do for plumbing, so that the create function is still called and provides a) the worker context (monaco.worker.IWorkerContext) for the mirror models, and b) the createData. Can I make this work without completely switching to ESM? What code can I consult? The only messages I receive inside the worker are -please-ignore- and $initialize, after which the promise returned by the call to getProxy() never resolves and no further messages arrive. What's the protocol here? Debugging all those tangled event handlers and figuring out what to do when and where when my worker isn't the only one loaded is pretty hard...

Do I really have to resort back to a custom build worker integration without mirror model support? 🙁

spahnke avatar Nov 13 '25 08:11 spahnke

For now I found a solution, let me know if this is the proper way as well when using the ESM build in the future, and please also let me know if I can simplify stuff.

I now build the actual worker script as an ESM module and bundle that separately importing the worker boilerplate by mimicking what e.g. the TS worker does, like this at the start of the worker code. The rest of the worker code can remain the same and still has its create function like in the AMD case. As source I used https://github.com/microsoft/monaco-editor/pull/4950 especially the common/initialize.ts and common/workers.ts, as well as ts.worker.ts.

import { start } from "../../../node_modules/monaco-editor/esm/vs/editor/editor.worker.start.js";

self.onmessage = (e) => {
	console.log(e);
	// ignore the first message
	initialize((ctx: monaco.worker.IWorkerContext, createData: IWorkerCreateData) => {
		return create(ctx, createData);
	});
};

let initialized = false;

export function isWorkerInitialized(): boolean {
	return initialized;
}

export function initialize(callback: (ctx: any, createData: any) => any): void {
	initialized = true;
	self.onmessage = (m) => {
		start((ctx: any) => {
			return callback(ctx, m.data);
		});
	};
}

To load I then (like above) create the Worker object myself and post one ignore message and one message with the creation parameters like this:

const worker = new Worker(window.location.origin + "/worker/eslint-worker.js", { type: "module" });
worker.postMessage('ignore');
worker.postMessage({ config: this.createEsLintCompatibleConfig() } as IWorkerCreateData);

spahnke avatar Nov 13 '25 12:11 spahnke

The worker situation already caused me a ton of headache, and I'd love to clean that up.

Are you just depending on these types?

export namespace worker {

	export interface IMirrorTextModel {
		readonly version: number;
	}

	export interface IMirrorModel extends IMirrorTextModel {
		readonly uri: Uri;
		readonly version: number;
		getValue(): string;
	}

	export interface IWorkerContext<H = {}> {
		/**
		 * A proxy to the main thread host object.
		 */
		host: H;
		/**
		 * Get all available mirror models in this worker.
		 */
		getMirrorModels(): IMirrorModel[];
	}

}

... a custom synchronization should be quite easy then.

You could also explore using LSP, which offers a standardized way to synchronize models.

hediet avatar Dec 16 '25 16:12 hediet

Created https://github.com/microsoft/monaco-editor/issues/5148 for cleaning up the worker mess. At least that mirroring "helper" code should live in the monaco-editor repo, not VS Code.

hediet avatar Dec 16 '25 16:12 hediet

On the client side I depend on all of these, where I basically call withSyncedResources for the current model URI(s) right before doing an RPC using the returned proxy of getProxy.

declare namespace editor {
    export function createWebWorker<T extends object>(opts: IInternalWebWorkerOptions): MonacoWebWorker<T>;
}

export interface MonacoWebWorker<T> {
    dispose(): void;
    getProxy(): Promise<T>;
    withSyncedResources(resources: Uri[]): Promise<T>;
}

On the worker side I depend on exactly the types you mentioned. Calling getMirrorModels to find the corresponding model and getValue to pass the code to ESLint. The ESLint diagnostics then go back client side via the proxy and are converted into editor diagnostics.

Going the LSP route for the diagnostics seems like a better way in the long run and would get rid of the manual model syncing, if it is sufficiently simple and doesn't introduce tons of more complexity. But strictly speaking I use the proxy/RPC mechanism for more than just the diagnostics and also use it to retrieve some metadata from the library, which is unrelated to language features (e.g. version, rule metadata, ...). Is there a mechanism to retrieve such metadata from a language server worker? I'm not completely familiar with the protocol and corresponding monaco API. Or would that be another manual migration using plain worker messages and reimplementing type safety?

How would a minimal worker implementation look like that e.g. just reports diagnostics, and how would I hook that up to monaco exactly?

spahnke avatar Dec 17 '25 06:12 spahnke