JS/TS Managing alternative implementations with RollupJS

o_a_e profile image Johannes Zillmann Originally published at blog.morethan.io on ・3 min read

Short post about a trivial thing to do. I’m in the JS/Electron world. I just decided I want to package my app for Electron but for a regular Browser as well. Why ?

  • A) I can do a demo version of the app on the web!
  • B) I can use Cypress for testing it!

Will see how far this goes, but currently i’m using only two Electron/Desktop features that can be easily mimicked in an browser environment:

  1. Reading and Writing app config => ElectronStore / Local Storage
  2. Reading and Writing files => Node FS API / Local Storage

Basic Structure

Simple. Lets just focus on the app config.

  • I defined a common ‘interface’ (AppConfig)
  • One implementation wrapping ElectronStore (ElectronAppConfig)
  • A second implementation wrapping the local storage (LocalAppConfig).

Most Naive Approach

I just kept all 3 classes under /src with a factory method:

export function createAppConfig(appConfigSchema) {
  if (__electronEnv__) {
    const ElectronStore = require('electron-store'); 
    return new ElelectronAppConfig(new ElectronStore({schema:appConfigSchem}));
  } else {
    const defaults = Object
      .reduce((o, key) => ({...o, [key]: appConfigSchema[key]['default'] }),{});
    return new LocalAppConfig(window.localStorage, defaults);

Then in the rollup.config.js i’m using the plugin-replace to steer the __electronEnv__ variable:

import replace from '@rollup/plugin-replace';

const electronEnv = !!process.env.ELECTRON;

plugins: [
  replace({__electronEnv__: electronEnv}),

And finally i enrich my NPM electron tasks with then env variable in the package.json:

"electron": "ELECTRON=true run-s build pure-electron",

That’s it for the naive approach. It’s working most of the times (sometimes there is a hiccup with a require not found error, but a rebuild usually solves it).

Anyway, the purist in me, wanted a clearer structure and also the inline require statements seemed odd.

Moving to a more satisfactory approach

Have another folder next to /src, let’s called it /includes with three sub-folders:

  • api : AppConfig, …
  • electron : index.js (contain factory methods for all electron implementations), ElectronAppConfig, …
  • browser : index.js (contain factory methods for all browser implementations), LocalAppConfig, …

Now use plugin-alias to alias the index.js of the desired implementation at build time in rollup.config.js:

import alias from '@rollup/plugin-alias';
const electronEnv = !!process.env.ELECTRON;
const storagePackage = electronEnv ? 'electron' : 'browser';

plugins: [
    entries: [
      { find: 'storage', replacement: `./includes/${storagePackage}/index.js` }

And access the implementation in your main code:

import { createAppConfig } from 'storage';

const appConfig = createAppConfig(appConfigSchema);

Easy. Not too much gain here, but some clearer structure!

And now in Typescript…

Once i moved to the approach above, i thought ‘Ok, lets try typescript’. Cause that’s an obvious thing to do if you’re talking about interfaces and implementations, right ?

I failed using the exact same approach but luckily the typescript path-mapping came to rescue:

Here is the rollup.config.js part:

import typescript from '@rollup/plugin-typescript';

plugins: [
 typescript({ target: 'es6', baseUrl: './', paths: { storage: [`./includes/${storagePackage}/index.js`] } })

Imports work the same as in the previous approach!

Final Words

Not sure if i delivered on the promise of shortness, but finding the second/third approaches took me longer than expected and drove me almost nuts. Part i blame on my inexperience in the JS world, part is that the search space for such a problem seems heavily convoluted. That said, there might be a couple of alternatives worth investigating:

If you have any feedback or inspiration, let me know!


markdown guide