Skip to content

Instantly share code, notes, and snippets.

@SMotaal
Last active January 13, 2023 20:07
Show Gist options
  • Save SMotaal/f1e6dbb5c0420bfd585874bd29f11c43 to your computer and use it in GitHub Desktop.
Save SMotaal/f1e6dbb5c0420bfd585874bd29f11c43 to your computer and use it in GitHub Desktop.
Loading ES Modules in Electron 2.0.0 using Protocol

Now that Electron has hit Chrome's 60s we can have proper module support. But of course, the standard (for very good reasons) prevents loading modules from file: and it makes sense for both Electron and NW.js to adhere to the statusquo.

So if you were really excited and this bums you, don't worry, you are in for awesome things.

The future Electron eco-system offers two options for native ES modules:

  1. Custom Electron protocol via Chromium's module loading subsystem

  2. Custom NodeJS loaders via Node's module loading subsystem recommended

Both methods can coexist. In fact, so far from tests, it seems that at least for hybrid applications, using both methods together will be the more suitable path.

This focuses on the first.

Revision: Draft 2

const { mainModule } = process, { error } = console;
function createProtocol(scheme, base, normalize = true) {
const mimeTypeFor = require('./mime-types'),
{ app, protocol } = require('electron'),
{ URL } = require('url'),
{ readFileSync: read } = require('fs'),
{ _resolveFilename: resolve } = require('module');
// Should only be called after app:ready fires
if (!app.isReady())
return app.on('ready', () => createProtocol(...arguments));
// Normalize standard URLs to match file protocol format
normalize = !normalize
? url => new URL(url).pathname
: url => new URL(
url.replace(/^.*?:[/]*/, `file:///`) // `${scheme}://./`
).pathname.replace(/[/]$/, '');
protocol.registerBufferProtocol(
scheme,
(request, respond) => {
let pathname, filename, data, mimeType;
try {
// Get normalized pathname from url
pathname = normalize(request.url);
// Resolve absolute filepath relative to mainModule
filename = resolve(`.${pathname}`, mainModule);
// Read contents into a buffer
data = read(filename);
// Resolve mimeType from extension
mimeType = mimeTypeFor(filename);
// Respond with mimeType & data
respond({ mimeType, data });
} catch (exception) {
error(exception, { request, pathname, filename, data, mimeType });
}
},
(exception) =>
exception && error(`Failed to register ${scheme} protocol`, exception)
);
}
module.exports = createProtocol;
<!DOCTYPE html>
<html>
<head>
<!-- Needed if not loading page from app://./index.html -->
<base href="app://./" />
<script type="module" src="app:test-module.mjs"></script>
</head>
<body>
Check the console!
</body>
</html>
const { app, protocol } = require('electron');
// Base path used to resolve modules
const base = app.getAppPath();
// Protocol will be "app://./…"
const scheme = 'app';
{ /* Protocol */
// Registering must be done before app::ready fires
// (Optional) Technically not a standard scheme but works as needed
protocol.registerStandardSchemes([scheme], { secure: true });
// Create protocol
require('./create-protocol')(scheme, base);
}
{ /* BrowserWindow */
let browserWindow;
const createWindow = () => {
if (browserWindow) return;
browserWindow = new BrowserWindow();
// Option A — using the custom protocol
// browserWindow.loadURL('app://./index.html');
// Option B — directly from file
browserWindow.loadFile('index.html');
}
app.isReady()
? createWindow()
: app.on('ready', createWindow);
}
const { extname } = require('path');
const mime = filename =>
mime[extname(`${filename || ''}`).toLowerCase()];
mime[''] = 'text/plain',
mime['.js'] =
mime['.ts'] =
mime['.mjs'] = 'text/javascript',
mime['.html'] =
mime['.htm'] = 'text/html',
mime['.json'] = 'application/json',
mime['.css'] = 'text/css',
mime['.svg'] = 'application/svg+xml';
module.exports = mime;
const message = 'Hello World!';
console.trace(message);
export default message;
@jarek-foksa
Copy link

Also, I had to use webFrame.registerURLSchemeAsPrivileged() in order to make window.fetch() work with app: URLs (I have updated my gist).

@Ontopic
Copy link

Ontopic commented May 4, 2018

After building Electron with the upgrade-to-chromium-66 branch I'm able to use native import on browser and renderer side.

const Loader = process.NativeModule.require('internal/process/esm_loader')

Loader.setup()

Loader.loaderPromise.then(loader => {
  loader.import('file:///x/main.mjs')
})

After that import can be used in main.mjs (or renderer.mjs if you use it as a preload script).

No prefix needed in the import specifier, even for node_modules (think import fs from 'fs' or import _ from 'lodash'). Importing Electron itself does not work yet, but that's literally the only require I need.

@Ontopic
Copy link

Ontopic commented May 4, 2018

@SMotaal Thought I would share, since your sharing so far has been really worthwhile

@bobjunga
Copy link

I am trying to figure out the best way to write a new electron app in 2023 using only ES modules. Is this still a good approach? Or are there better ways now? I do not want to use a local server to deliver esm.

Do you know if its possible at this time to enable the file: protocol to work in the renderer? I am a bit confused about why enabling nodeIntegration does not also allow the ESM loader to load from file:

BTW, my goal is to disable network fetch by default and treat security from a local perspective. If my electron app does not allow loading remote URLs, I am thinking that is would be better to allow the file: protocol to work from the renderer so that I dont have to use a custom protocol schema in my imports.

@SMotaal
Copy link
Author

SMotaal commented Jan 12, 2023

@SMotaal Thought I would share, since your sharing so far has been really worthwhile

@Ontopic been off grid, but just noticed this and it is much appreciated 👍

@SMotaal
Copy link
Author

SMotaal commented Jan 12, 2023

Do you know if its possible at this time to enable the file: protocol to work in the renderer?

@bobjunga ah, been a long time, but per my past understanding, the approach is similar to @jarek-foksa's comments from before which was through registerFileProtocol (or something else maybe) and registerSchemeAsPrivileged but following current examples.

This has to do with browser layers for security on standard schemes, which likely still applies today, but I could be mistaken.

That's as much as I am able at this point to trust would likely work, but I have not worked with electron since 2019.

Hope this helps :)

@bobjunga
Copy link

@SMotaal thanks for the quick reply. After pouring over chromium,blink, and V8 all morning, its dawning on me why I will not be able to to ES modules in electron's renderer as I would like. I will stick to using esm for almost modern syntax without actually using the builtin ES module support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment