Slide 1

Slide 1 text

Electron: How and Why @dtanzer 1

Slide 2

Slide 2 text

Build cross-platform desktop apps with JavaScript, HTML, and CSS If you can build a website, you can build a desktop app. Electron is a framework for creating native applications with web technologies like JavaScript, HTML, and CSS. It takes care of the hard parts so you can focus on the core of your application. -- https://www.electronjs.org/

Slide 3

Slide 3 text

Today: Why Electron Marmota.app Getting Started Concepts Publishing your App This Presentation: Speakerdeck: https://speakerdeck.com/dtanzer/ marmota.app presentation (Markdown): https://marmota.app/blog/talk-electron/ @dtanzer 3

Slide 4

Slide 4 text

Why Electron @dtanzer 4

Slide 5

Slide 5 text

Advantages Advantages Advantages Advantages Advantages Windows, Linux, MacOS Auto-update is "free" Somtimes "Most economical option" @dtanzer 5

Slide 6

Slide 6 text

Downsides Downsides Downsides Downsides Downsides App Size Resource Usage Native look & Feel (possible, but can be hard) @dtanzer 6

Slide 7

Slide 7 text

For Me For Me For Me For Me For Me "Power of the web" TypeScript JavaScript Ecosystem @dtanzer 7

Slide 8

Slide 8 text

"Power of the Web" HTML and CSS Browser capabilities (animations, video, audio, ...) Auto-reload React, Vue, Angular, ... Developer tools and developer experience @dtanzer 8

Slide 9

Slide 9 text

TypeScript const v: string 5 = @dtanzer 9

Slide 10

Slide 10 text

export type ChannelToMain = { '--presentations--get-screen-sources': () => void, } export interface PresentationsApi { getScreenSources: ChannelToMain[ '--presentations--get-screen-sources'], } export const presentations: PresentationsApi = { getScreenSources: () => rendererIpc.sendAsync( '--presentations--get-screen-sources'), } @dtanzer 10

Slide 11

Slide 11 text

type HasWon, P extends Player> = { T: 'F' F: Or< And, And Or, And, And, And, And, And }[G['isEmpty']] https://github.com/dtanzer/typetactoe/blob/master/src/tictactoe.ts @dtanzer 11

Slide 12

Slide 12 text

JavaScript Ecosystem @dtanzer 12

Slide 13

Slide 13 text

marmota.app @dtanzer 13

Slide 14

Slide 14 text

Demo Time marmota.app Presentation Software @dtanzer 14

Slide 15

Slide 15 text

My First Electron Application My First Electron Application My First Electron Application My First Electron Application My First Electron Application For a hardware company Visualization and optimization @dtanzer 15

Slide 16

Slide 16 text

Getting Started @dtanzer 16

Slide 17

Slide 17 text

Electron Forge Electron Forge Electron Forge Electron Forge Electron Forge npx create-electron-app my-new-app \ --template=typescript-webpack @dtanzer 17

Slide 18

Slide 18 text

index.js index.js index.js index.js index.js Menu.setApplicationMenu(buildApplicationMenu()) app.on('ready', () => { registerProtocol() const mainWindow = createMainWindow() registerWindow('main', mainWindow) }) @dtanzer 18

Slide 19

Slide 19 text

index.html index.html index.html index.html index.html marmota.app - Simple Markdown Presentations //ugly hack for react-shadow, which accesses the "global" //variable global = globalThis
@dtanzer 19

Slide 20

Slide 20 text

And the React Code? And the React Code? And the React Code? And the React Code? And the React Code? @dtanzer 20

Slide 21

Slide 21 text

package.json [ "@electron-forge/plugin-webpack", { "renderer": { "entryPoints": [ { "html": "./src/index.html", "js": "./src/renderer.tsx", "name": "main_window", "preload": { "js": "./src/preload.ts" } } ] } }] @dtanzer 21

Slide 22

Slide 22 text

renderer.ts if(w.location.search.indexOf('mode=presentation')>=0) { ReactDOM.render(, reactRoot) } else if(w.location.search.indexOf('mode=presenter-screen')>=0) { ReactDOM.render(, reactRoot) } else if(w.location.search.indexOf('mode=export-or-print')>=0) { ReactDOM.render(, reactRoot) } else { const { documentsModel, tabsModel } = createModel() ReactDOM.render( , reactRoot) } @dtanzer 22

Slide 23

Slide 23 text

Developer Tools Developer Tools Developer Tools Developer Tools Developer Tools app.on('ready', () => { /* ... */ if(isDevelopment()) { installExtension(REACT_DEVELOPER_TOOLS) .then((name) => console.log(`Added Extension: ${name}`)) .catch((err) => console.log('An error occurred: ', err)) } }) @dtanzer 23

Slide 24

Slide 24 text

Reloading the Application Reloading the Application Reloading the Application Reloading the Application Reloading the Application Webpack: Reloads automatically Otherwise: type "rs" @dtanzer 24

Slide 25

Slide 25 text

Demo Time "my-new-app" @dtanzer 25

Slide 26

Slide 26 text

Concepts @dtanzer 26

Slide 27

Slide 27 text

Main and Renderer Processes Main and Renderer Processes Main and Renderer Processes Main and Renderer Processes Main and Renderer Processes main process renderer processes @dtanzer 27

Slide 28

Slide 28 text

Browser Windows Browser Windows Browser Windows Browser Windows Browser Windows const titleBarStyle = process.platform==='darwin'?'hidden':undefined const window = new BrowserWindow({ ...options, titleBarStyle, }) window.once('ready-to-show', () => { window.show() }) window.loadURL(`${MAIN_WINDOW_WEBPACK_ENTRY}${modeParameter}`) window.webContents.on("did-frame-finish-load", () => { if(isDevelopment()) { window.webContents.openDevTools() } }) @dtanzer 28

Slide 29

Slide 29 text

Security Security Security Security Security Expose all node APIs to renderers (insecure) -or- Expose an API @dtanzer 29

Slide 30

Slide 30 text

Expose an API: preload.js import { contextBridge, ipcRenderer, } from 'electron' import { api } from '$main/mainapi/api' import { AppApi } from '$api/app/types' import { registerRendererApiCallbacks } from './mainapi/renderer' contextBridge.exposeInMainWorld('api', api) registerRendererApiCallbacks() @dtanzer 30

Slide 31

Slide 31 text

Expose an API: api.js import { app, } from './app/renderer' import { action, } from './action/renderer' /* ... */ export const api = { app, action, /* ... */ } type Api = typeof api declare global { interface Window { api: Api } } @dtanzer 31

Slide 32

Slide 32 text

Expose an API: Usage export type ExportOrPrintWindowProperties = { /* ... */ _getPlatform?: typeof window.api.app.getPlatform, } export function ExportOrPrintWindow({ /* ... */ _getPlatform = window.api.app.getPlatform, }: ExportOrPrintWindowProperties): React.ReactElement { const macTitleBar = _getPlatform() === 'darwin'? : undefined /* ... */ } @dtanzer 32

Slide 33

Slide 33 text

Node APIs Node APIs Node APIs Node APIs Node APIs function savePresentationAs(rawPresentation: string) { const filePath = dialog.showSaveDialogSync({ title: 'Save Presentation As', }) if(filePath) { fs.writeFileSync(filePath, rawPresentation, 'utf-8') return { savedFilePath: filePath, savedBasePath: path.dirname(filePath), savedFileName: path.basename(filePath), } } return undefined } @dtanzer 33

Slide 34

Slide 34 text

Application Menu Application Menu Application Menu Application Menu Application Menu Menu.setApplicationMenu(buildApplicationMenu()) export function buildApplicationMenu(): Menu { const aboutMenuItem = new MenuItem({ click: (_, browserWindow, __) => { sendActionEventToWindow('application.about', /* ... */) }, label: 'About', }) /* ... */ helpMenu.submenu?.append(aboutMenuItem) menu.append(helpMenu) return menu } @dtanzer 34

Slide 35

Slide 35 text

IPC IPC IPC IPC IPC ipcMain IPC-Object, used in the main process ipcRenderer IPC-Object, used in the renderer processes import { ipcRenderer } from "electron"; ipcRenderer.sendSync('some event', ...args) ipcMain.on(channel, ('some event', ...args) => { console.log('recevied event') } @dtanzer 35

Slide 36

Slide 36 text

How I do IPC How I do IPC How I do IPC How I do IPC How I do IPC Two type-safe helper classes Data Types shared by main and renderer processes Other IPC-Code split into main.ts and renderer.ts @dtanzer 36

Slide 37

Slide 37 text

IPC: Shared Data Types export type ChannelToMain = { '--annotations--publish-added-annotation': /* function */, } export type ChannelToRenderer = { '--annotations--annotation-published': /* function */, } export interface AnnotationsApi { publishAddedAnnotation: ChannelToMain[ '--annotations--publish-added-annotation'], onAnnotationAdded: (callback: (annotation: SerializedAnnotation) => unknown) => void, }``` @dtanzer 37

Slide 38

Slide 38 text

IPC: renderer.ts export const annotations: AnnotationsApi = { publishAddedAnnotation: ( w: AnnotationWindowName, a: SerializedAnnotation ) => rendererIpc.sendAsync( '--annotations--publish-added-annotation', w, a), onAnnotationAdded: ( callback: (annotation: SerializedAnnotation) => unknown ) => { annotationAddedCallback = callback }, } rendererIpc.on({ '--annotations--annotation-published': ( serialized: SerializedAnnotation ) => annotationAddedCallback?.(serialized), }) @dtanzer 38

Slide 39

Slide 39 text

IPC: main.ts export function registerMainAnnotationsCallbacks() { mainIpc.on({ '--annotations--publish-added-annotation': publishAddedAnnotation, }) } function publishAddedAnnotation(/* ... */) { /* ... */ } @dtanzer 39

Slide 40

Slide 40 text

Webpack Webpack Webpack Webpack Webpack Monaco Editor Using Imports Resolve Configuration @dtanzer 40

Slide 41

Slide 41 text

resolve Configuration resolve Configuration resolve Configuration resolve Configuration resolve Configuration import { getWindow } from "$api/app/main" webpack.resolve.js module.exports = { resolve: { extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], fallback: { path: false, fs: false, }, alias: { '$api': path.resolve(__dirname, 'src', 'mainapi'), }, }, } @dtanzer 41

Slide 42

Slide 42 text

Resolve: Jest const config: Config.InitialOptions = { moduleNameMapper: { "^\\$api(.*)$": "/src/mainapi$1", "^(.*)/MonacoEditor$": "/__mocks__/reactMonaco.js", "^monaco-editor(.*)$": "/__mocks__/monacoMock.js", } }; @dtanzer 42

Slide 43

Slide 43 text

Resolve: tsconfig.json "paths": { "$api/*": ["src/mainapi/*"] } @dtanzer 43

Slide 44

Slide 44 text

Content Security Policy (CSP) Content Security Policy (CSP) Content Security Policy (CSP) Content Security Policy (CSP) Content Security Policy (CSP) index.html @dtanzer 44

Slide 45

Slide 45 text

CSP: package.json "devContentSecurityPolicy": "default-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline' assets:; img-src 'self' data: assets: presentation: design:; font-src 'self' assets:", @dtanzer 45

Slide 46

Slide 46 text

Publishing your App @dtanzer 46

Slide 47

Slide 47 text

Building / Publishing Building / Publishing Building / Publishing Building / Publishing Building / Publishing With tools / scripts from electron-forge Configured in package.json @dtanzer 47

Slide 48

Slide 48 text

Building for Windows Building for Windows Building for Windows Building for Windows Building for Windows "makers": [ { "name": "@electron-forge/maker-squirrel", "config": { "remoteReleases": "https://download.marmota.app/win32", "name": "marmota.app", "certificateFile": "build/evcodesign.pfx", "certificatePassword": "authenticode-certificate-password", "setupIcon": "./assets/images/marmota.ico" } }, ... ] @dtanzer 48

Slide 49

Slide 49 text

Certificate Password #!/bin/sh echo Please enter the Authenticode certificate password read certificate_password sed -i -e "s/\"certificatePassword\": \"authenticode-certificate-... npm run gather-licenses npm run make git checkout package.json @dtanzer 49

Slide 50

Slide 50 text

Result out \-- make \-- squirrel.windows \-- x64 +-- RELEASES +-- marmota-22.19.0 Setup.exe +-- marmota-22.17.0-full.nupkg +-- marmota-22.19.0-delta.nupkg \-- marmota-22.19.0-full.nupkg @dtanzer 50

Slide 51

Slide 51 text

Installing from EXE Download and double-click. @dtanzer 51

Slide 52

Slide 52 text

Building for Mac Building for Mac Building for Mac Building for Mac Building for Mac "packagerConfig": { "osxSign": { "identity": "[Developer ID]", "hardened-runtime": true, "entitlements": "entitlements.plist", "entitlements-inherit": "entitlements.plist", "signature-flags": "library" }, "osxNotarize": { "appleId": "[Apple ID]", "appleIdPassword": "appleid-app-password", "ascProvider": "[ASC Provider]" }, "icon": "./assets/images/marmota", "appBundleId": "app.marmota.presentations" } @dtanzer 52

Slide 53

Slide 53 text

Maker Config "makers": [ { "name": "@electron-forge/maker-dmg", "config": { "format": "ULFO", "background": "./assets/images/dmg-background.png" } }, { "name": "@electron-forge/maker-zip", "platforms": [ "darwin" ] }, ... ] @dtanzer 53

Slide 54

Slide 54 text

Result out \-- make +-- marmota-22.19.0-arm64.dmg \-- zip \-- darwin \-- arm64 \-- marmota-darwin-arm64-22.19.0.zip @dtanzer 54

Slide 55

Slide 55 text

Installing from DMG @dtanzer 55

Slide 56

Slide 56 text

Building for Linux Building for Linux Building for Linux Building for Linux Building for Linux "makers": [ { "name": "@electron-forge/maker-deb", "config": { "options": { "icon": "./assets/images/marmota.png" } } }, { "name": "@electron-forge/maker-rpm", "config": { "options": { "icon": "./assets/images/marmota.png" } } } ], @dtanzer 56

Slide 57

Slide 57 text

Installing from Package Manager sudo dnf config-manager --add-repo https://download.marmota... sudo rpm --import https://download.marmota.app/linux/rpm/... sudo dnf install marmota marmota 1. Add the repository 2. Import the public key 3. Install marmota.app 4. Run marmota.app @dtanzer 57

Slide 58

Slide 58 text

Code Signing Code Signing Code Signing Code Signing Code Signing Windows: Authenticode "EV" certificate MacOS: Developer account, app specific password Linux: Any GPG key @dtanzer 58

Slide 59

Slide 59 text

Auto-Update Auto-Update Auto-Update Auto-Update Auto-Update Windows, Mac: Application Startup Linux: Package Manager @dtanzer 59

Slide 60

Slide 60 text

Application Code Application Code Application Code Application Code Application Code export function Updater(): React.ReactElement { const [ showDownloadUpdatePopup, setShowDownloadUpdatePopup, ]=... useEffect(() => { window.api.app.checkForUpdates(() => { setShowDownloadUpdatePopup(true) }) }, []) const downloadUpdatePopup = createDownloadUpdatePopup(...) return <> {downloadUpdatePopup} } @dtanzer 60

Slide 61

Slide 61 text

function checkForUpdates(mainWindow: BrowserWindow | undefined) { try { autoUpdater.on('update-downloaded', (_: any) => { mainWindow?.webContents.send('--app--updates-available') }) let path : string = process.platform if(process.platform === 'darwin') { path += '.php?version=' ... } const url = 'https://download.marmota.app/' + path autoUpdater.setFeedURL({ url: url, }) autoUpdater.checkForUpdates() autoUpdater.on('error', (error: any) => {...}) autoUpdater.on('update-not-available', () => { ... }) } catch (e) { ... } } @dtanzer 61

Slide 62

Slide 62 text

Windows: "Repository" Windows: "Repository" Windows: "Repository" Windows: "Repository" Windows: "Repository" Upload the output of make to an HTTP server: RELEASES marmota-22.19.0 Setup.exe marmota-22.17.0-full.nupkg marmota-22.19.0-delta.nupkg marmota-22.19.0-full.nupkg @dtanzer 62

Slide 63

Slide 63 text

Mac: "Squirrel Server" Mac: "Squirrel Server" Mac: "Squirrel Server" Mac: "Squirrel Server" Mac: "Squirrel Server" Run the squirrel server from GitHup or write your own: { "name":"marmota.app 22.19.0", "notes":"", "url":"https://download.marmota.app/macOS/marmota-darwin-arm64-22.19.0.zip } @dtanzer 63

Slide 64

Slide 64 text

...From User's Perspective @dtanzer 64

Slide 65

Slide 65 text

Linux: Package Manager Linux: Package Manager Linux: Package Manager Linux: Package Manager Linux: Package Manager Create package manager repositories Upload them to an HTTP server @dtanzer 65

Slide 66

Slide 66 text

To Recap... To Recap... To Recap... To Recap... To Recap... @dtanzer 66

Slide 67

Slide 67 text

About David About David About David About David About David Trainer, Coach, Developer https://davidtanzer.net @dtanzer This Presentation: Speakerdeck: https://speakerdeck.com/dtanzer/ marmota.app (Markdown): https://marmota.app/blog/talk-electron/