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.
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.
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 </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
</span>
${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.
Related
I'm currently using Snowpack to build/prepare an application. I'm trying to import a JavaScript library I installed as part of the dependencies (block) in the package.json file, but somehow Snowpack is not picking up the library.
This is (an excerpt with the relevant content of) the package.json file:
{
"private": true,
"type": "module",
"scripts": {
"build": "snowpack build",
"preview": "snowpack dev"
},
"dependencies": {
"impress.js": "1.1.0"
},
"devDependencies": {
"snowpack": "3.3.7"
},
"engines": {
"node": "14.x"
}
}
The snowpack.config.js only contains these lines:
/** #type {import("snowpack").SnowpackUserConfig } */
export default {
devOptions: {
open: "none",
},
mount: {
src: {
url: "/",
},
},
};
I was expecting Snowpack to bundle the impress.js library with this HTML file:
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<script src="node_modules/impress.js/js/impress.min.js"></script>
<script src="scripts/global.js" type="module"></script>
</body>
</html>
Is there a way to configure Snowpack for such things?
If you simply want to convert the Impress module to ESM have you considered trying esinstall?
i.e.
convert.mjs:
import {install} from 'esinstall';
await install(['impress.js'], {});
node convert.mjs
and then in your HTML file:
<script src="web_modules/impress.js"></script>
In case somebody come across something similar to this, the way I found to bundle Impress.js (and probably any other library that has/hasn't been modularized) is to just import it in the JavaScript file (notice the type="module" in the HTML script tag):
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<script src="scripts/global.js" type="module"></script>
</body>
</html>
import "impress.js";
impress(); // ...bootstraps/initializes Impress.js
I am learning typescript.
Faced the problem of connecting the axios library.
File main.ts
/// <reference path="../node_modules/axios/index.d.ts" />
//import axios from 'axios';
export function ok_lets_go()
{
axios.get('https://ya.ru')
.then((response) => {
console.log(response.data);
console.log(response.status);
});
}
If I uncomment the import line in the code
//import axios from 'axios';
Then everything compiles fine with no errors. But then this very import line gets into the Js code, and the browser swears at it (Edge based on Chromium).
If I use the line
/// <reference path="../node_modules/axios/index.d.ts" />
then compilation errors occur
error TS2304: Cannot find name 'axios'.
error TS7006: Parameter 'response' implicitly has an 'any' type.
File axios/index.d.ts exists, try to set the name wrong - there is an error message that the file is not found.
File index.html
<!DOCTYPE html>
<meta charset="utf-8">
<html>
<head>
<title>eXtodo</title>
<meta name="google" content="notranslate">
<meta http-equiv="Content-Type" content="text/html; Charset=utf-8">
<script type="text/javascript" src="./build/axios.js"></script>
</head>
<body>
<div id='app'></div>
<script type="module">
import {ok_lets_go} from './build/main.js';
ok_lets_go();
</script>
</body>
</html>
File tsconfig.json
{
"compilerOptions": {
"target": "ES6",
"module": "es6",
"sourceMap": true,
"outDir": "./build",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"]
},
"compileOnSave": true,
"include": ["./src"],
}
This question already has answers here:
Unable to use Node.js APIs in renderer process
(2 answers)
Closed 1 year ago.
I'm new to Electron, and I've really been struggling with getting it to work. I'm experiencing behavior I cannot explain, so here's a sum:
I cannot get the communication between Electron and the html to work
"Uncaught ReferenceError: require is not defined" inside the website, even though I have nodeIntegration:true
File Tree:
./
index.html
index.js
package-lock.json
package.json
node_modules/
index.js:
const electron = require("electron");
const Ffmpeg = require("fluent-ffmpeg");
const CmdExec = require('child_process');
const {
app,
BrowserWindow,
ipcMain
} = electron;
function createWindow() {
//If I put the main window ini into here, and then call app.on("ready", createWindow()); app says
//"Cant create window before ready", even though I just moved the funcion from inside ready to here..
}
app.on('ready', () => {
mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true
}
});
mainWindow.loadURL(`${__dirname}/index.html`);
});
ipcMain.on("video:submit", (event, path) =>{
CmdExec.exec("echo hello", (value)=>{console.log(value)});
});
html:
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="">
</head>
<body>
<h1>WELCOME!</h1>
<script src="" async defer></script>
<form action="">
<div>
<br>
<label for=""></label>
<input type="file" accept="video/*" name="" id="">
</div>
<button type="submit">get info</button>
</form>
<script>
const electron = require("electron");
electron.send('perform-action', args);
document.querySelector("form").addEventListener("submit", (event) => {
event.preventDefault();
const {path} = document.querySelector("input").files[0];
window.api.send("video:submit", path);
});
//Tried multiple methos Ive found on stackoverflow,, dont think I implemented them right
//though
</script>
</body>
</html>
package.json:
{
"name": "media_encoder",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"electron": "electron ."
},
"author": "",
"license": "ISC",
"dependencies": {
"electron": "^12.0.0"
}
}
Electron 12 is now defaulting contextIsolation to true, which disables Node (here are the release notes; and here's the PR).
Here's a discussion of this change. nodeIntegration for what it's worth is going to be removed in a future Electron version.
The easiest way to fix this is to simply disable context isolation:
mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
That being said, you might want to consider keeping contextIsolation enabled for security reasons. See this document explaining why this feature bolsters the security of your application.
I'm trying to expose a function to a webpage, that can be called externally, or on window.load, or at anypoint.
DoThing.ts
import * as lib from "libs";
export default function DoAThingFunc():void{
console.log('Do a thing)'
}
This is then imported thing
ExposeDoThing.js
import DoAThingFunc from './DoThing'
window.exposeFunc = DoAThing
window.exposeFunc():
webpack 4 bundle
entry: {
main: './src/MainEntry.tsx',
popupcallback: './src/ExposeDoThing.js'
},
output: {
path: path.join(__dirname, outputDir + "/js"),
filename: '[name].js',
publicPath: "/js/",
library:'pclGlobal'
},
MyPage.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pop up</title>
</head>
<body>
<script src="/js/vendor-main.js" type="text/javascript"></script>
<script src="/js/popupcallback.js" type="text/javascript"></script>
</body>
</html>
Nothing is being called, pclGlobal is undefined even though:
var pclGlobal=(window.webpackJsonppclGlobal=window.webpackJsonppclGloba...
is present in the output. And nothing is being called from the functions.
I just want the function DoAThingFunc() to fire when the script has loaded, what am I missing?
I think you need to expose your bundle as a library. check this link about output section in webpack config, and check the right way to do it.
Maybe a config like this: (pay attention to libraryExport attr)
module.exports = {
entry: './index.js',
output: {
path: './lib',
filename: 'yourlib.js',
libraryTarget: 'umd',
library: 'YourLibraryName',
umdNamedDefine: true,
libraryExport: 'default'
}
};
I noticed the vendor-main.js in my quoyted html example, except we don't have one in the weback entry and yet there is file output...
It looks like we used to have a vendors bundle, and then stopped but left the following the webpack.config.js
runtimeChunk: {
name: entrypoint => `vendor-${entrypoint.name}`
}
This has a weird effect. If your entry is not called main, then it wouldn't any execute anyexport default functions.
When I try to run the application in the browser I get in the debug console window the following message:
Module name "Person" has not been loaded yet for context: _. Use require([])
Of course if merge the content of the .ts files all is working perfectly.
I create the Person.ts file:
export interface IPerson {
firstName: string;
lastName: string;
}
export class Person implements IPerson {
private _middleName: string;
public set middleName(value: string) {
if (value.length <= 0)
throw "Must supply person name";
this._middleName = value;
}
public get middleName(): string {
return this._middleName;
}
constructor(public firstName: string, public lastName: string) { };
Validate() {
alert('test');
}
}
and the app.ts file:
import {Person} from "./Person"
class Employee extends Person {
Validate() {
alert('new test inside Employee');
}
}
let p1 = new Person("Shahar", "Eldad");
p1.Validate();
try {
p1.middleName = "";
}
catch (err) {
alert(err);
}
let p2 = new Employee("Shahar", "Eldad");
p2.Validate();
document.body.innerHTML = p1.firstName + " " + p2.lastName;
and last my index.html file:
<!DOCTYPE html>
<html>
<head>
<meta charset="windows-1255">
<title>Insert title here</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.2/require.min.js" data-main="app.js"></script>
</head>
<body>
</body>
</html>
and last my tsconfig.json file:
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"target": "es6"
},
"files": [
"app.ts",
"Person.ts"
]
}
I tried at first to transpile with target es5 and then I moved to es6 (and last moved to the tsconfig.json file)
Update
I did like #Louis and it seems to work - copied all files from my question and only edited tsconfig.json to hold amd and es5. I couldn`t see any difference between the before and after the copy. Weird.
Using "module": "commonjs" when you want the output of the TypeScript compiler to be loaded by RequireJS, is definitely wrong. You should use "module": "amd". (You may need to change change your target back to es5 too.)
The error you get is because with "module": "commonjs", the compiler will output code similar to this for your import {Person} from "./Person":
var _Person = require("./Person");
var Person = _Person.Person;
The call to require will cause RequireJS to execute but that will fail because RequireJS does not support such code directly. The code above would work if it is in a define like this:
define(function (require) {
var _Person = require("./Person");
var Person = _Person.Person;
// Rest of the module.
});
When you use "module": "amd", the compiler produces code similar to this snippet and it works.