microsoft / monaco-editor

A browser based code editor
https://microsoft.github.io/monaco-editor/
MIT License
40.43k stars 3.6k forks source link

Unexpected usage exception when using `handlebars`, `razor`, `scss` or `less` #2212

Closed Prinzhorn closed 3 years ago

Prinzhorn commented 4 years ago

I just got "Uncaught Error: Unexpected usage" myself. I found that there are so many issues and nobody seems to care. I looked into it and the root cause seems to be too simple to be true?

  1. This always passed null as foreignModule argument https://github.com/microsoft/vscode/blob/b449c95a8460443ee6bf985bb6c8db63d395e6e6/src/vs/editor/editor.worker.ts#L30
  2. The foreignModule variable inside the scope is never changed, so the second constructor argument here is always null too
  3. This means this._foreignModuleFactory is always null as well https://github.com/microsoft/vscode/blob/b449c95a8460443ee6bf985bb6c8db63d395e6e6/src/vs/editor/common/services/editorSimpleWorker.ts#L342
  4. Which prevents this if block from being entered https://github.com/microsoft/vscode/blob/b449c95a8460443ee6bf985bb6c8db63d395e6e6/src/vs/editor/common/services/editorSimpleWorker.ts#L645
  5. And fall through to here https://github.com/microsoft/vscode/blob/b449c95a8460443ee6bf985bb6c8db63d395e6e6/src/vs/editor/common/services/editorSimpleWorker.ts#L662

@alexdima any idea why you did that (ignore the first worker message)? https://github.com/microsoft/vscode/commit/66091601a52d4f495e8b90c8c56283a5b1b07bd0

The issue can also be reproduced in the official ESM examples and it has likely been present from day one of ESM support. All you need to do is use a language that uses the generic worker, e.g. put handlebars here https://github.com/microsoft/monaco-editor-samples/blob/c5586a25cfd069349b6242a3d140223cc3d9110c/browser-esm-parcel/src/index.js#L23

akryvasheyeu commented 4 years ago

looks like it's difficult to build workers correctly

in my case a got this error too with Angular 9 and inner exception (inside worker): regeneratorRuntime is not defined. to solve this I wrapped each worker in an angular worker (https://angular.io/guide/web-worker):

what I got for example:

editor.worker.ts:

/// <reference lib="webworker" />

import 'monaco-editor/esm/vs/editor/editor.worker.js';

json.worker.ts:

/// <reference lib="webworker" />

import 'monaco-editor/esm/vs/language/json/json.worker.js';

after I defined getWorker callback in MonacoEnvironment:

self.MonacoEnvironment = {
  getWorker: (_, label) => {
    if (label === 'json') {
      return new Worker('./json.worker.ts', {
        type: 'module',
      });
    }

    return new Worker('./editor.worker.ts', {
      type: 'module',
    });
  },
};

and this works great for development and production builds, maybe it will helpful for someone

alexdima commented 3 years ago

@Prinzhorn

This is a complex topic because code is reused across AMD and ESM and things work differently under the two module systems.

The main conceptual difference is that under AMD, the web worker always starts with the entry point workerMain.js, which then imports editorSimpleWorker.js (this is sent in an initial message), which then can optionally loads custom web worker code if the web worker is a language worker (e.g. cssWorker require([moduleId]). The module id to load is received as a message by the web worker. This is possible in AMD because the AMD loader contains sufficient knowledge at runtime to convert the module id to a script path and load the code using importScripts. On the other hand, under ESM, dynamic imports don't generally work e.g. await import(variableContainingModuleId) will throw when processed by Webpack or other bundlers. Under ESM, all the imports must be spelled out. That is why the strategy under ESM is for the embedder of the editor to define a function MonacoEnvironment.getWorkerUrl. When the needed worker must be created, instead of creating workerMain.js as in the AMD case, the custom worker is started (like css.worker.js which then loads editor.worker.js which then loads editorSimpleWorker.js). So under ESM, the generic code is loaded by the custom web worker code, while under AMD the generic code loads the custom web worker code.

The Unexpected Usage error comes when the embedder fails to define MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker correctly in the ESM case.

Now to specifically answer your question, editor.worker.js is also loaded directly by the core editor for the generic web worker the core runs for diffing, link computation, word suggestions, etc.

The code on the UI side which constructs web workers and talks to them does not have the knowledge if it is running under ESM or under AMD. So it behaves the same in both cases. That is why even in the ESM case, the code on the UI side sends an initial message to load editorSimpleWorker.js which is discarded here. You can see that custom web workers also ignore the initial message e.g. here.

As I mentioned in the beginning, this is quite complex because the same code is reused under AMD and ESM, the UI side behaves the same way regardless of AMD or ESM, and there is the generic editor core web worker and there are custom web workers that the languages use using the same infrastructure.

I am open to having made a mistake in this area, but if that is the case, can you please provide reproducible steps instead of an analysis?

Prinzhorn commented 3 years ago

Thanks for looking into this.

I am open to having made a mistake in this area, but if that is the case, can you please provide reproducible steps instead of an analysis?

Like I said, the official examples trigger the error (both browser-esm-parcel and browser-esm-webpack). Otherwise I'd probably not have made such bold claims :sweat_smile:

Here are the detailed steps:

git clone https://github.com/Microsoft/monaco-editor-samples.git
cd monaco-editor-samples
npm install .
npm run simpleserver

So far this is copied from the official example repo. Now update the language to one that needs a custom worker:

sed -i "s/language: 'javascript'/language: 'handlebars'/g" browser-esm-parcel/src/index.js
 monaco.editor.create(document.getElementById('container'), {
    value: ['function x() {', '\tconsole.log("Hello world!");', '}'].join('\n'),
-   language: 'javascript'
+   language: 'handlebars'
 });

And now follow the official steps to build the parcel esm example

cd browser-esm-parcel 
npm install .
npm run build
npm run simpleserver

Visit http://localhost:9999/

Chromium

Selection_760

Firefox

Selection_761

Like I said this works equally for browser-esm-webpack, I just happen to use parcel for my projects.

Prinzhorn commented 3 years ago

Hm, this seems to be an issue with handlebars in particular. I tried some other languages and also some garbage like foobarbaz and nothing else triggers it.

alexdima commented 3 years ago

I can reproduce. It looks like handlebars and razor are configured to have web workers by monaco-html here and they are not covered in any of the samples correctly.

Edit: It looks that monaco-css is also doing this for scss and less.

alexdima commented 3 years ago

Looks like the monaco-editor-webpack-plugin already supported this here, so there are no more places where these languages need to be supported in ESM.

Prinzhorn commented 3 years ago

Nice, thank you