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:
- Reading and Writing app config => ElectronStore / Local Storage
- 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
.keys(appConfigSchema)
.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: [
alias({
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:
- Dynamic Modules: https://medium.com/@leonardobrunolima/javascript-tips-dynamically-importing-es-modules-with-import-f0093dbba8e1
- Multiple packages (with individual dependencies) managed with… lets say Lerna…
If you have any feedback or inspiration, let me know!
Top comments (0)