Aurelia + Electron + Webpack


#1

I’ve finally managed to put together the most basic example of building an Electron app with Aurelia, bundling with Webpack and making use of native modules. The app uses the usb-detection library to list connected USB devices and updates the list when changes are detected.

The key bits are:

  • Builds and bundles both main and renderer processes from TypeScript
  • Supports hot-module-reload for the renderer process
  • Bundles all JavaScript and excludes node_modules from packaged app for smallest app size
  • Native modules are built and included

To run the app:

yarn && yarn build && yarn run

To build a distributable for the current platform:

yarn && yarn build && yarn release

I plan on adding some example CircleCI/AppVeyor configurations that will automatically build distributables for Windows/macOS/Linux.

Next on the list of things to try is a multi-window app with state synchronized between processes using aurelia-store.


#2

Do you plan to keep the store in the main or in each renderer and having them synced? Really looking forward to your result


#3

I think the store would be based and persisted from the main process as it sticks around as you open and close renderer windows. It would then be synced to every renderer via IPC. The actions would be registered in whichever process you’d want them to be executed. So the window management would be executed from the main process but could be triggered from any of the windows. The window states would be kept in the store and each window would pluck its own state from the global state. If you included things like window position and dimensions in this state, it will all get saved.

Like web apps, Electron apps mostly display dialogs within the application window, but I think it would be great to get the dialog renderer hosted in an entirely separate window process. It would take some creative bundling to get it working.

A synchronised state store can also help with security.

Electron apps have traditionally run with full node support in the renderer processes. This makes it very easy to use node APIs and modules. For example, in the above app, we can list the connected USB devices like this:

import * as usbDetect from 'usb-detection';

export class App {
  public devices: usbDetect.Device[] = [];

  public async attached() {
    this.devices = await usbDetect.find();

    usbDetect.on('change', async device => {
      this.devices = await usbDetect.find();
    });

    usbDetect.startMonitoring();
  }
}

However, this can have security implications.

Imagine we create an app for editing markdown documents and an XSS flaw is found in the markdown preview library we’re using. A malicious markdown file might be able to inject scripts into the renderer HTML and would then have access to the full node APIs. Loading remote content can be mitigated with Content Security Policy, but this isn’t going to protect you from local content which you need to read as part of the apps features.

The recommendation now is to disable node integration in renderers and only give them access to required node APIs via IPC to the main process.

Using aurelia-store could simplify this IPC and then if a renderer process becomes compromised, its attack vectors are limited to the actions you’ve pre-defined. As long as you don’t create actions that give blanket file access, it should be very secure.


#4

I guess the IPC part could be handled with a simple after middleware forwarding the necessary info to the renderers. Definitely interesting. Would be cool to simulate a multi panel app where you can float panels


#5

Yes, electron-redux does it with this middleware.

About halfway through this article, someone from the Slack Electron team talks about how they use electron-redux to sync state between all their Electron processes. The whole app is built around that and redux-observable.


#6

I created a branch with a basic example using aurelia-store to sync state between main and renderer process. The UI now has a refresh button that triggers USB device enumeration in the main process.

ElectronStore extends Store to override dispatch(). If the action is not registered in the current process, it forwards it via IPC to the other process. Its still missing some logic here to avoid an infinite loop if the action hasn’t been registered in any process. It uses some basic middleware to forward the updated state to the other process.

The main issue with this solution is that IPC is asynchronous, so if actions are triggered simultaneously in both processes, there is the possibility that state changes get lost/overwritten. This could be mitigated by only allowing registration of actions in the main process.


#7

Thanks for doing this. This seems to be working fine for me, except I get an error where GitHub is complaining about my access token during the release. The release installs just fine though. Any thoughts. I never have issues with GitHub.

GitHub Personal Access Token is not set, neither programmatically, nor using env "GH_TOKEN"

This is actually solved by add --publish=never to the release


#8

I’ve looked through the sample and must admit I do really like it.
It not only shows how to do proper IPC communication to avoid leaving the sandbox in Electron, but also serves as a great example that Aurelia-Store is essentially nothing else than a simple class which can easily be extended to provide your own means.

Do you think that the Store / IPC part could be extracted into a kind of own plugin on top of the Aurelia Store plugin? I feel this could get good traction and additional features if easily consumable.
I’m not sure its worth to put that directly into the aurelia plugin itself as it is a pretty specific use-case and shouldn’t pollute the plugins overall size.


#9

@elitemike electron-builder uses the fact that the npm script is named release as a hint to do a full package + publish. You could also stop this be renaming it to something like package. We do all our building on CI and because this error doesn’t actually stop packaging, we keep it as a reminder to whoever is ignoring the normal release workflow! When GH_TOKEN is set on CI, releases are automatically pushed to repository in package.json even if its private.

@zewa666 I was hoping to do it with only middleware but in the end extending made more sense. I suppose registerMiddleware could also be overridden to ensure that the built in middleware get removed and re-added last?

The front end is not fully isolated yet as the Electron APIs are still enabled. To fully disable them requires the use of a preload script which loads first in the window and exposes the required IPC to the isolated page context later. Ideally, the library could configure this but Webpack complicates things as it requires another entry point.


#10

I’m sure it could be rewired. But since you extend it you can do pretty much whatever you like :slight_smile:

Yep Webpack pretty much seems to become more and more the source of all evil in JS land :frowning:


#11

That makes sense. Since i like the name release, I’ll probably stick with my workaround. I won’t be building this for any github repo but I don’t want to deal with the error in my build pipeline. My team is doing our best to eliminate any warnings/errors in our normal processes. One project I work on has over 1k warnings in a c# project because the original team didn’t ever look at the warnings tab :confused: and now it’s a job just to clean that up.

I’ll probably have some questions for you. Your example is simpler than another I started to follow so I’m curious about the differences. Sadly or happily, not sure yet, it’s my kids field trip day and I’m off to join in that fun so this has to wait.


#12

This is me being webpack stupid, but I cannot get css or scss to work. I’ve tried numerous web pack configs. I settled on the css loader https://webpack.js.org/loaders/css-loader/ All compiles, but the css won’t load. Any suggestions. They are being added to the view like this. I removed the SASS/SCSS out of this.

<require from="@progress/kendo-ui/css/web/kendo.common-material.min.css"></require>
<require from="@progress/kendo-ui/css/web/kendo.material.min.css"></require>
<require from="@progress/kendo-ui/css/web/kendo.material.mobile.min.css"></require>

My webpack.common

import { AureliaPlugin } from 'aurelia-webpack-plugin';
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
import * as HtmlWebpackPlugin from 'html-webpack-plugin';
import * as path from 'path';
import * as webpack from 'webpack';
import * as merge from 'webpack-merge';
import * as Stylish from 'webpack-stylish';

export const outDir = 'dist';


const commonConfig: webpack.Configuration = {
  stats: 'none',
  output: {
    path: path.resolve(__dirname, '..', outDir),
    filename: '[name].js',
    libraryTarget: 'commonjs2'
  },

  resolve: {
    extensions: ['.ts', '.js'],
    modules: ['src', 'node_modules'].map(x => path.resolve(x))
  },

  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
        loader: 'url-loader',
        options: {
          limit: 10000
        }
      },
      { test: /\.ts$/i, use: 'awesome-typescript-loader' },
      { test: /\.html$/i, use: 'html-loader' },
      {
        // Includes .node binaries in the output
        test: /\.node$/,
        loader: 'awesome-node-loader'
      },
      {
        // Ensures our output sourcemap includes sourcemaps from dependencies
        test: /\.js$/,
        use: 'source-map-loader',
        enforce: 'pre',
        exclude: [/reflect-metadata/]
      }
    ]
  },

  plugins: [
    new Stylish(),
    // The 'bindings' library finds native binaries dynamically which is
    // incompatible with webpack. This replaces 'bindings' with a file
    // which has static paths
    new webpack.NormalModuleReplacementPlugin(/^bindings$/, require.resolve('./bindings')),
    new webpack.NoEmitOnErrorsPlugin(),
  ],

  node: {
    __dirname: false,
    __filename: false
  }
};

export const renderer = merge({}, commonConfig, {
  name: 'renderer',
  entry: { renderer: path.resolve(__dirname, '../src/renderer') },
  target: 'electron-renderer',

  optimization: {
    // Aurelia doesn't like this enabled
    concatenateModules: false
  },

  plugins: [
    new AureliaPlugin({ aureliaApp: undefined }),
    new HtmlWebpackPlugin({
      title: require('../package.json').productName
    })
  ]
});

export const main = merge({}, commonConfig, {
  name: 'main',
  entry: { main: path.resolve(__dirname, '../src/main') },
  target: 'electron-main',
  // Ensure the package.json ends up in the output directory so Electron can be
  // run straight on the output
  plugins: [new CopyWebpackPlugin(['package.json'])]
});


#14

I never tried to get CSS working on my basic example so maybe that would be a good start!

Update
@elitemike sass basics now working .