reactjstypescriptwebpackcreate-react-appmonaco-editor

How to load npm module type definition in Monaco using Webpack and react create-react-app


I wanted to load some module type definitions in Monaco inside a react app for tutorial purposes.

I actually managed to get it working after much pain but in a very hacky way. So I'm not asking how to do it, but rather, how to do it right.

The part I hope I can solve with Webpack is that right now I made a Node.js script that take all the .d.ts file it can find in the build folder of a private npm module and save them inside a large .json file.

in this format

{ [filePath]: 'fileContentAsString' }

Then in react I import that json and call addExtraLib for each one.

for (const filePath in sdkTypesJson) {
  // We add every .d.ts file to Monaco
  monaco.languages.typescript.typescriptDefaults.addExtraLib(
    sdkTypesJson[filePath],
    'file:///' + filePath.replace('dist/src/', '')
  );
}

Is there a way with some webpack magic to avoid having to create the json file?


Solution

  • After two days of pain and misery, this is what I found work the best.

    const files = require.context('!!raw-loader!./node_modules/my-module-name/dist/src/', true, /\.d.ts$/);
    
    files.keys().forEach((key: string) => {
      // We add every .d.ts file to Monaco
      Monaco.languages.typescript.typescriptDefaults.addExtraLib(
        files(key).default,
        'file:///node_modules/my-module-name/' + key.substr(2)
      );
    });
    

    require.context tell Webpack to bundle all file from that path that matches the regex. Here I get all the .d.ts files containing the type description of my module.

    !!raw-loader! tell to load the file without trying to execute it. It would crash the browser. You need to install the raw-loader module for Webpack.

    Then you can have code like this in your embedded Monaco instance and have the typing Intellisense working like it would in vs-code.

    import {
      function
    } from '@my-module-name';
    
    export default async function run(value: string) {
      alert('Code ran. Value passed is ' + value);
    };
    

    Back in your app code, you can then get the code from Monaco and eval the code. In my case when the user press a 'run' button in a react app.

    const model = editor.current.getModel();
    
    if (model && sdk.current && editorType === 'text') {
      // Be cool if Monaco worker could transpile the file to js. Don't work for some reason.
      // Monaco.languages.typescript.getTypeScriptWorker()
      //   .then(function(worker) {
      //     worker(model.uri)
      //       .then(function(client) {
      //         client.getEmitOutput(model.uri.toString()).then((output: any) => {
      //           console.log(output);
      //         });
      //       });
      //   });
    
      // @ts-ignore
      const js = window.ts.transpile(model.getValue());
      const setup = `const exports = { default: null };`;
      const final = setup + ' ' + js;
    
      try {
        const runMethod = eval(final);
        const newState = await runMethod('My value');
      } catch (error) {
        console.error(error);
        alert(error);
      }
    }
    

    In theory, you should be able to ask the Monaco typescript worker to transpile the file for you but I couldn't manage to make it work.

    So I loaded the typescriptServices in my react index.html file with this line for now.

    <script src="https://unpkg.com/typescript@latest/lib/typescriptServices.js"></script>