skip to Main Content

tl;dr: (how) can I use an ES6 module that imports other modules’ exports, in a VS Code extension creating a Webview?

Situation

I’m trying to update and improve a VS Code extension first written 4 years ago. The extension creates a webview, using HTML and JavaScript modules. This is the code that used to work:

<head>
   <!-- ... -->
   <script type='module'>
   'use strict';

   import { loadFromString as loadSCXML } from '${scxmlDomJs}';
   import SCXMLEditor from '${scxmlEditorJs}';
   import NeatXML from '${neatXMLJs}';

   // …
   </script>
</head>

…where the contents of the ${…} strings were replaced with URIs generated via:

path.join(extensionContext.extensionPath, 'resources', 'scxmlDOM.js')

These days the Webview in VS Code is now locked down for security, and (as I understand it) I need to replace the inline <script> element with something like the following:

<head>
   <!-- ... -->
   <meta http-equiv="Content-Security-Policy"
         content="default-src 'none'; script-src 'nonce-${nonce}'">
   <script nonce="${nonce}" src="${mainJS}"></script>

where mainJS is a path like above, further wrapped in a call to webview.asWebviewUri(…).

The Question

If I move my module code into a separate file main.js, how can it import other modules, when the paths to those modules need to be generated?

I’ve found several working examples (including the one linked above) for how to make script in webviews work with CORS and nonces, but I cannot find a resource on how to make it work when those scripts are modules. The closest I’ve found is this question which only might be related, but which is also unanswered.

2

Answers


  1. Chosen as BEST ANSWER

    One solution that works (tested) is to use an import map to map simple names to the URI in the HTML, and modify the main.js to import by the simple names.

    Webview HTML:

    <head>
       <!-- ... -->
       <meta http-equiv="Content-Security-Policy"
             content="default-src 'none'; script-src 'nonce-${nonce}'">
       <script nonce="${nonce}" type="importmap">
          {
             "imports": {
                "scxmlDOM":    "${scxmlDOMJS}",
                "scxmlEditor": "${scxmlEditorJS}",
                "neatXML":     "${neatXMLJS}"
             }
          }
       </script>
       <script nonce="${nonce}" type="module" src="${mainJS}"></script>
    

    main.js:

    'use strict';
    
    import { loadFromString as loadSCXML } from 'scxmlDOM';
    import SCXMLEditor from 'scxmlEditor';
    import NeatXML from 'neatXML';
    
    // …
    

    I don't know if the nonce is strictly needed on the import map <script> element, but it certainly works with it present.

    Note that the ${…} URIs are not literals, but expected to be replaced with the output from the asWebviewUri() function.


  2. In my extension vscode-antlr4 I don’t use an import map. Instead I set up my project such that for the webview contents I have an own tsconfig.json file which causes tsc to produce ES2022 modules (while for the extension itself CommonJS is used).

    
    {
        "compilerOptions": {
            "declaration": true,
            "module": "ES2022",
            "target": "ES2022",
            "outDir": "../../out",
            "removeComments": true,
            "noImplicitAny": true,
            "inlineSources": true,
            "inlineSourceMap": true,
            "isolatedModules": false,
            "allowSyntheticDefaultImports": true,
            "allowUmdGlobalAccess": true, // For D3.js
            "moduleResolution": "node",
            "experimentalDecorators": true,
            "strictNullChecks": true,
            "alwaysStrict": true,
            "composite": true,
            "rootDir": "../.."
        },
        "include": [
            "./*.ts"
        ],
        "exclude": []
    }
    

    This setup allows me to import 3rd party libs, like antlr4ts and d3 from the node_modules folder. I can now import these webview scripts in my webview content code like shown for example in the railroad diagram provider.

    public generateContent(webview: Webview, uri: Uri, options: IWebviewShowOptions): string {
            const fileName = uri.fsPath;
            const baseName = basename(fileName, extname(fileName));
    
            const nonce = this.generateNonce();
            const scripts = [
                FrontendUtils.getMiscPath("railroad-diagrams.js", this.context, webview),
            ];
            const exportScriptPath = FrontendUtils.getOutPath("src/webview-scripts/GraphExport.js", this.context,
                webview);
    
            if (!this.currentRule || this.currentRuleIndex === undefined) {
                return `<!DOCTYPE html>
                    <html>
                        <head>
                            ${this.generateContentSecurityPolicy(webview, nonce)}
                        </head>
                        <body><span style="color: #808080; font-size: 16pt;">No rule selected</span></body>
                    </html>`;
            }
    
            let diagram = `<!DOCTYPE html>
                <html>
                <head>
                    <meta http-equiv="Content-type" content="text/html; charset=UTF-8"/>
                    ${this.generateContentSecurityPolicy(webview, nonce)}
                    ${this.getStyles(webview)}
                    <base href="${uri.toString(true)}">
                    <script nonce="${nonce}">
                        let graphExport;
                    </script>
                </head>
                <body>
                ${this.getScripts(nonce, scripts)}`;
    
            if (options.fullList) {
                diagram += `
                    <div class="header">
                        <span class="rrd-color"><span class="graph-initial">Ⓡ</span>rd&nbsp;&nbsp;</span>All rules
                        <span class="action-box">
                            Save to HTML<a onClick="graphExport.exportToHTML('rrd', '${baseName}');">
                                <span class="rrd-save-image" />
                            </a>
                        </span>
                    </div>
                    <div id="container">`;
    
                const symbols = this.backend.listTopLevelSymbols(fileName, false);
                for (const symbol of symbols) {
                    if (symbol.kind === SymbolKind.LexerRule
                        || symbol.kind === SymbolKind.ParserRule
                        || symbol.kind === SymbolKind.FragmentLexerToken) {
                        const script = this.backend.getRRDScript(fileName, symbol.name);
                        diagram += `<h3 class="${symbol.name}-class">${symbol.name}</h3>
                            <script nonce="${nonce}">${script}</script>`;
                    }
                }
                diagram += "</div>";
            } else {
                diagram += `
                    <div class="header">
                        <span class="rrd-color">
                            <span class="graph-initial">Ⓡ</span>ule&nbsp;&nbsp;
                        </span>
                            &nbsp;&nbsp;${this.currentRule} <span class="rule-index">(rule index: ${this.currentRuleIndex})
                        </span>
                        <span class="action-box">
                            Save to SVG
                            <a onClick="graphExport.exportToSVG('rrd', '${this.currentRule}');">
                                <span class="rrd-save-image" />
                            </a>
                        </span>
                    </div>
                    <div id="container">
                        <script nonce="${nonce}" >${this.backend.getRRDScript(fileName, this.currentRule)}</script>
                    </div>`;
            }
    
            diagram += `
                <script nonce="${nonce}" type="module">
                    import { GraphExport } from "${exportScriptPath}";
                    graphExport = new GraphExport();
                </script>
            </body></html>`;
    
            return diagram;
        }
    

    As you can see I set a base href in the code, which helps with relative imports. The entire implementation is split into two parts. One is in the tag where the graphExport variable is declared, to allow it to be used by event handling code. This variable is then initialized in the tag, where the GraphExport class is imported.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search