Reputation: 67664
I followed the Electron typescript quick-start code structure. Basically I cloned the repo. It worked fine until I wanted to split my code in multiple .ts files, and import them in the renderer script.
Then I get the Uncaught ReferenceError: exports is not defined
. Because of this line on top of the renderer.ts:
import { stuff } from "./otherfile.ts";
After digging for more info, it seems that the reason is the "module": "commonjs"
from tsconfig... But if I change that to esnext
then Electron will not load the preload script anymore!
Has anyone actually managed to get Electron and typescript fully working? I mean, with being able to use import across multiple files and stuff like that?
file structure:
/dist
/dist/main.js
/dist/preload.js
/dist/renderer.js
/dist/stuff.js
/src
/src/main.ts
/src/preload.ts
/src/renderer.ts
/src/stuff.ts
/index.html
/src/main.ts:
import { ipcMain } from "electron"; // imports work fine in main
...
ipcMain.on(...
/src/preload.ts:
import { contextBridge, ipcRenderer} from "electron"; // imports work fine in preload
contextBridge.exposeInMainWorld("my-api", { ....
/src/renderer.ts
import stuff from "./stuff.ts"; // import fails in renderer (exports is not defined error)
/src/stuff.ts
const stuff = { ... };
export default stuff;
/index.html
<html>
...
<script src="./dist/renderer.js"></script>
</html>
ts.config:
If I manually add var exports = {}
in renderer.js
after ts compiles the file, then I get a different error "require is not defined"
Upvotes: 10
Views: 4225
Reputation: 13138
Sorry, it's not possible the way that boilerplate is configured.
To use import
/require
inside of the renderer
process, you must either lower the default security settings in the latest versions of Electron OR use a transpiler so that the final code the renderer
executes will not include import
/require
.
And in the electron-quick-start-typescript
project you'll find this file that confirms that as the issue.
// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// No Node.js APIs are available in this process unless
// nodeIntegration is set to true in webPreferences.
// Use preload.js to selectively enable features
// needed in the renderer process.
Node.js APIs include import
and require
.
Furthermore, when you do access require
, if sandbox is enabled then you're actually getting a polyfilled version, not the underlying require
- see these notes about preload script sandboxing
Your only other option short of changing boilerplates is to access anything you need via the preload script like window.api.yourFunction
instead.
You may wish to use a boilerplate which includes transpilation via Webpack to enable you to use import
inside of your renderer process.
I believe both of these boilerplates accommodate it (and I can probably advise you somewhat if you get stuck with one of them)
PS - If you're writing code intended for distribution to others, please do not enable nodeIntegration
as it breaks the security model of Electron!
PPS - You can also try "module": "ES2020"
, which is what we use - but it will not fix your issue of not being able to import
/require
within the renderer
process.
Upvotes: 5
Reputation: 1071
I believe your error is caused by confusing CommonJS (CJS) and ES modules (ESM). It sounds like you are using CJS exports with ESM imports which is not compatible. The next section is a working Election example and after that is a comparison of CJS and ESM exports (export) and require (import).
Your OP did not include any code so this is my best guess at what the issue was and provides a working solution. Start by setting up the code:
# Clone this repository
git clone https://github.com/electron/electron-quick-start-typescript
# Go into the repository
cd electron-quick-start-typescript
# Install dependencies
npm install
Next create the following file:
// src/otherfile.ts
export const stuff = {
fullOf: 'stuff'
}
export function shareLocation(loc: string): void {
console.log(`I was imported by: ${loc}`);
}
Now open the src/main.ts
file and make the following changes:
// Add import to top of file.
import {stuff, shareLocation} from "./otherfile"
// Add this code to the end of the createWindow() function.
shareLocation('main.ts (main.js)');
Now if you run this example with npm start
you will see I was imported by: main.ts (main.js)
in your terminal.
If you tried to do this with the src/preload.ts
file you will get an error because of sandboxing. To see this, make the following change in src/preload.ts
:
// Add to first line.
import {stuff, shareLocation} from "./otherfile";
// Add after the for loop inside the DOMContentLoaded function.
shareLocation('preload.ts (preload.js)');
Now if run npm start
you will get an error in the electron window (web browser) console. This is because of important security settings within Electron. Make the following change to your src/main.ts
file:
// Modify the webPreferences of the new BrowserWindow.
const mainWindow = new BrowserWindow({
height: 600,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
sandbox: false // <=== ADD THIS LINE
},
width: 800,
});
Your import will work as intended now and you should see I was imported by: preload.ts (preload.js)
in the electron window (web browser) console. Keep in mind this code is insecure! You should use IPC instead of disabling the sandbox.
If you are still getting an error I believe it is because your confusing CommonJS (CJS) and ES modules (ESM). It sounds like you are using CJS exports with ESM imports. See the next section that demos the difference.
CommonJS was the original method for exporting and requiring (importing) modules in Node. It was a standard introduced by Mozilla engineer Kevin Dangoor in 2009. You would write some code in one file like so:
// example.js
function hello() {
console.log('World!');
}
module.exports = hello;
And then require (import) the code into another module/file like so:
// main.js
const hello = require('./example.js');
hello();
Just like ES modules, which is a newer standard, "you can export functions
, var
, let
, const
, and classes
" (MDN Docs). Here is an bigger example that exports two functions using a single export object
:
// example.js
function hello() {
console.log('World!');
}
function foo() {
console.log('Bar!');
}
module.exports = {
hello,
foo
};
We could even change the exported object
to refer to our functions by a different name. We could change the export code to this for example:
module.exports = {
H: hello,
F: foo
};
We would then require (import) this code like so:
// main.js
const {hello, foo} = require('./example.js');
hello();
foo();
// OR with the other export example
const {H, F} = require('./example.js');
H();
F();
This is the modern approach to including JavaScript code from one file/module into another, and is designed to work in modern browsers. If you were manually writing JavaScript modules for the web you would have to add the type="module"
attribute to the script tag that loads your module.
In your setup, and many others, you have bundlers (webpack for example) of some kind that handle this for you by compiling/transpiling your code for you. This topic is out of scope to this question though, so lets look at an example.
Using ESM our simple example now becomes:
// example.js
function hello() {
console.log('World!');
}
export default hello;
Notice how our export statement has changed. To keep things simple I provide a default
export when exporting a single item. We can also inline the export:
// example.js
export default function hello() {
console.log('World!');
}
And then we import the code into another module/file like so:
// main.js
import hello from "example";
hello();
If you do not want to use default
in your exported module you will have to use a different syntax for importing your code. As Darryl Noakes mentions, that would be Named Exports. Since this is a TypeScript project this includes changing your TypeScript config. This SO answer covers what to do if you want to go this route with this project.
If you export multiple items you do not have to use the default
statement:
// example.js
function hello() {
console.log('World!');
}
function foo() {
console.log('Bar!');
}
export {
hello,
foo
}
And you import the code in a similar fashion to object destructing:
// main.js
import {hello, foo} from "example";
hello();
foo();
For additional help with import
this MDN Doc is a great start as well as this Free Code Camp Article. JavaScript Tutorial has a great tutorial on what object destructing is. This SO Q&A goes more into module imports including why they are not actually using destructing. Thanks again to Darryl Noakes for pointing that out!
Upvotes: 2
Reputation: 29301
Electron doesn't fully support ECMAScript modules yet, but this should not impact your Typescript code, which can be very modern.
OPTION 1 (LOOSE FILES)
Do this in your index.html to bootstrap your renderer process's Typescript code, as in this initial sample of mine. This will get you up and running quickly, though it uses node integration, which should be disabled before you release to production:
<body>
<div id='root' class='container'></div>
<script type='text/javascript'>
require('./built/renderer');
</script>
</body>
OPTION 2 (BUNDLED)
To get rid of the require statement, and operate closer to an SPA, in line with Electron security recommendations, you can use webpack bundling and reference built Typescript as follows, as in this updated sample of mine:
<body>
<div id='root' class='container'></div>
<script type='module' src='vendor.bundle.js'></script>
<script type='module' src='app.bundle.js'></script>
</body>
SETTINGS
I specify type=commonjs
in package.json and module=commonjs
in the tsconfig.json file, whereas in non-Electron projects I use ECMAScript module settings instead, to build slightly better output.
The art of it of course is to produce a modern code setup that you can grow according to your own preferences, rather than being limited to what starter projects do.
Upvotes: 1