Skip to content
This repository has been archived by the owner on Nov 5, 2021. It is now read-only.

Add support for creating a custom webworker subclass #65

Merged
merged 7 commits into from
Sep 7, 2020

Conversation

orta
Copy link
Contributor

@orta orta commented Aug 21, 2020

It had been on my TODO to explore this for a while, but a chat with @evmar yesterday reminded me that I should take a look

This PR allows Monaco users to extend the TypeScriptWorker class used to handle web workers in their own code without having to dig into internals of Monaco.

How it works

It adds a new option (which I'm open to bike shedding on naming, I just wanted something) to the language defaults:

monaco.languages.typescript.typescriptDefaults.setWorkerOptions({ customWorkerPath: "http://localhost:5000/test/custom-worker.js" })

This is used to synchronously call that URL in a webworker, with the JS expecting to edit the global scope of the worker adding a factory function:

self.customTSWorkerFactory = (TypeScriptWorker) => {
  return class MonacoTSWorker extends TypeScriptWorker {

    // Adds a custom function to the webworker
    async getDTSEmitForFile(fileName) {
      const result = await this.getEmitOutput(fileName)
      const firstDTS = result.outputFiles.find(o => o.name.endsWith(".d.ts"))
      return (firstDTS && firstDTS.text) || ""
    }

  }
}

Users can then make calls to the worker using the same APIs as previously:

document.getElementById('logDTS').onclick = async () => {
      const model = editor.getModel()
      const worker = await monaco.languages.typescript.getTypeScriptWorker()
      const thisWorker = await worker(model.uri)
      const dts = await thisWorker.getDTSEmitForFile(model.uri.toString())
      console.log(dts)
};

This can be used to extend the web worker for setups where you do some heavy processing, but want to keep that backgrounded.

@orta
Copy link
Contributor Author

orta commented Aug 21, 2020

Coming back to look at this, I've settled on customTSWorkerFactory as the factory name and customWorkerPath as the option param

@orta
Copy link
Contributor Author

orta commented Aug 24, 2020

@ark120202 mentioned in the ts discord that for the ts2lua playground, they bundle a custom webworker by setting MonacoEnvironment.getWorker allowing for a custom class with functions

This is a cool tactic, and maybe I could get this working using that API entirely. Without digging into it yet, I think the hard bit looks to be how to get access to the original TypeScriptWorker class to extend from in my worker. ( This isn't an issue in their case because they bundle )

@alexdima
Copy link
Member

👍 I've seen lots of people in issues ask for something like this.

Both approaches have advantages and disadvantages. For the ESM bits, our webpack bundler plugin wants to own MonacoEnvironment.getWorker, so that might complicate things there.

My concern is more on the API side. What can we type in a .d.ts file as being safe to use from this custom web worker? How do we ensure that we don't break people by accident with a new release?

@orta
Copy link
Contributor Author

orta commented Aug 30, 2020

I was going to add a public API interface for the worker, which the class implements but there are already type definitions for the public API for the TS worker in monaco.languages.typescript.TypeScriptWorker

If the class breaks that public API, then it's a breaking change for downstream consumers of this method, which _ I think_ should be enough. If you'd like a test for that via the compiler, I can duplicate the interface separately and have that be used to validate the class conforms to an unchanging interface?

@alexdima
Copy link
Member

monaco.languages.typescript.TypeScriptWorker is the worker proxy available on the UI side via monaco.languages.getTypeScriptWorker().

I thought the custom worker proposed here would be useful for people to interact directly with the ts service.

@orta
Copy link
Contributor Author

orta commented Aug 31, 2020

Great point, I'll expand this demo with some of the TS infra I was expecting to use ( microsoft/TypeScript-Website#998 ) and pass in the worker's version of TS to the factory function to give it a real world example

@orta
Copy link
Contributor Author

orta commented Aug 31, 2020

OK, I've got a non-trivial example now running.

This is in the same ballpark as what I'd use in the TS playground (but without caching the program etc.) In the web-worker it creates a fully virtual FS, makes a TS program, extracts the AST of the editor's text as a string and passes that back to the site which logs it out.

I'd class the api shape of the factory function as Monaco-typescript's public API, and so I've used:

/** The shape of the factory */
export interface CustomTSWebWorkerFactory {
	(TSWorkerClass: typeof TypeScriptWorker, ts: typeof import("typescript"), libs: Record<string, string>): typeof TypeScriptWorker
}

As a check that we keep our side of that contract. It also means folks can use that type themselves like I do via JSDoc etc.

@orta orta force-pushed the custom_worker branch 2 times, most recently from 18b0f5d to 5ee395c Compare August 31, 2020 17:48
package.json Show resolved Hide resolved
src/monaco.contribution.ts Show resolved Hide resolved
src/monaco.contribution.ts Show resolved Hide resolved
test/custom-worker.js Show resolved Hide resolved
@alexdima alexdima added this to the August 2020 milestone Sep 7, 2020
@alexdima alexdima self-assigned this Sep 7, 2020
@alexdima alexdima merged commit 251cec6 into master Sep 7, 2020
@alexdima
Copy link
Member

alexdima commented Sep 7, 2020

👍 Makes good sense!

@nikhilnxvverma1
Copy link

Hello, Can this API be used to create an ambient context in typescript? Much like how you have you see a *.d.ts files in VSCode. If so how?

return (firstDTS && firstDTS.text) || ""
}

async printAST(fileName) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@orta this is a very nice example that might come handy to me, thank you for that! I'm wondering though - isn't there a way to just access the existing program? Recreating it like here seems like an overkill for what I need - especially that I'd have to learn how to properly configure it in the very same way that it's done by default and it's easy to do some mistakes in that.

Copy link
Contributor Author

@orta orta Oct 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it can use this._langaugeService.getProgram to grab it

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly what I ended up doing:
https://github.com/statelyai/xstate-viz/blob/cd128a15486d9253edaaa360ee6c1b150f293f5c/public/ts-worker.js#L59

but it's not a part of the public interface so feels like a hack. I wonder if this could be added as part of the public API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave that one to @alexdima to decide, but I'm also reaching in for it in microsoft/TypeScript-Website#2063 - which is a pretty good argument for moving it out of private IMO.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR welcome.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants