├── example ├── babel.config.js ├── electron-webpack.js ├── .gitignore ├── index.js └── package.json ├── src ├── Electron.ts ├── Webpack.ts └── Config.ts ├── .gitattributes ├── .eslintrc.js ├── template ├── electron │ ├── webpack.config.js │ └── main │ │ └── index.js └── electron-webpack.js ├── scripts ├── start.js └── customize.js ├── tsconfig.json ├── .gitignore ├── LICENSE ├── README.md ├── package.json └── bin └── expo-electron.js /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['expo'], 3 | }; 4 | -------------------------------------------------------------------------------- /src/Electron.ts: -------------------------------------------------------------------------------- 1 | export * from './Config'; 2 | export * from './Webpack'; 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['universe/node', 'universe/web'], 3 | rules: { 4 | 'react/jsx-fragments': 'off', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /example/electron-webpack.js: -------------------------------------------------------------------------------- 1 | const { withExpoAdapter } = require('../'); 2 | 3 | module.exports = withExpoAdapter({ 4 | projectRoot: __dirname, 5 | }); 6 | -------------------------------------------------------------------------------- /template/electron/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { withExpoWebpack } = require('@expo/electron-adapter'); 2 | 3 | module.exports = config => { 4 | return withExpoWebpack(config); 5 | }; 6 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | *.orig.* 9 | web-build/ 10 | web-report/ 11 | electron-build/ 12 | dist/ -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | const { ensureElectronConfig } = require('../'); 2 | 3 | ensureElectronConfig(process.cwd()); 4 | 5 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 1; 6 | 7 | require('electron-webpack/out/dev/dev-runner'); 8 | -------------------------------------------------------------------------------- /template/electron-webpack.js: -------------------------------------------------------------------------------- 1 | const { withExpoAdapter } = require('@expo/electron-adapter'); 2 | 3 | module.exports = withExpoAdapter({ 4 | projectRoot: __dirname, 5 | // Provide any overrides for electron-webpack: https://github.com/electron-userland/electron-webpack/blob/master/docs/en/configuration.md 6 | }); 7 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | import React from 'react'; 3 | import { StyleSheet, Text, View } from 'react-native'; 4 | 5 | const styles = StyleSheet.create({ 6 | container: { 7 | flex: 1, 8 | alignItems: 'center', 9 | justifyContent: 'center', 10 | }, 11 | }); 12 | 13 | function App() { 14 | return ( 15 | 16 | Open up App.js to start working on your app! 17 | 18 | ); 19 | } 20 | 21 | registerRootComponent(App); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "lib": ["ES2017", "dom"], 6 | "rootDir": "src", 7 | "declaration": true, 8 | "composite": true, 9 | "esModuleInterop": true, 10 | "inlineSources": true, 11 | "module": "CommonJS", 12 | "moduleResolution": "node", 13 | "noImplicitReturns": true, 14 | "resolveJsonModule": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "target": "ES2017", 18 | "skipLibCheck": true, 19 | "typeRoots": ["node_modules/@types"] 20 | }, 21 | "exclude": ["**/__mocks__/*", "**/__tests__/*"] 22 | } 23 | -------------------------------------------------------------------------------- /scripts/customize.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const fs = require('fs-extra'); 3 | 4 | const { copyTemplateToProject, ensureMinProjectSetupAsync } = require('../'); 5 | process.on('unhandledRejection', err => { 6 | throw err; 7 | }); 8 | 9 | const projectRoot = fs.realpathSync(process.cwd()); 10 | 11 | console.log(); 12 | console.log(chalk.magenta('\u203A Copying Expo Electron main process to local project...')); 13 | 14 | copyTemplateToProject(projectRoot); 15 | ensureMinProjectSetupAsync(projectRoot); 16 | 17 | console.log( 18 | chalk.magenta( 19 | '\u203A You can now edit the Electron main process in the `electron/main` folder of your project...' 20 | ) 21 | ); 22 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-example", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "ELECTRON_DISABLE_SECURITY_WARNINGS=1 electron-webpack dev", 6 | "build": "electron-webpack && electron-builder --dir -c.compression=store", 7 | "test:build": "electron-webpack && electron-builder --dir -c.compression=store -c.mac.identity=null" 8 | }, 9 | "dependencies": { 10 | "expo": "^41.0.1", 11 | "react": "16.13.1", 12 | "react-dom": "16.13.1", 13 | "react-native-web": "~0.13.7", 14 | "source-map-support": "^0.5.16" 15 | }, 16 | "devDependencies": { 17 | "electron": "5.0.6", 18 | "electron-builder": "^21.0.11", 19 | "electron-webpack": "^2.7.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | /build 58 | 59 | /*.tsbuildinfo 60 | 61 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present 650 Industries, Inc. (aka Expo) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 👋 Welcome to
@expo/electron-adapter 4 |

5 | 6 | 7 | 8 |

9 | Create Desktop apps with Expo and Electron! 10 |

11 | 12 | --- 13 | 14 | > Warning: This package is no longer maintained. 15 | 16 | This package wraps `electron-webpack` and adds support for Expo web and other universal React packages. 17 | 18 | ## Documentation 19 | 20 | To learn more about Electron usage with Expo, check out the blog post: [Making Desktop apps with Electron, React Native, and Expo][docs]. 21 | 22 | ## License 23 | 24 | The Expo source code is made available under the [MIT license](LICENSE). Some of the dependencies are licensed differently, with the BSD license, for example. 25 | 26 | 27 | 28 | --- 29 | 30 |

31 | 32 | 33 | 34 | 35 | License: MIT 36 | 37 |

38 | 39 | [docs]: https://dev.to/evanbacon/making-desktop-apps-with-electron-react-native-and-expo-5e36 40 | [expo-cli-issues]: https://github.com/expo/expo-cli/issues 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@expo/electron-adapter", 3 | "version": "0.0.55", 4 | "description": "Use Electron with Expo", 5 | "main": "build/Electron.js", 6 | "types": "build/Electron.d.ts", 7 | "scripts": { 8 | "watch": "tsc --watch", 9 | "build": "tsc", 10 | "test": "cd example && yarn && yarn test:build", 11 | "prepare": "yarn run clean && yarn build", 12 | "clean": "rimraf ./tsconfig.tsbuildinfo", 13 | "lint": "eslint ." 14 | }, 15 | "bin": { 16 | "expo-electron": "./bin/expo-electron.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/expo/expo-electron-adapter.git" 21 | }, 22 | "keywords": [ 23 | "expo", 24 | "expo-web", 25 | "json", 26 | "electron", 27 | "pwa", 28 | "react", 29 | "react-native", 30 | "react-dom", 31 | "react-native-web" 32 | ], 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/expo/expo-electron-adapter/issues" 36 | }, 37 | "homepage": "https://github.com/expo/expo-electron-adapter", 38 | "files": [ 39 | "build", 40 | "scripts", 41 | "bin", 42 | "template" 43 | ], 44 | "devDependencies": { 45 | "@expo/babel-preset-cli": "0.2.20", 46 | "@expo/webpack-config": "0.12.68", 47 | "@types/fs-extra": "^9.0.11", 48 | "eslint-config-universe": "^7.0.1", 49 | "rimraf": "^3.0.2" 50 | }, 51 | "publishConfig": { 52 | "access": "public" 53 | }, 54 | "peerDependencies": { 55 | "@expo/webpack-config": "^0.10.1", 56 | "electron": "^6.0.12" 57 | }, 58 | "dependencies": { 59 | "@expo/config": "3.3.38", 60 | "@expo/package-manager": "0.0.41", 61 | "@expo/spawn-async": "^1.5.0", 62 | "chalk": "^4.0.0", 63 | "electron-webpack": "^2.7.4", 64 | "resolve-from": "^5.0.0", 65 | "typescript": "^4.2.4" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /template/electron/main/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { app, BrowserWindow } from 'electron'; 4 | import * as path from 'path'; 5 | import { format as formatUrl } from 'url'; 6 | 7 | const isDevelopment = process.env.NODE_ENV !== 'production'; 8 | 9 | // global reference to mainWindow (necessary to prevent window from being garbage collected) 10 | let mainWindow; 11 | 12 | function createMainWindow() { 13 | const browserWindow = new BrowserWindow({ webPreferences: { nodeIntegration: true } }); 14 | 15 | if (isDevelopment) { 16 | browserWindow.webContents.openDevTools(); 17 | } 18 | 19 | if (isDevelopment) { 20 | browserWindow.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`); 21 | } else { 22 | browserWindow.loadURL( 23 | formatUrl({ 24 | pathname: path.join(__dirname, 'index.html'), 25 | protocol: 'file', 26 | slashes: true, 27 | }) 28 | ); 29 | } 30 | 31 | browserWindow.on('closed', () => { 32 | mainWindow = null; 33 | }); 34 | 35 | browserWindow.webContents.on('devtools-opened', () => { 36 | browserWindow.focus(); 37 | setImmediate(() => { 38 | browserWindow.focus(); 39 | }); 40 | }); 41 | 42 | return browserWindow; 43 | } 44 | 45 | // quit application when all windows are closed 46 | app.on('window-all-closed', () => { 47 | // on macOS it is common for applications to stay open until the user explicitly quits 48 | if (process.platform !== 'darwin') { 49 | app.quit(); 50 | } 51 | }); 52 | 53 | app.on('activate', () => { 54 | // on macOS it is common to re-create a window even after all windows have been closed 55 | if (mainWindow === null) { 56 | mainWindow = createMainWindow(); 57 | } 58 | }); 59 | 60 | // create main BrowserWindow when electron is ready 61 | app.on('ready', () => { 62 | mainWindow = createMainWindow(); 63 | }); 64 | -------------------------------------------------------------------------------- /bin/expo-electron.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Copyright (c) 2019-present, Expo 4 | * Copyright (c) 2015-present, Facebook, Inc. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | // Makes the script crash on unhandled rejections instead of silently 13 | // ignoring them. In the future, promise rejections that are not handled will 14 | // terminate the Node.js process with a non-zero exit code. 15 | process.on('unhandledRejection', err => { 16 | throw err; 17 | }); 18 | const spawnAsync = require('@expo/spawn-async'); 19 | const { realpathSync } = require('fs-extra'); 20 | 21 | const { ensureMinProjectSetupAsync } = require('../build/Config'); 22 | 23 | const args = process.argv.slice(2); 24 | 25 | const scriptIndex = args.findIndex(x => x === 'start' || x === 'customize'); 26 | const script = scriptIndex === -1 ? args[0] : args[scriptIndex]; 27 | const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : []; 28 | 29 | if (['start', 'customize'].includes(script)) { 30 | spawnAsync( 31 | 'node', 32 | nodeArgs.concat(require.resolve('../scripts/' + script)).concat(args.slice(scriptIndex + 1)), 33 | { stdio: 'inherit' } 34 | ).then(result => { 35 | if (result.signal) { 36 | if (result.signal === 'SIGKILL') { 37 | console.log( 38 | 'The build failed because the process exited too early. ' + 39 | 'This probably means the system ran out of memory or someone called ' + 40 | '`kill -9` on the process.' 41 | ); 42 | } else if (result.signal === 'SIGTERM') { 43 | console.log( 44 | 'The build failed because the process exited too early. ' + 45 | 'Someone might have called `kill` or `killall`, or the system could ' + 46 | 'be shutting down.' 47 | ); 48 | } 49 | process.exit(1); 50 | } 51 | process.exit(result.status); 52 | }); 53 | } else if (script === undefined) { 54 | const projectRoot = realpathSync(process.cwd()); 55 | 56 | ensureMinProjectSetupAsync(projectRoot); 57 | } else { 58 | console.log('Invalid command "' + script + '".'); 59 | } 60 | -------------------------------------------------------------------------------- /src/Webpack.ts: -------------------------------------------------------------------------------- 1 | import { withAlias } from '@expo/webpack-config/addons'; 2 | import { getAliases, getConfig, getModuleFileExtensions, getPaths } from '@expo/webpack-config/env'; 3 | import { createBabelLoaderFromEnvironment } from '@expo/webpack-config/loaders'; 4 | import { ExpoDefinePlugin, ExpoInterpolateHtmlPlugin } from '@expo/webpack-config/plugins'; 5 | import { 6 | getPluginsByName, 7 | getRulesByMatchingFiles, 8 | resolveEntryAsync, 9 | } from '@expo/webpack-config/utils'; 10 | import { AnyConfiguration } from '@expo/webpack-config/webpack/types'; 11 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 12 | import * as path from 'path'; 13 | 14 | // skipEntry defaults to false 15 | export function withExpoWebpack( 16 | config: AnyConfiguration, 17 | options: { projectRoot?: string; skipEntry?: boolean } = {} 18 | ) { 19 | const projectRoot = options.projectRoot || process.cwd(); 20 | 21 | // Support react-native-web aliases 22 | // @ts-ignore: webpack version mismatch 23 | config = withAlias(config, getAliases(projectRoot)); 24 | 25 | const env: any = { 26 | platform: 'electron', 27 | projectRoot, 28 | locations: getPaths(projectRoot), 29 | }; 30 | if (!config.plugins) config.plugins = []; 31 | if (!config.resolve) config.resolve = {}; 32 | 33 | env.config = getConfig(env); 34 | 35 | const [plugin] = getPluginsByName(config, 'HtmlWebpackPlugin'); 36 | if (plugin) { 37 | const { options } = plugin.plugin as any; 38 | // Replace HTML Webpack Plugin so we can interpolate it 39 | // @ts-ignore: webpack version mismatch 40 | config.plugins.splice(plugin.index, 1, new HtmlWebpackPlugin(options)); 41 | config.plugins.splice( 42 | plugin.index + 1, 43 | 0, 44 | // Add variables to the `index.html` 45 | // @ts-ignore 46 | ExpoInterpolateHtmlPlugin.fromEnv(env, HtmlWebpackPlugin) 47 | ); 48 | } 49 | 50 | // Add support for expo-constants 51 | // @ts-ignore: webpack version mismatch 52 | config.plugins.push(ExpoDefinePlugin.fromEnv(env)); 53 | 54 | // Support platform extensions 55 | config.resolve.extensions = getModuleFileExtensions('electron', 'web'); 56 | config.resolve.extensions.push('.node'); 57 | 58 | if (!options.skipEntry) { 59 | if (!env.locations.appMain) { 60 | throw new Error( 61 | `The entry point for your project couldn't be found. Please install \`expo\`, or define it in the package.json main field` 62 | ); 63 | } 64 | 65 | const electronWebpackDefaultEntryPoints = [ 66 | path.resolve(env.projectRoot, 'index'), 67 | path.resolve(env.projectRoot, 'app'), 68 | ]; 69 | const expoEntry = config.entry; 70 | config.entry = async () => { 71 | const entries = await resolveEntryAsync(expoEntry); 72 | 73 | const expoEntryPointPath = env.locations.appMain; 74 | 75 | if (entries.renderer && !entries.renderer.includes(expoEntryPointPath)) { 76 | if (!Array.isArray(entries.renderer)) { 77 | entries.renderer = [entries.renderer]; 78 | } 79 | 80 | entries.renderer = entries.renderer.filter( 81 | inputPath => 82 | !electronWebpackDefaultEntryPoints.some(possibleEntryPoint => 83 | inputPath.includes(possibleEntryPoint) 84 | ) 85 | ); 86 | 87 | entries.renderer.push(expoEntryPointPath); 88 | 89 | if (entries.renderer.length > 2) { 90 | throw new Error( 91 | `electron-adapter app entry hack doesn't work with this version of electron-webpack. The expected entry length should be 2, instead got: [${entries.renderer.join( 92 | ', ' 93 | )}]` 94 | ); 95 | } else { 96 | console.log(` Using custom entry point > ${expoEntryPointPath}`); 97 | } 98 | } 99 | return entries; 100 | }; 101 | } 102 | 103 | const babelConfig = createBabelLoaderFromEnvironment(env); 104 | 105 | // Modify externals https://github.com/electron-userland/electron-webpack/issues/81 106 | const includeFunc = babelConfig.include as (path: string) => boolean; 107 | if (config.externals) { 108 | config.externals = (config.externals as any) 109 | .map((external: any) => { 110 | if (typeof external !== 'function') { 111 | const relPath = path.join('node_modules', external); 112 | if (!includeFunc(relPath)) return external; 113 | return null; 114 | } 115 | return (ctx: any, req: any, cb: any) => { 116 | const relPath = path.join('node_modules', req); 117 | return includeFunc(relPath) ? cb() : external(ctx, req, cb); 118 | }; 119 | }) 120 | .filter(Boolean); 121 | } 122 | 123 | // Replace JS babel loaders with Expo loaders that can handle RN libraries 124 | const rules = getRulesByMatchingFiles(config, [env.locations.appMain]); 125 | 126 | for (const filename of Object.keys(rules)) { 127 | for (const loaderItem of rules[filename]) { 128 | (config.module || { rules: [] }).rules.splice(loaderItem.index, 0, babelConfig); 129 | return config; 130 | } 131 | } 132 | 133 | return config; 134 | } 135 | -------------------------------------------------------------------------------- /src/Config.ts: -------------------------------------------------------------------------------- 1 | import { createForProject } from '@expo/package-manager'; 2 | import chalk from 'chalk'; 3 | import fs from 'fs-extra'; 4 | import * as path from 'path'; 5 | import resolveFrom from 'resolve-from'; 6 | // const PACKAGE_MAIN_PROCESS_PATH = '../template/main'; 7 | // const PACKAGE_TEMPLATE_PATH = '../../webpack-config/web-default/index.html'; 8 | // const PACKAGE_RENDER_WEBPACK_CONFIG_PATH = '../template/webpack.config'; 9 | const PACKAGE_MAIN_PROCESS_PATH = '@expo/electron-adapter/template/electron/main'; 10 | const PACKAGE_TEMPLATE_PATH = '@expo/webpack-config/web-default/index.html'; 11 | const PACKAGE_RENDER_WEBPACK_CONFIG_PATH = 12 | '@expo/electron-adapter/template/electron/webpack.config'; 13 | 14 | const LOCAL_ELECTRON_PATH = './electron'; 15 | const LOCAL_MAIN_PROCESS_PATH = './electron/main'; 16 | const LOCAL_TEMPLATE_PATH = './electron/template/index.html'; 17 | const LOCAL_RENDER_WEBPACK_CONFIG_PATH = './electron/webpack.config'; 18 | 19 | function getFolder(projectRoot: string, localPath: string, packagePath: string): string { 20 | const resolvedFullPath = 21 | resolveFrom.silent(projectRoot, localPath) || resolveFrom.silent(projectRoot, packagePath); 22 | 23 | if (!resolvedFullPath) throw new Error(`Failed to resolve path for file: ${packagePath}`); 24 | return path.relative(projectRoot, path.dirname(resolvedFullPath)); 25 | } 26 | 27 | function getFile(projectRoot: string, localPath: string, packagePath: string): string { 28 | const localTemplate = path.join(projectRoot, localPath); 29 | if (fs.pathExistsSync(localTemplate)) { 30 | return localTemplate; 31 | } 32 | const resolvedFullPath = 33 | resolveFrom.silent(projectRoot, packagePath) || path.resolve(projectRoot, packagePath); 34 | return path.relative(projectRoot, resolvedFullPath); 35 | } 36 | 37 | function resolveFile(projectRoot: string, localPath: string, packagePath: string): string { 38 | const localTemplate = resolveFrom.silent(projectRoot, localPath); 39 | if (localTemplate) { 40 | return path.relative(projectRoot, localTemplate); 41 | } 42 | return path.relative(projectRoot, resolveFrom(projectRoot, packagePath)); 43 | } 44 | 45 | export function withExpoAdapter({ 46 | projectRoot, 47 | ...config 48 | }: { 49 | projectRoot?: string; 50 | [key: string]: any; 51 | }): { [key: string]: any } { 52 | projectRoot = projectRoot || fs.realpathSync(process.cwd()); 53 | 54 | const webpackConfig = resolveFile( 55 | projectRoot, 56 | LOCAL_RENDER_WEBPACK_CONFIG_PATH, 57 | PACKAGE_RENDER_WEBPACK_CONFIG_PATH 58 | ); 59 | 60 | const mainSourceDirectory = getFolder( 61 | projectRoot, 62 | LOCAL_MAIN_PROCESS_PATH, 63 | PACKAGE_MAIN_PROCESS_PATH 64 | ); 65 | 66 | return { 67 | commonSourceDirectory: './electron/common', 68 | staticSourceDirectory: './electron/static', 69 | ...config, 70 | main: { 71 | sourceDirectory: mainSourceDirectory, 72 | ...((config || {}).main || {}), 73 | }, 74 | renderer: { 75 | sourceDirectory: './', 76 | template: getFile(projectRoot, LOCAL_TEMPLATE_PATH, PACKAGE_TEMPLATE_PATH), 77 | webpackConfig, 78 | ...((config || {}).renderer || {}), 79 | }, 80 | }; 81 | } 82 | 83 | export function copyTemplateToProject(projectPath: string) { 84 | const outputPath = path.resolve(projectPath, LOCAL_ELECTRON_PATH); 85 | if (!fs.pathExistsSync(outputPath)) { 86 | fs.ensureDirSync(path.dirname(outputPath)); 87 | fs.copy(path.resolve(__dirname, '../template/electron'), outputPath); 88 | } 89 | } 90 | 91 | export function ensureElectronConfig(projectPath: string) { 92 | const outputPath = path.resolve(projectPath, 'electron-webpack.js'); 93 | if (!fs.pathExistsSync(outputPath)) { 94 | fs.copy(path.resolve(__dirname, '../template/electron-webpack.js'), outputPath); 95 | } 96 | } 97 | 98 | export async function ensureMinProjectSetupAsync(projectRoot: string): Promise { 99 | ensureElectronConfig(projectRoot); 100 | await ensureGitIgnoreAsync(projectRoot); 101 | await ensureDependenciesAreInstalledAsync(projectRoot); 102 | } 103 | 104 | const generatedTag = `@generated: @expo/electron-adapter@${ 105 | require('@expo/electron-adapter/package.json').version 106 | }`; 107 | 108 | function createBashTag(): string { 109 | return `# ${generatedTag}`; 110 | } 111 | 112 | export async function ensureGitIgnoreAsync(projectRoot: string): Promise { 113 | const destinationPath = path.resolve(projectRoot, '.gitignore'); 114 | 115 | // Ensure a default expo .gitignore exists 116 | if (!(await fs.pathExists(destinationPath))) { 117 | return; 118 | } 119 | 120 | // Ensure the .gitignore has the required fields 121 | let contents = await fs.readFile(destinationPath, 'utf8'); 122 | 123 | const tag = createBashTag(); 124 | if (contents.includes(tag)) { 125 | console.warn( 126 | chalk.yellow( 127 | `\u203A The .gitignore already appears to contain expo generated files. To rengerate the code, delete everything from "# ${generatedTag}" to "# @end @expo/electron-adapter" and try again.` 128 | ) 129 | ); 130 | return; 131 | } 132 | 133 | console.log(chalk.magenta(`\u203A Adding the generated folders to your .gitignore`)); 134 | 135 | const ignore = [ 136 | '', 137 | tag, 138 | '/.expo/*', 139 | '# Expo Web', 140 | '/web-build/*', 141 | '# electron-webpack', 142 | '/dist', 143 | '# @end @expo/electron-adapter', 144 | '', 145 | ]; 146 | 147 | contents += ignore.join('\n'); 148 | await fs.writeFile(destinationPath, contents); 149 | } 150 | 151 | function getDependencies( 152 | projectRoot: string 153 | ): { dependencies: string[]; devDependencies: string[] } { 154 | const dependencies = ['react-native-web', 'electron@^6.0.12'].filter( 155 | dependency => !resolveFrom.silent(projectRoot, dependency.split('@^').shift()!) 156 | ); 157 | const devDependencies = ['@expo/electron-adapter', '@expo/webpack-config'].filter( 158 | dependency => !resolveFrom.silent(projectRoot, dependency) 159 | ); 160 | 161 | return { dependencies, devDependencies }; 162 | } 163 | 164 | async function ensureDependenciesAreInstalledAsync(projectRoot: string): Promise { 165 | const { dependencies, devDependencies } = getDependencies(projectRoot); 166 | const all = [...dependencies, ...devDependencies]; 167 | if (!all.length) { 168 | console.log(chalk.yellow(`\u203A All of the required dependencies are installed already.`)); 169 | return; 170 | } else { 171 | console.log(chalk.magenta(`\u203A Installing the missing dependencies: ${all.join(', ')}.`)); 172 | } 173 | 174 | const packageManager = createForProject(projectRoot); 175 | 176 | if (dependencies.length) await packageManager.addAsync(...dependencies); 177 | if (devDependencies.length) await packageManager.addDevAsync(...devDependencies); 178 | } 179 | --------------------------------------------------------------------------------