smotaal
3/6/2018 - 5:24 PM

Loading ES Modules in Electron 2.0.0 using Protocol

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;