├── test ├── fixtures │ ├── simple │ │ ├── src │ │ │ ├── main │ │ │ │ ├── index.js │ │ │ │ └── foo.js │ │ │ └── renderer │ │ │ │ ├── foo.html │ │ │ │ ├── style.css │ │ │ │ └── index.js │ │ └── package.json │ └── typescript │ │ ├── src │ │ └── main │ │ │ └── index.ts │ │ └── package.json ├── .babelrc ├── tsconfig.json ├── src │ ├── titleTest.ts │ ├── loaderDetectionTest.ts │ ├── test.ts │ └── helpers │ │ └── helper.ts ├── out │ └── __snapshots__ │ │ ├── titleTest.js.snap │ │ └── loaderDetectionTest.js.snap └── babel-jest.js ├── .github ├── FUNDING.yml └── issue_template.md ├── packages ├── electron-webpack │ ├── webpack.app.config.js │ ├── webpack.main.config.js │ ├── webpack.test.config.js │ ├── webpack.renderer.config.js │ ├── webpack.renderer.dll.config.js │ ├── vendor │ │ └── runnerw.exe │ ├── vue-renderer-entry.js │ ├── tsconfig.json │ ├── src │ │ ├── electron-main-hmr │ │ │ ├── webpack-env.d.ts │ │ │ ├── main-hmr.ts │ │ │ ├── HmrServer.ts │ │ │ └── HmrClient.ts │ │ ├── configurators │ │ │ ├── vue │ │ │ │ ├── vue-main-dev-entry.ts │ │ │ │ └── vue.ts │ │ │ ├── eslint.ts │ │ │ ├── dll.ts │ │ │ ├── js.ts │ │ │ └── ts.ts │ │ ├── electron-builder.ts │ │ ├── cli.ts │ │ ├── core.ts │ │ ├── util.ts │ │ ├── targets │ │ │ ├── MainTarget.ts │ │ │ ├── BaseTarget.ts │ │ │ └── RendererTarget.ts │ │ ├── config.ts │ │ ├── dev │ │ │ ├── devUtil.ts │ │ │ ├── ChildProcessManager.ts │ │ │ ├── WebpackDevServerManager.ts │ │ │ └── dev-runner.ts │ │ ├── plugins │ │ │ ├── WatchMatchPlugin.ts │ │ │ └── WebpackRemoveOldAssetsPlugin.ts │ │ └── main.ts │ ├── typings │ │ ├── crocket.d.ts │ │ ├── webpack-dev-server.d.ts │ │ └── yargs.d.ts │ ├── tsconfig-base.json │ ├── .yarnclean │ ├── package.json │ └── scheme.json ├── electron-webpack-js │ ├── readme.md │ └── package.json ├── electron-webpack-vue │ ├── readme.md │ └── package.json ├── electron-webpack-eslint │ ├── readme.md │ └── package.json └── electron-webpack-ts │ ├── readme.md │ ├── changelog.md │ └── package.json ├── netlify.toml ├── lerna.json ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── encodings.xml ├── vcs.xml ├── jsLinters │ └── tslint.xml ├── modules.xml ├── misc.xml ├── inspectionProfiles │ └── Project_Default.xml ├── rc-producer.yml ├── dictionaries │ └── develar.xml └── codeStyleSettings.xml ├── npm-publish.sh ├── babel.config.js ├── .gitignore ├── .yarnclean ├── .circleci └── config.yml ├── docs ├── en │ ├── extra.js │ ├── cli-commands.md │ ├── core-concepts.md │ ├── using-static-assets.md │ ├── extending-as-a-library.md │ ├── dependency-management.md │ ├── environment-variables.md │ ├── building.md │ ├── technical-differences-of-electron-compile.md │ ├── dll-bundle-splitting.md │ ├── project-structure.md │ ├── index.md │ ├── development.md │ ├── configuration.md │ ├── modifying-webpack-configurations.md │ └── add-ons.md └── publish.sh ├── electron-webpack.iml ├── mkdocs.yml ├── package.json └── readme.md /test/fixtures/simple/src/main/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/simple/src/main/foo.js: -------------------------------------------------------------------------------- 1 | console.log("hi") -------------------------------------------------------------------------------- /test/fixtures/simple/src/renderer/foo.html: -------------------------------------------------------------------------------- 1 |

Hi

-------------------------------------------------------------------------------- /test/fixtures/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Test" 3 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.electron.build/donate 2 | -------------------------------------------------------------------------------- /test/fixtures/typescript/src/main/index.ts: -------------------------------------------------------------------------------- 1 | class A { 2 | readonly foo = 123 3 | } -------------------------------------------------------------------------------- /packages/electron-webpack/webpack.app.config.js: -------------------------------------------------------------------------------- 1 | module.exports = env => require("./out/main").getAppConfiguration(env) 2 | -------------------------------------------------------------------------------- /packages/electron-webpack/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | module.exports = env => require("./out/main").getMainConfiguration(env) -------------------------------------------------------------------------------- /packages/electron-webpack/webpack.test.config.js: -------------------------------------------------------------------------------- 1 | module.exports = env => require("./out/main").getTestConfiguration(env) -------------------------------------------------------------------------------- /packages/electron-webpack/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = env => require("./out/main").getRendererConfiguration(env) 2 | -------------------------------------------------------------------------------- /packages/electron-webpack/webpack.renderer.dll.config.js: -------------------------------------------------------------------------------- 1 | module.exports = env => require("./out/main").getDllConfiguration(env) 2 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [Settings] 2 | ID = "905ec7c5-2006-493e-ac00-d2d81b0bcc56" 3 | 4 | [Build] 5 | Publish = "" 6 | Functions = "" 7 | -------------------------------------------------------------------------------- /packages/electron-webpack/vendor/runnerw.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/electron-userland/electron-webpack/HEAD/packages/electron-webpack/vendor/runnerw.exe -------------------------------------------------------------------------------- /test/fixtures/simple/src/renderer/style.css: -------------------------------------------------------------------------------- 1 | .f1 { 2 | background: image("static/a/foo.png") 3 | } 4 | 5 | .f2 { 6 | background: image("static/b/foo.png") 7 | } -------------------------------------------------------------------------------- /test/fixtures/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Test", 3 | "devDependencies": { 4 | "electron-webpack-ts": "*", 5 | "typescript": "*" 6 | } 7 | } -------------------------------------------------------------------------------- /packages/electron-webpack/vue-renderer-entry.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | 3 | Vue.config.productionTip = false 4 | Vue.config.devtools = process.env.NODE_ENV === "development" -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "independent", 7 | "npmClient": "yarn", 8 | "useWorkspaces": true 9 | } 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/electron-webpack-js/readme.md: -------------------------------------------------------------------------------- 1 | # electron-webpack-js 2 | 3 | JavaScript add-on for [electron-webpack](https://github.com/electron-userland/electron-webpack). Bundled, no need to install. -------------------------------------------------------------------------------- /test/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | sourceMaps: "inline", 3 | plugins: [ 4 | [ 5 | "@babel/plugin-transform-modules-commonjs", 6 | { 7 | lazy: true 8 | } 9 | ] 10 | ] 11 | } -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/electron-webpack-vue/readme.md: -------------------------------------------------------------------------------- 1 | # electron-webpack-vue 2 | 3 | Vue.js add-on for [electron-webpack](https://github.com/electron-userland/electron-webpack). 4 | 5 | ## Install 6 | 7 | Please see [Vue.js](https://webpack.electron.build/add-ons#vuejs). 8 | -------------------------------------------------------------------------------- /packages/electron-webpack-eslint/readme.md: -------------------------------------------------------------------------------- 1 | # electron-webpack-eslint 2 | 3 | ESLint add-on for [electron-webpack](https://github.com/electron-userland/electron-webpack). 4 | 5 | ### Install 6 | 7 | Please see [ESLint](https://webpack.electron.build/add-ons#eslint). 8 | -------------------------------------------------------------------------------- /packages/electron-webpack-ts/readme.md: -------------------------------------------------------------------------------- 1 | # electron-webpack-ts 2 | 3 | TypeScript add-on for [electron-webpack](https://github.com/electron-userland/electron-webpack). 4 | 5 | ## Install 6 | 7 | Please see [TypeScript](https://webpack.electron.build/add-ons#typescript). 8 | -------------------------------------------------------------------------------- /.idea/jsLinters/tslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../node_modules/ts-babel/tsconfig-base.json", 3 | "compilerOptions": { 4 | "outDir": "out", 5 | "baseUrl": "../packages", 6 | "skipLibCheck": true 7 | }, 8 | "declaration": false, 9 | "include": [ 10 | "src/**/*.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/electron-webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base.json", 3 | "compilerOptions": { 4 | "stripInternal": true, 5 | "outDir": "out", 6 | "skipLibCheck": true 7 | }, 8 | "include": [ 9 | "**/*.ts", "src/electron-builder.ts", "typings/*.d.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/simple/src/renderer/index.js: -------------------------------------------------------------------------------- 1 | import "./style.css" 2 | import html from "@/foo.html" 3 | import * as path from "path" 4 | 5 | console.log(html) 6 | 7 | const fileContents = fs.readFileSync(path.join(__static, "/foo.txt")).toString("hex") 8 | console.log(fileContents) 9 | 10 | class A { 11 | } -------------------------------------------------------------------------------- /packages/electron-webpack/src/electron-main-hmr/webpack-env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace __WebpackModuleApi { 2 | interface Hot { 3 | check(isAutoApply?: boolean): Promise | null> 4 | 5 | apply(options?: AcceptOptions): Promise> 6 | 7 | status(): "abort" | "fail" | "idle" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/electron-webpack-ts/changelog.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 (2018-03-18) 2 | 3 | ### Features 4 | 5 | * Upgrade `ts-loader` to `4.1.0` (webpack 4 support). 6 | 7 | # 1.1.0 (2017-08-27) 8 | dde84 9 | ### Features 10 | 11 | * Upgrade `ts-loader` to `2.3.4` ([configFile](https://github.com/TypeStrong/ts-loader/pull/607) option is now used by electron-webpack). -------------------------------------------------------------------------------- /packages/electron-webpack/src/configurators/vue/vue-main-dev-entry.ts: -------------------------------------------------------------------------------- 1 | import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer" 2 | 3 | // install vue-devtools 4 | require("electron").app.on("ready", () => { 5 | installExtension(VUEJS_DEVTOOLS) 6 | .catch(error => { 7 | console.log("Unable to install `vue-devtools`: \n", error) 8 | }) 9 | }) -------------------------------------------------------------------------------- /npm-publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ln -f readme.md packages/electron-webpack/readme.md 5 | 6 | npm publish packages/electron-webpack || true 7 | npm publish packages/electron-webpack-eslint || true 8 | npm publish packages/electron-webpack-js || true 9 | npm publish packages/electron-webpack-ts || true 10 | npm publish packages/electron-webpack-vue || true -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // bloody babel 7 requires to use ONLY babel.config.js and old convenient key in package.json or .babelrc ARE NOT SUPPORTED ANYMORE 4 | module.exports = function (api) { 5 | // bloody babel 7 requires this string to revalidate cache of config on env change 6 | api.env() 7 | return { 8 | "presets": [ 9 | "babel-preset-ts-node8", 10 | ], 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /packages/*/out/ 3 | node_modules/ 4 | lerna-debug.log 5 | packages/electron-webpack/readme.md 6 | _book/ 7 | #packages/electron-webpack/scheme.json 8 | 9 | # to not exclude .js.snap (jest snapshots) 10 | /test/out/**/*.js 11 | /test/out/**/*.map 12 | 13 | .idea/workspace.xml 14 | .idea/shelf/ 15 | 16 | /CHANGELOG.md 17 | yarn-error.log 18 | 19 | .renderer-index-template.html 20 | 21 | /site/ -------------------------------------------------------------------------------- /packages/electron-webpack-eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-webpack-eslint", 3 | "version": "6.0.0", 4 | "license": "MIT", 5 | "author": "Greg Holguin ", 6 | "files": [], 7 | "repository": "electron-userland/electron-webpack", 8 | "dependencies": { 9 | "babel-eslint": "^10.1.0", 10 | "eslint": "^6.8.0", 11 | "eslint-friendly-formatter": "^4.0.1", 12 | "eslint-loader": "^4.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/electron-webpack-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-webpack-ts", 3 | "version": "4.0.1", 4 | "license": "MIT", 5 | "author": "Vladimir Krivosheev ", 6 | "files": [], 7 | "repository": "electron-userland/electron-webpack", 8 | "dependencies": { 9 | "fork-ts-checker-webpack-plugin": "^4.1.2", 10 | "ts-loader": "^6.2.2" 11 | }, 12 | "peerDependencies": { 13 | "typescript": "^3.8.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | * **Version**: 4 | 5 | 6 | 7 | 8 | * **Target**: 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/electron-webpack-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-webpack-vue", 3 | "version": "2.4.0", 4 | "license": "MIT", 5 | "author": "Vladimir Krivosheev ", 6 | "files": [], 7 | "repository": "electron-userland/electron-webpack", 8 | "dependencies": { 9 | "vue-class-component": "^7.2.3", 10 | "vue-html-loader": "^1.2.4", 11 | "vue-loader": "^15.9.1", 12 | "vue-style-loader": "^4.1.2", 13 | "vue-template-compiler": "^2.6.11" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/electron-webpack/typings/crocket.d.ts: -------------------------------------------------------------------------------- 1 | declare module "crocket" { 2 | export interface CrocketConnectOptions { 3 | path: string 4 | } 5 | 6 | export default class Crocket { 7 | connect(options: CrocketConnectOptions, connected: (error?: Error) => void): void 8 | 9 | listen(options: CrocketConnectOptions, handler: (error?: Error) => void): void 10 | 11 | on(name: string, handler: (data: T) => void): void 12 | 13 | emit(name: string, data?: any): void 14 | } 15 | } -------------------------------------------------------------------------------- /packages/electron-webpack-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-webpack-js", 3 | "version": "2.4.1", 4 | "license": "MIT", 5 | "author": "Vladimir Krivosheev ", 6 | "files": [], 7 | "repository": "electron-userland/electron-webpack", 8 | "dependencies": { 9 | "@babel/core": "^7.9.0", 10 | "babel-loader": "^8.1.0", 11 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 12 | "@babel/preset-env": "^7.9.0", 13 | "babel-plugin-component": "^1.1.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.idea/rc-producer.yml: -------------------------------------------------------------------------------- 1 | - &defaults 2 | files: ["test/src/**/*", "!**/helpers/**/*"] 3 | script: "node_modules/jest-cli/bin/jest.js" 4 | scriptArgs: ["-i", &filePattern '--testPathPattern=[/\\]{1}${fileNameWithoutExt}\.\w+$'] 5 | rcName: "${fileNameWithoutExt}" 6 | 7 | - 8 | <<: *defaults 9 | lineRegExp: '^\s*(?:test|it|testAndIgnoreApiRate)(?:\.[\w.]+)?\("([^"'']+)' 10 | scriptArgs: ["-i", "-t", "${0regExp}", *filePattern] 11 | rcName: "${fileNameWithoutExt}.${0}" 12 | env: 13 | # DEBUG: electron-webpack* -------------------------------------------------------------------------------- /packages/electron-webpack/tsconfig-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitReturns": true, 11 | 12 | "inlineSources": true, 13 | "sourceMap": true, 14 | 15 | "allowSyntheticDefaultImports": true, 16 | "experimentalDecorators": true, 17 | 18 | "newLine": "lf", 19 | 20 | "noEmitOnError": true 21 | } 22 | } -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | node_modules/*/test 4 | node_modules/*/tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | 11 | # examples 12 | example 13 | examples 14 | 15 | # code coverage directories 16 | coverage 17 | .nyc_output 18 | 19 | # build scripts 20 | Makefile 21 | Gulpfile.js 22 | Gruntfile.js 23 | 24 | # configs 25 | .tern-project 26 | .gitattributes 27 | .editorconfig 28 | .*ignore 29 | .eslintrc 30 | .jshintrc 31 | .flowconfig 32 | .documentup.json 33 | .yarn-metadata.json 34 | 35 | # misc 36 | *.gz 37 | *.md 38 | -------------------------------------------------------------------------------- /packages/electron-webpack/.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | node_modules/*/test 4 | node_modules/*/tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | 11 | # examples 12 | example 13 | examples 14 | 15 | # code coverage directories 16 | coverage 17 | .nyc_output 18 | 19 | # build scripts 20 | Makefile 21 | Gulpfile.js 22 | Gruntfile.js 23 | 24 | # configs 25 | .tern-project 26 | .gitattributes 27 | .editorconfig 28 | .*ignore 29 | .eslintrc 30 | .jshintrc 31 | .flowconfig 32 | .documentup.json 33 | .yarn-metadata.json 34 | 35 | # misc 36 | *.gz 37 | *.md 38 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/electron-main-hmr/main-hmr.ts: -------------------------------------------------------------------------------- 1 | require("source-map-support/source-map-support.js").install() 2 | 3 | const socketPath = process.env.ELECTRON_HMR_SOCKET_PATH! 4 | if (socketPath == null) { 5 | throw new Error(`[HMR] Env ELECTRON_HMR_SOCKET_PATH is not set`) 6 | } 7 | 8 | // module, but not relative path must be used (because this file is used as entry) 9 | const HmrClient = require("electron-webpack/out/electron-main-hmr/HmrClient").HmrClient 10 | // tslint:disable:no-unused-expression 11 | new HmrClient(socketPath, (module as any).hot, () => { 12 | return __webpack_hash__ 13 | }) -------------------------------------------------------------------------------- /.idea/dictionaries/develar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | crocket 5 | dotenv 6 | eaddrinuse 7 | embeddedjs 8 | errored 9 | gitbook 10 | lesscss 11 | minification 12 | netlify 13 | nunjucks 14 | runnerw 15 | sigusr 16 | transpilation 17 | transpiling 18 | typescriptlang 19 | undelayed 20 | vscode 21 | 22 | 23 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/node:10 7 | environment: 8 | JEST_JUNIT_OUTPUT: ./test-reports/test.xml 9 | steps: 10 | - checkout 11 | - restore_cache: 12 | keys: 13 | - dependencies-{{ checksum "yarn.lock" }} 14 | - run: 15 | command: yarn 16 | - save_cache: 17 | key: dependencies-{{ checksum "yarn.lock" }} 18 | paths: 19 | - node_modules 20 | - run: 21 | command: yarn test -- --ci --testResultsProcessor="jest-junit" 22 | - store_test_results: 23 | path: test-reports -------------------------------------------------------------------------------- /docs/en/extra.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | // document.addEventListener("DOMContentLoaded", () => { 4 | loadNavPane() 5 | // }) 6 | 7 | function loadNavPane() { 8 | let nav = document.getElementsByClassName("md-nav") 9 | for (let i = 0; i < nav.length; i++) { 10 | const item = nav.item(i) 11 | if (typeof item.style === "undefined") { 12 | continue; 13 | } 14 | 15 | if (item.getAttribute("data-md-level") && item.getAttribute("data-md-component")) { 16 | item.style.display = 'block' 17 | item.style.overflow = 'visible' 18 | } 19 | } 20 | 21 | nav = document.getElementsByClassName("md-nav__toggle") 22 | for (let i = 0; i < nav.length; i++) { 23 | nav.item(i).checked = true; 24 | } 25 | } -------------------------------------------------------------------------------- /electron-webpack.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/configurators/eslint.ts: -------------------------------------------------------------------------------- 1 | import { WebpackConfigurator } from "../main" 2 | 3 | export function configureEslint(configurator: WebpackConfigurator) { 4 | const hasPreset = configurator.hasDevDependency("electron-webpack-eslint") 5 | if (!(hasPreset || (configurator.hasDevDependency("eslint") && configurator.hasDevDependency("eslint-loader")))) { 6 | return 7 | } 8 | 9 | const options: { [name: string]: any } = { 10 | cwd: configurator.projectDir 11 | } 12 | if (hasPreset || configurator.hasDevDependency("eslint-friendly-formatter")) { 13 | options.formatter = require("eslint-friendly-formatter") 14 | } 15 | 16 | configurator.rules.push({ 17 | test: /\.(jsx?|tsx?|vue)$/, 18 | enforce: "pre", 19 | exclude: /node_modules/, 20 | loader: "eslint-loader", 21 | options 22 | }) 23 | } -------------------------------------------------------------------------------- /test/src/titleTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path" 2 | import { bufferToString, rootDir, testWebpack } from "./helpers/helper" 3 | 4 | test("title true", () => testTitle(true)) 5 | test("title false", () => testTitle(false)) 6 | test("title null", () => testTitle(null)) 7 | 8 | const rendererConfig = require("electron-webpack/webpack.renderer.config") 9 | 10 | async function testTitle(title: boolean | null) { 11 | const projectDir = path.join(rootDir, "test/fixtures/simple") 12 | const configuration = await rendererConfig({ 13 | production: true, 14 | minify: false, 15 | configuration: { 16 | projectDir, 17 | title 18 | }, 19 | }) 20 | const fs = await testWebpack(configuration, projectDir, false) 21 | expect(bufferToString(fs.meta(`${projectDir}/dist/renderer/index.html`), projectDir).toString()).toMatchSnapshot() 22 | } -------------------------------------------------------------------------------- /docs/en/cli-commands.md: -------------------------------------------------------------------------------- 1 | `electron-webpack` comes with a simple CLI to help run your application in development and compile your source code. Here is a general overview of what's available. 2 | 3 | #### `electron-webpack dev` 4 | Run application in development mode. 5 | 6 | #### `electron-webpack [config]` 7 | Compile source code of specific configuration. Can be `main`, `renderer`, `renderer-dll`, or `app` (default). 8 | 9 | #### `electron-webpack --help` 10 | Yields quick overview of all CLI features. 11 | 12 | --- 13 | 14 | ### Package Scripts 15 | To make things easier, make use of setting up scripts in your `package.json` to use the CLI easier. 16 | 17 | ```json tab="package.json" 18 | { 19 | "scripts": { 20 | "dev": "electron-webpack dev", 21 | "compile": "electron-webpack" 22 | } 23 | } 24 | ``` 25 | 26 | Now you can run with `yarn dev` & `yarn compile`. 27 | -------------------------------------------------------------------------------- /docs/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # netlify-cli can be used to publish directory directly without intermediate git repository, 5 | # but it will complicate deployment because in this case you need to be authenticated on Netlify. 6 | # But for what, if you are already configure Git access? 7 | # Also, this approach allows us to manage access using GitHub and easily give access to publish to project members. 8 | 9 | 10 | # https://stackoverflow.com/questions/3311774/how-to-convert-existing-non-empty-directory-into-a-git-working-directory-and-pus 11 | 12 | cd _book 13 | 14 | # do not use force push - netlify doesn't trigger deploy for forced push 15 | git clone --no-checkout --branch en --single-branch git@github.com:develar/generated-gitbook-electron-webpack.git ./repo.tmp 16 | mv ./repo.tmp/.git ./ 17 | rmdir ./repo.tmp 18 | git add --all . 19 | git commit -m "update" 20 | git push -------------------------------------------------------------------------------- /packages/electron-webpack/src/electron-builder.ts: -------------------------------------------------------------------------------- 1 | import { Lazy } from "lazy-val" 2 | import { getElectronWebpackConfiguration } from "./config" 3 | 4 | interface Context { 5 | projectDir: string 6 | packageMetadata: Lazy<{ [key: string]: any } | null> | null 7 | } 8 | 9 | export default async function(context: Context) { 10 | const electronWebpackConfig = await getElectronWebpackConfiguration(context) 11 | const distDir = electronWebpackConfig.commonDistDirectory!! 12 | return { 13 | extraMetadata: { 14 | main: "main.js" 15 | }, 16 | files: [ 17 | { 18 | from: ".", 19 | filter: ["package.json"] 20 | }, 21 | { 22 | from: `${distDir}/main` 23 | }, 24 | { 25 | from: `${distDir}/renderer` 26 | }, 27 | { 28 | from: `${distDir}/renderer-dll` 29 | } 30 | ], 31 | extraResources: [ 32 | { 33 | from: electronWebpackConfig.staticSourceDirectory, 34 | to: electronWebpackConfig.staticSourceDirectory 35 | } 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/electron-main-hmr/HmrServer.ts: -------------------------------------------------------------------------------- 1 | import Crocket from "crocket" 2 | import { Stats } from "webpack" 3 | 4 | const debug = require("debug")("electron-webpack:dev-runner") 5 | 6 | export class HmrServer { 7 | private state = false 8 | readonly ipc = new Crocket() 9 | 10 | listen(): Promise { 11 | return new Promise((resolve, reject) => { 12 | const socketPath = `/tmp/electron-main-ipc-${process.pid.toString(16)}.sock` 13 | this.ipc.listen({path: socketPath}, error => { 14 | if (error != null) { 15 | reject(error) 16 | } 17 | if (debug.enabled) { 18 | debug(`HMR Server listening on ${socketPath}`) 19 | } 20 | resolve(socketPath) 21 | }) 22 | }) 23 | } 24 | 25 | beforeCompile() { 26 | this.state = false 27 | } 28 | 29 | built(stats: Stats): void { 30 | this.state = true 31 | setImmediate(() => { 32 | if (!this.state) { 33 | return 34 | } 35 | 36 | const hash = stats.toJson({assets: false, chunks: false, children: false, modules: false}).hash 37 | if (debug.enabled) { 38 | debug(`Send built: hash ${hash}`) 39 | } 40 | this.ipc.emit("/built", {hash}) 41 | }) 42 | } 43 | } -------------------------------------------------------------------------------- /docs/en/core-concepts.md: -------------------------------------------------------------------------------- 1 | Below is a general overview of what `electron-webpack` aims to solve, and what it isn't. 2 | 3 | #### `electron-webpack` is a module 4 | It is not a fully featured boilerplate; it is a single **updatable** package. It includes many `webpack` configurations to help jump-start your development needs. If you are looking for a *boilerplate* to get started, please see [electron-webpack-quick-start](https://github.com/electron-userland/electron-webpack-quick-start). 5 | 6 | #### `electron-webpack` has a CLI 7 | You can take use of a few useful commands such as running in development and compiling your source code ([more info](./cli-commands.md)). 8 | 9 | #### `electron-webpack` can be extended 10 | By using `electron-webpack` you are **not** restricted to any sort of API abstraction of `webpack`. Although there are [Add-ons](./add-ons.md) made available to simplify smaller tasks, the entirety of `webpack`'s documentation is fully applicable ([more info](./extending-as-a-library.md)). 11 | 12 | #### `electron-webpack` is agnostic 13 | Aside from setting up core `webpack` configurations with `@babel/preset-env` and making optimizations specific to the `electron` environment, `electron-webpack` does its best to not impose or encourage any sort of project structure or build cycle. Just as stated before, this is a module and can be used as tool outside of its CLI. 14 | -------------------------------------------------------------------------------- /packages/electron-webpack/typings/webpack-dev-server.d.ts: -------------------------------------------------------------------------------- 1 | declare module "webpack-dev-server" { 2 | // import * as core from "express-serve-static-core" 3 | import * as http from "http" 4 | import * as webpack from "webpack" 5 | 6 | namespace WebpackDevServer { 7 | export interface Configuration { 8 | contentBase?: string 9 | hot?: boolean 10 | https?: boolean 11 | historyApiFallback?: boolean 12 | compress?: boolean 13 | proxy?: any 14 | staticOptions?: any 15 | quiet?: boolean 16 | noInfo?: boolean 17 | lazy?: boolean 18 | filename?: string | RegExp 19 | watchOptions?: webpack.WatchOptions 20 | publicPath?: string 21 | headers?: any 22 | stats?: webpack.compiler.StatsOptions | webpack.compiler.StatsToStringOptions 23 | public?: string 24 | disableHostCheck?: boolean 25 | 26 | // setup?(app: core.Express, ctx: any): void 27 | } 28 | 29 | export interface WebpackDevServer { 30 | new (webpack: webpack.Compiler, 31 | config: Configuration): WebpackDevServer 32 | 33 | listen(port: number, 34 | hostname: string, 35 | callback?: Function): http.Server 36 | 37 | listen(port: number, 38 | callback?: Function): http.Server 39 | } 40 | } 41 | 42 | const wds: WebpackDevServer.WebpackDevServer 43 | export default wds 44 | } -------------------------------------------------------------------------------- /test/out/__snapshots__/titleTest.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`title false 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | " 13 | `; 14 | 15 | exports[`title null 1`] = ` 16 | " 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | " 25 | `; 26 | 27 | exports[`title true 1`] = ` 28 | " 29 | 30 | 31 | 32 | Test 33 | 34 |
35 | 36 | " 37 | `; 38 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/configurators/vue/vue.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path" 2 | import { WebpackConfigurator } from "../../main" 3 | 4 | export function configureVue(configurator: WebpackConfigurator) { 5 | if (!configurator.hasDependency("vue")) { 6 | return 7 | } 8 | 9 | configurator.extensions.push(".vue") 10 | 11 | Object.assign(configurator.config.resolve!!.alias, { 12 | vue$: "vue/dist/vue.esm.js", 13 | "vue-router$": "vue-router/dist/vue-router.esm.js", 14 | }) 15 | 16 | if (!configurator.isProduction && configurator.type === "main") { 17 | configurator.entryFiles.push(path.join(__dirname, "vue-main-dev-entry.js")) 18 | } 19 | } 20 | 21 | export function configureVueRenderer(configurator: WebpackConfigurator) { 22 | configurator.entryFiles.push(path.join(__dirname, "../../../vue-renderer-entry.js")) 23 | 24 | configurator.debug("Vue detected") 25 | configurator.rules.push( 26 | { 27 | test: /\.html$/, 28 | use: "vue-html-loader" 29 | }, 30 | { 31 | test: /\.vue$/, 32 | use: { 33 | loader: "vue-loader", 34 | options: { 35 | loaders: { 36 | sass: "vue-style-loader!css-loader!sass-loader?indentedSyntax=1", 37 | scss: "vue-style-loader!css-loader!sass-loader", 38 | } 39 | } 40 | } 41 | }, 42 | ) 43 | 44 | const VueLoaderPlugin = require("vue-loader/lib/plugin") 45 | configurator.plugins.push(new VueLoaderPlugin()) 46 | } -------------------------------------------------------------------------------- /packages/electron-webpack/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as path from "path" 4 | import yargs from "yargs" 5 | 6 | // not strict because we allow to pass any webpack args 7 | // tslint:disable-next-line:no-unused-expression 8 | yargs 9 | .command(["app", "compile", "*"], "Compile application", yargs => yargs, argv => build("app")) 10 | .command(["main"], "Compile main process", yargs => yargs, argv => build("main")) 11 | .command(["renderer"], "Compile renderer process", yargs => yargs, argv => build("renderer")) 12 | .command(["dll"], "Compile DLL bundles", yargs => yargs, argv => build("renderer.dll")) 13 | .command(["dev"], "Run a development mode", yargs => yargs, argv => runInDevMode()) 14 | .argv 15 | 16 | function build(configFile: string) { 17 | const args = process.argv 18 | // if command `electron-webpack` - remove first 2 args, if `electron-webpack compile` (or any other subcommand name) - 3 19 | const sliceIndex = args.length > 2 && !args[2].startsWith("-") ? 3 : 2 20 | const extraWebpackArgs = sliceIndex < args.length ? args.slice(sliceIndex) : [] 21 | // remove extra args 22 | args.length = 2 23 | if (!extraWebpackArgs.some(it => it.includes("--env.production"))) { 24 | args.push("--env.production") 25 | } 26 | args.push("--progress") 27 | args.push(...extraWebpackArgs) 28 | args.push("--config", path.join(__dirname, "..", `webpack.${configFile}.config.js`)) 29 | 30 | require("yargs")(args.slice(2)) 31 | require(require.resolve("webpack-cli")) 32 | } 33 | 34 | function runInDevMode() { 35 | require("./dev/dev-runner") 36 | } -------------------------------------------------------------------------------- /docs/en/using-static-assets.md: -------------------------------------------------------------------------------- 1 | When using `webpack` to bundle all of our assets together we lose the ability to provide a full path to our assets. This is especially important when we need to use modules like `fs` or those that require a file path to an asset. `electron-webpack` is aware of that issue and provides a solution. 2 | 3 | You may have noticed in [Project Structure](./project-structure.md) there is a directory specifically for static assets (`static/`). It is here where we can put assets we explicity don't want `webpack` to bundle. So now how can we access their path? 4 | 5 | ### Using the `__static` variable 6 | 7 | Similar to how `__dirname` can provide you with a path to the parent directory name when working in a Node.js environment, `__static` is made available to provide you a path to your `static/` folder. This variable is available in both `development` and `production`. 8 | 9 | #### Use Case 10 | 11 | Let's say we have a static Text file (`foobar.txt`) we need to read into our application using `fs`. Here's how we can use the `__static` variable to get a reliable path to our asset. 12 | 13 | ```txt tab="static/foobar.txt" 14 | foobarbizzbuzz 15 | ``` 16 | 17 | ##### someScript.js (`main` or `renderer` process) 18 | ```js 19 | import fs from 'fs' 20 | import path from 'path' 21 | 22 | /* use `path` to create the full path to our asset */ 23 | const pathToAsset = path.join(__static, '/foobar.txt') 24 | 25 | /* use `fs` to consume the path and read our asset */ 26 | const fileContents = fs.readFileSync(pathToAsset, 'utf8') 27 | 28 | console.log(fileContents) 29 | // => "foobarbizzbuzz" 30 | ``` 31 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: electron-webpack 2 | use_directory_urls: false 3 | theme: 4 | name: material 5 | logo: 6 | icon: 'build' 7 | palette: 8 | primary: 'blue' 9 | accent: 'blue' 10 | 11 | docs_dir: docs/en 12 | 13 | repo_name: electron-userland/electron-webpack 14 | repo_url: 'https://github.com/electron-userland/electron-webpack' 15 | edit_uri: "" 16 | dev_addr: 0.0.0.0:8000 17 | strict: true 18 | 19 | extra_javascript: 20 | - extra.js 21 | 22 | markdown_extensions: 23 | - admonition 24 | - smarty 25 | - pymdownx.smartsymbols 26 | - pymdownx.details 27 | - pymdownx.inlinehilite 28 | - pymdownx.superfences 29 | - markdown_include.include: 30 | base_path: docs/en 31 | - toc: 32 | permalink: true 33 | 34 | google_analytics: 35 | - 'UA-104881670-1' 36 | - webpack.electron.build 37 | 38 | nav: 39 | - Introduction: index.md 40 | - Core Concepts: core-concepts.md 41 | - Development: 42 | - Development: development.md 43 | - Add-ons: add-ons.md 44 | - Configuration: configuration.md 45 | - CLI Commands: cli-commands.md 46 | - Dll Bundle Splitting: dll-bundle-splitting.md 47 | - Environment Variables: environment-variables.md 48 | - Extending as a Library: extending-as-a-library.md 49 | - Modifying Webpack Configurations: modifying-webpack-configurations.md 50 | - Using Static Assets: using-static-assets.md 51 | - Project Structure: project-structure.md 52 | - Building: building.md 53 | - Miscellaneous: 54 | - Dependency Management: dependency-management.md 55 | - Technical Differences of electron-compile: technical-differences-of-electron-compile.md -------------------------------------------------------------------------------- /packages/electron-webpack/src/core.ts: -------------------------------------------------------------------------------- 1 | export interface PackageMetadata { 2 | name?: string 3 | 4 | dependencies: { [key: string]: any } 5 | devDependencies: { [key: string]: any } 6 | 7 | electronWebpack?: ElectronWebpackConfiguration | null 8 | } 9 | 10 | export interface ElectronWebpackConfiguration { 11 | whiteListedModules?: Array 12 | externals?: Array 13 | electronVersion?: string 14 | 15 | renderer?: ElectronWebpackConfigurationRenderer | null 16 | main?: ElectronWebpackConfigurationMain | null 17 | 18 | staticSourceDirectory?: string | null 19 | commonSourceDirectory?: string | null 20 | commonDistDirectory?: string | null 21 | 22 | title?: string | boolean | null 23 | 24 | projectDir?: string | null 25 | } 26 | 27 | export type ConfigurationType = "main" | "renderer" | "renderer-dll" | "test" 28 | 29 | export interface PartConfiguration { 30 | sourceDirectory?: string | null 31 | } 32 | 33 | export interface ElectronWebpackConfigurationRenderer extends PartConfiguration { 34 | dll?: Array | { [key: string]: any } | null 35 | webpackConfig?: string | null 36 | webpackDllConfig?: string | null 37 | template?: string | null 38 | } 39 | 40 | export interface ElectronWebpackConfigurationMain extends PartConfiguration { 41 | /** 42 | * The extra [entry points](https://webpack.js.org/concepts/entry-points/). 43 | */ 44 | extraEntries?: Array | { [key: string]: string | Array } | string 45 | webpackConfig?: string | null 46 | } 47 | 48 | export interface ConfigurationEnv { 49 | production?: boolean 50 | autoClean?: boolean 51 | 52 | minify?: boolean 53 | 54 | forkTsCheckerLogger?: any 55 | 56 | configuration?: ElectronWebpackConfiguration 57 | } 58 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/util.ts: -------------------------------------------------------------------------------- 1 | import * as BluebirdPromise from "bluebird" 2 | import { stat, Stats } from "fs-extra" 3 | import { createServer } from "net" 4 | import * as path from "path" 5 | 6 | export async function statOrNull(file: string): Promise { 7 | return orNullIfFileNotExist(stat(file)) 8 | } 9 | 10 | export function orNullIfFileNotExist(promise: Promise): Promise { 11 | return orIfFileNotExist(promise, null) 12 | } 13 | 14 | export function orIfFileNotExist(promise: Promise, fallbackValue: T): Promise { 15 | return promise 16 | .catch(e => { 17 | if (e.code === "ENOENT" || e.code === "ENOTDIR") { 18 | return fallbackValue 19 | } 20 | throw e 21 | }) 22 | } 23 | 24 | export function getFirstExistingFile(names: Array, rootDir: string | null): Promise { 25 | return BluebirdPromise.filter(names.map(it => rootDir == null ? it : path.join(rootDir, it)), it => statOrNull(it).then(it => it != null)) 26 | .then(it => it.length > 0 ? it[0] : null) 27 | } 28 | 29 | export function getFreePort(defaultHost: string, defaultPort: number) { 30 | return new Promise((resolve, reject) => { 31 | const server = createServer({pauseOnConnect: true}) 32 | server.addListener("listening", () => { 33 | const port = (server.address() as any).port 34 | server.close(() => resolve(port)) 35 | }) 36 | 37 | function doListen(port: number) { 38 | server.listen({ 39 | host: defaultHost, 40 | port, 41 | backlog: 1, 42 | exclusive: true 43 | }) 44 | } 45 | 46 | server.on("error", e => { 47 | if ((e as any).code === "EADDRINUSE") { 48 | server.close(() => doListen(0)) 49 | } 50 | else { 51 | reject(e) 52 | } 53 | }) 54 | 55 | doListen(defaultPort) 56 | }) 57 | } -------------------------------------------------------------------------------- /packages/electron-webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-webpack", 3 | "version": "2.8.2", 4 | "license": "MIT", 5 | "author": "Vladimir Krivosheev ", 6 | "main": "out/main.js", 7 | "files": [ 8 | "out", 9 | "*.js", 10 | "scheme.json", 11 | "tsconfig-base.json", 12 | "vendor" 13 | ], 14 | "bin": { 15 | "electron-webpack": "./out/cli.js" 16 | }, 17 | "repository": "electron-userland/electron-webpack", 18 | "engines": { 19 | "node": ">=10.0" 20 | }, 21 | "//": "@types/webpack-env in the dependencies because used by clients", 22 | "dependencies": { 23 | "@svgr/webpack": "^5.5.0", 24 | "@types/webpack-env": "^1.15.1", 25 | "async-exit-hook": "^2.0.1", 26 | "bluebird": "^3.7.2", 27 | "chalk": "^4.0.0", 28 | "crocket": "^0.9.11", 29 | "css-hot-loader": "^1.4.4", 30 | "css-loader": "^3.4.2", 31 | "debug": "^4.1.1", 32 | "dotenv": "^8.2.0", 33 | "dotenv-expand": "^5.1.0", 34 | "electron-devtools-installer": "^2.2.4", 35 | "electron-webpack-js": "~2.4.1", 36 | "file-loader": "^6.0.0", 37 | "fs-extra": "^9.0.0", 38 | "html-loader": "^1.1.0", 39 | "html-webpack-plugin": "^4.0.4", 40 | "lazy-val": "^1.0.4", 41 | "mini-css-extract-plugin": "^0.9.0", 42 | "node-loader": "^0.6.0", 43 | "read-config-file": "~4.0.1", 44 | "semver": "^7.1.3", 45 | "source-map-support": "^0.5.16", 46 | "style-loader": "^1.1.3", 47 | "terser-webpack-plugin": "^2.3.5", 48 | "url-loader": "^4.0.0", 49 | "webpack-cli": "^3.3.11", 50 | "webpack-dev-server": "^3.10.3", 51 | "webpack-merge": "^4.2.2", 52 | "yargs": "^15.3.1" 53 | }, 54 | "peerDependencies": { 55 | "webpack": "^4.42.1" 56 | }, 57 | "resolutions": { 58 | "js-yaml": "^3.13.1", 59 | "htmlnano": "^0.2.5" 60 | }, 61 | "types": "./out/main.d.ts", 62 | "publishConfig": { 63 | "tag": "next" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/targets/MainTarget.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path" 2 | import { BannerPlugin, DefinePlugin } from "webpack" 3 | import { WebpackConfigurator } from "../main" 4 | import { getFirstExistingFile } from "../util" 5 | import { BaseTarget, configureFileLoader } from "./BaseTarget" 6 | 7 | export class MainTarget extends BaseTarget { 8 | constructor() { 9 | super() 10 | } 11 | 12 | configureRules(configurator: WebpackConfigurator): void { 13 | super.configureRules(configurator) 14 | 15 | configurator.rules.push({ 16 | test: /\.(png|jpg|gif)$/, 17 | use: [ 18 | { 19 | loader: "url-loader", 20 | // to avoid any issues related to asar, embed any image up to 10MB as data url 21 | options: configureFileLoader("imgs", 10 * 1024 * 1024), 22 | } 23 | ] 24 | }) 25 | } 26 | 27 | async configurePlugins(configurator: WebpackConfigurator): Promise { 28 | await BaseTarget.prototype.configurePlugins.call(this, configurator) 29 | 30 | if (configurator.isProduction) { 31 | configurator.plugins.push(new DefinePlugin({ 32 | __static: `process.resourcesPath + "/${configurator.staticSourceDirectory}"` 33 | })) 34 | 35 | // do not add for main dev (to avoid adding to hot update chunks), our main-hmr install it 36 | configurator.plugins.push(new BannerPlugin({ 37 | banner: 'require("source-map-support/source-map-support.js").install();', 38 | test: /\.js$/, 39 | raw: true, 40 | entryOnly: true, 41 | })) 42 | return 43 | } 44 | 45 | configurator.entryFiles.push(path.join(__dirname, "../electron-main-hmr/main-hmr")) 46 | const devIndexFile = await getFirstExistingFile(["index.dev.ts", "index.dev.js"], path.join(configurator.projectDir, "src/main")) 47 | if (devIndexFile != null) { 48 | configurator.entryFiles.push(devIndexFile) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/en/extending-as-a-library.md: -------------------------------------------------------------------------------- 1 | One of the great benefits of using `electron-webpack` is that the entirity of `webpack`'s documentation still applies. You can easily skip using the provided CLI and modify the configurations to meet your needs. Since `electron` uses a multi-process architecture, there are also multiple `webpack` configurations. Let's cover what's provided. 2 | 3 | ### Configuration Files 4 | 5 | * `electron-webpack/webpack.main.config.js` (`main` process) 6 | * `electron-webpack/webpack.renderer.config.js` (`renderer` process) 7 | * `electron-webpack/webpack.renderer.dll.config.js` (Dll bundle spliting) 8 | 9 | If you are wanting to look at these configurations internally, you can easily do the following to print them into your console. Notice that each configuration returns a `Promise`. 10 | 11 | ```js 12 | const webpackMain = require('electron-webpack/webpack.main.config.js') 13 | const { inspect } = require('util') 14 | 15 | webpackMain().then(config => { 16 | console.log(inspect(config, { 17 | showHidden: false, 18 | depth: null, 19 | colors: true 20 | })) 21 | }) 22 | ``` 23 | 24 | ## Adding a Custom Loader/Plugin 25 | 26 | Let's say we need to support `*.txt` files in our `renderer` process and want to use the `raw-loader` to do so, here's how we can get that setup. 27 | 28 | ##### Install `raw-loader` 29 | ```bash 30 | yarn add -D raw-loader 31 | ``` 32 | 33 | ```js tab="myCustomWebpack.renderer.config.js" 34 | const webpackRenderer = require('electron-webpack/webpack.renderer.config.js') 35 | 36 | module.exports = env => { 37 | return new Promise((resolve, reject) => { 38 | 39 | /* get provided config */ 40 | webpackRenderer(env).then(rendererConfig => { 41 | 42 | /* add `raw-loader` */ 43 | rendererConfig.module.rules.push({ 44 | test: /\.txt$/, 45 | use: 'raw-loader' 46 | }) 47 | 48 | /* return modified config to webpack */ 49 | resolve(rendererConfig) 50 | }) 51 | }) 52 | } 53 | ``` 54 | 55 | Now with your new custom webpack configuration file, you can use the native [`webpack` CLI](https://webpack.js.org/api/cli/). 56 | 57 | ```bash 58 | webpack --config myCustomWebpack.renderer.config.js 59 | ``` 60 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/configurators/dll.ts: -------------------------------------------------------------------------------- 1 | import { readdir, readJson } from "fs-extra" 2 | import * as path from "path" 3 | import { DllPlugin, DllReferencePlugin } from "webpack" 4 | import { WebpackConfigurator } from "../main" 5 | import { orNullIfFileNotExist, statOrNull } from "../util" 6 | 7 | export async function configureDll(configurator: WebpackConfigurator): Promise { 8 | let dllManifest: string | null = null 9 | const projectDir = configurator.projectDir 10 | 11 | if (configurator.type === "renderer-dll") { 12 | const dll = configurator.electronWebpackConfiguration.renderer!!.dll 13 | if (dll == null) { 14 | throw new Error(`renderer-dll requires DLL configuration`) 15 | } 16 | 17 | configurator.config.entry = Array.isArray(dll) ? {vendor: dll} : dll 18 | 19 | dllManifest = path.join(configurator.commonDistDirectory, configurator.type, "manifest.json") 20 | configurator.plugins.push(new DllPlugin({ 21 | name: "[name]", 22 | path: dllManifest, 23 | context: projectDir, 24 | })) 25 | 26 | const output = configurator.config.output! 27 | // leave as default "var" 28 | delete output.libraryTarget 29 | output.library = "[name]" 30 | } 31 | else if (configurator.type === "renderer") { 32 | const dllDir = path.join(configurator.commonDistDirectory, "renderer-dll") 33 | const dirStat = await statOrNull(dllDir) 34 | if (dirStat == null || !dirStat.isDirectory()) { 35 | configurator.debug("No DLL directory") 36 | return null 37 | } 38 | 39 | configurator.debug(`DLL directory: ${dllDir}`) 40 | configurator.plugins.push(new DllReferencePlugin({ 41 | context: projectDir, 42 | manifest: await readJson(path.join(dllDir, "manifest.json")), 43 | })) 44 | } 45 | 46 | return dllManifest 47 | } 48 | 49 | export async function getDllAssets(dllDir: string, configurator: WebpackConfigurator) { 50 | if (configurator.electronWebpackConfiguration.renderer!!.dll == null) { 51 | return [] 52 | } 53 | 54 | const files = await orNullIfFileNotExist(readdir(dllDir)) 55 | return files == null ? [] : files.filter(it => it.endsWith(".js") || it.endsWith(".css")).sort() 56 | } -------------------------------------------------------------------------------- /test/src/loaderDetectionTest.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, writeJson } from "fs-extra" 2 | import * as path from "path" 3 | import { assertThat, getMutableProjectDir, testWebpack } from "./helpers/helper" 4 | 5 | test("nunjucks", async () => { 6 | const projectDir = await getMutableProjectDir() 7 | await writeJson(path.join(projectDir, "package.json"), { 8 | name: "Test", 9 | devDependencies: { 10 | "nunjucks-loader": "*" 11 | } 12 | }) 13 | await writeFile(path.join(projectDir, "src/main/page.njk"), "myGlobal = {{ myGlobal }}") 14 | await writeFile(path.join(projectDir, "src/main/index.js"), 'import "./page.njk"') 15 | const configuration = await require("electron-webpack/webpack.main.config.js")({production: true, configuration: { 16 | projectDir, 17 | }}) 18 | 19 | return await assertThat(testWebpack(configuration, projectDir)).throws(projectDir) 20 | }) 21 | 22 | test("sass", async () => { 23 | const projectDir = await getMutableProjectDir() 24 | await writeFile(path.join(projectDir, "src/renderer/file.scss"), ` 25 | $font-stack: Helvetica, sans-serif; 26 | $primary-color: #333; 27 | 28 | body { 29 | font: 100% $font-stack; 30 | color: $primary-color; 31 | }`) 32 | await writeFile(path.join(projectDir, "src/renderer/index.js"), 'import "./file.scss"') 33 | const configuration = await require("electron-webpack/webpack.renderer.config.js")({production: true, configuration: { 34 | projectDir, 35 | }}) 36 | 37 | return await assertThat(testWebpack(configuration, projectDir)).throws(projectDir) 38 | }) 39 | 40 | test("react", async () => { 41 | const projectDir = await getMutableProjectDir() 42 | await writeJson(path.join(projectDir, "package.json"), { 43 | name: "Test", 44 | devDependencies: { 45 | "@babel/preset-react": "*" 46 | } 47 | }) 48 | await writeFile(path.join(projectDir, "src/renderer/file.jsx"), ` 49 | 50 | Click Me 51 | 52 | `) 53 | await writeFile(path.join(projectDir, "src/renderer/index.js"), 'import "./file.jsx"') 54 | const configuration = require("electron-webpack/webpack.renderer.config.js")({production: true, configuration: { 55 | projectDir, 56 | }}) 57 | await assertThat(configuration).throws(projectDir) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/config.ts: -------------------------------------------------------------------------------- 1 | import { readJson } from "fs-extra" 2 | import { Lazy } from "lazy-val" 3 | import * as path from "path" 4 | import { getConfig } from "read-config-file" 5 | import { ElectronWebpackConfiguration } from "./core" 6 | import { orNullIfFileNotExist } from "./util" 7 | 8 | export function getPackageMetadata(projectDir: string) { 9 | return new Lazy(() => orNullIfFileNotExist(readJson(path.join(projectDir, "package.json")))) 10 | } 11 | 12 | export interface ConfigurationRequest { 13 | projectDir: string 14 | packageMetadata: Lazy<{ [key: string]: any } | null> | null 15 | } 16 | 17 | export function getDefaultRelativeSystemDependentCommonSource(): string { 18 | return path.join("src", "common") 19 | } 20 | 21 | /** 22 | * Return configuration with resolved staticSourceDirectory / commonDistDirectory / commonSourceDirectory. 23 | */ 24 | export async function getElectronWebpackConfiguration(context: ConfigurationRequest): Promise { 25 | const result = await getConfig({ 26 | packageKey: "electronWebpack", 27 | configFilename: "electron-webpack", 28 | projectDir: context.projectDir, 29 | packageMetadata: context.packageMetadata 30 | }) 31 | const configuration: ElectronWebpackConfiguration = result == null || result.result == null ? {} : result.result 32 | if (configuration.staticSourceDirectory == null) { 33 | configuration.staticSourceDirectory = "static" 34 | } 35 | if (configuration.commonDistDirectory == null) { 36 | configuration.commonDistDirectory = "dist" 37 | } 38 | if (configuration.commonSourceDirectory == null) { 39 | configuration.commonSourceDirectory = getDefaultRelativeSystemDependentCommonSource() 40 | } 41 | configuration.commonDistDirectory = path.resolve(context.projectDir, configuration.commonDistDirectory) 42 | configuration.commonSourceDirectory = path.resolve(context.projectDir, configuration.commonSourceDirectory) 43 | 44 | if (configuration.renderer === undefined) { 45 | configuration.renderer = {} 46 | } 47 | if (configuration.main === undefined) { 48 | configuration.main = {} 49 | } 50 | 51 | if (configuration.projectDir == null) { 52 | configuration.projectDir = context.projectDir 53 | } 54 | return configuration 55 | } 56 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/configurators/js.ts: -------------------------------------------------------------------------------- 1 | import { gte } from "semver" 2 | import { WebpackConfigurator } from "../main" 3 | 4 | export function createBabelLoader(configurator: WebpackConfigurator) { 5 | // better to use require instead of just preset name to avoid babel resolving (in our test we set custom resolver - and only in case of explicit required it works) 6 | const presets = [ 7 | [ 8 | require("@babel/preset-env").default, { 9 | modules: false, 10 | targets: computeBabelEnvTarget(configurator.isRenderer, configurator.electronVersion), 11 | }], 12 | ] 13 | const plugins = [ 14 | require("@babel/plugin-syntax-dynamic-import").default, 15 | ] 16 | 17 | if (configurator.type !== "main" && configurator.hasDependency("element-ui")) { 18 | plugins.push([require("babel-plugin-component"), { 19 | libraryName: "element-ui", 20 | styleLibraryName: "theme-chalk" 21 | }]) 22 | } 23 | 24 | addBabelItem(presets, configurator.getMatchingDevDependencies({ 25 | includes: ["babel-preset-", "@babel/preset-"], 26 | excludes: ["babel-preset-env", "@babel/preset-env"], 27 | })) 28 | addBabelItem(plugins, configurator.getMatchingDevDependencies({ 29 | includes: ["babel-plugin-", "@babel/plugin-"], 30 | excludes: ["babel-plugin-syntax-dynamic-import", "@babel/plugin-syntax-dynamic-import"], 31 | })) 32 | 33 | return { 34 | loader: "babel-loader", 35 | options: { 36 | presets, 37 | plugins 38 | } 39 | } 40 | } 41 | 42 | function addBabelItem(to: Array, names: Array) { 43 | for (const name of names) { 44 | const module = require(name) 45 | to.push([module.default || module]) 46 | } 47 | } 48 | 49 | function computeBabelEnvTarget(isRenderer: boolean, electronVersion: string) { 50 | if (isRenderer) { 51 | return { 52 | electron: electronVersion 53 | } 54 | } 55 | 56 | let nodeVersion = "7.4.0" 57 | if (gte(electronVersion, "3.0.0")) { 58 | nodeVersion = "10.2.0" 59 | } 60 | else if (gte(electronVersion, "2.0.0")) { 61 | nodeVersion = "8.9.3" 62 | } 63 | else if (gte(electronVersion, "1.8.2")) { 64 | nodeVersion = "8.2.1" 65 | } 66 | else if (gte(electronVersion, "1.7.3")) { 67 | nodeVersion = "7.9.0" 68 | } 69 | 70 | return { 71 | node: nodeVersion 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /docs/en/dependency-management.md: -------------------------------------------------------------------------------- 1 | When creating `electron` applications, dependency management can be a little different for specific cases. To make sure everybody is on the same page, make sure to take a look at the below documentation. 2 | 3 | ### Using `yarn` 4 | 5 | As previously mentioned, the use of the [`yarn`](https://yarnpkg.com) package manager is **strongly** recommended, as opposed to `npm`. Aside from a more stable dependency tree, one other major benefit to using `yarn` is the ability to [*clean*](https://yarnpkg.com/en/docs/cli/clean) your `node_modules` to help eliminate redudant files that will help reduce your application's final build size. 6 | 7 | ### package.json 8 | #### `dependencies` 9 | 10 | These dependencies **will** be included in your final production application. If your application needs a certain module to function, then install it here! 11 | 12 | #### `devDependencies` 13 | These dependecies **will not** be included in your final production application. Here you can install modules needed specifically for development, like build scripts, task runners, `webpack` accessories, etc. 14 | 15 | ### Installing Native Node Modules 16 | 17 | When using native node modules, those written in C/C++, we need to ensure they are built against `electron`'s packaged `node` version. We can use [`electron-builder`](https://www.electron.build/cli)'s `install-app-deps` command to rebuild those modules to resolve any conflicts we might run into. 18 | 19 | ##### Running `install-app-deps` 20 | ```bash 21 | yarn add electron-builder --dev 22 | ./node_modules/.bin/electron-builder install-app-deps 23 | ``` 24 | 25 | It may also be worth adding a `package.json` script for this command if you plan on using it often (`yarn rebuild-deps`). 26 | 27 | ```json tab="package.json" 28 | { 29 | "scripts": { 30 | "rebuild-deps": "electron-builder install-app-deps" 31 | } 32 | } 33 | ``` 34 | 35 | If you choose not to use `electron-builder` as your build tool, you can still run the command using `npx` without side affects. 36 | 37 | ```bash 38 | # using `npm@^5.2.0` 39 | npx electron-builder install-app-deps 40 | ``` 41 | 42 | #### Final Notes 43 | If you do expect your application to use native node modules, it is **highly recommended** to use [`electron-builder`](https://github.com/electron-userland/electron-builder) for your build tool as it handles these conflicts for you during the build step. 44 | -------------------------------------------------------------------------------- /docs/en/environment-variables.md: -------------------------------------------------------------------------------- 1 | Sometimes you need your `webpack` build to be dependent upon specific environment variables. Whether you need to set a global API Url or even the application name, here's how you can set variables that `electron-webpack` will provide for your application. 2 | 3 | ### `ELECTRON_WEBPACK_APP_*` 4 | 5 | If you want `electron-webpack` to provide environment variables during the build process, use the `ELECTRON_WEBPACK_APP_*` namespace and they will be injected. 6 | 7 | #### Use Case 8 | 9 | Let's say our application uses a CI server to run tests, and we also want that build to use our development API. We can set an environment variable to define the base Url that [`axios`](https://www.npmjs.com/package/axios) can use. 10 | 11 | ##### Set our environment variable 12 | ```bash 13 | # linux and darwin systems 14 | ELECTRON_WEBPACK_APP_API_URL="http://dev.domain.com/api" 15 | 16 | # win32 systems 17 | set ELECTRON_WEBPACK_APP_API_URL="http://dev.domain.com/api" 18 | ``` 19 | 20 | ##### Reference that variable in our code 21 | ```js 22 | import axios from 'axios' 23 | 24 | axios.defaults.baseURL = process.env.ELECTRON_WEBPACK_APP_API_URL 25 | ``` 26 | 27 | ### `ELECTRON_ARGS` 28 | 29 | If you need to provide arguments to the `electron` process itself during development, you can do so using the `ELECTRON_ARGS` environment variable. 30 | 31 | 32 | ##### From the CLI 33 | 34 | When specifying `ELECTRON_ARGS` from the CLI, please note that it will be parsed as a JSON string of an array of values. Thus, you will have to wrap the value in square brackets, and on unix systems escape characters like double quotes. 35 | For example, if in package.json you have a script `"dev": "electron-webpack dev"`, you can invoke it with the `--inspect-brk` argument like this: 36 | 37 | ``` 38 | # linux and darwin systems, git bash on windows 39 | ELECTRON_ARGS=[\"--inspect-brk=9229\"] yarn dev 40 | 41 | # win32 systems, cmd 42 | set ELECTRON_ARGS=["--inspect-brk=9229"]&&yarn dev 43 | ``` 44 | 45 | ##### From an `.env` file 46 | 47 | Since `electron-webpack` uses the [dotenv](https://www.npmjs.com/package/dotenv) library under the hood, instead of wrestling with CLI arguments, escaping and differences between Windows and Unix systems, you can instead create an `.env` file and specify environment variables: 48 | 49 | ```bash tab=".env.development" 50 | ELECTRON_ARGS=["--inspect-brk=9229"] 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/en/building.md: -------------------------------------------------------------------------------- 1 | # Building 2 | 3 | One important thing to understand is that `electron-webpack` is agnostic about the build tool you decide to use for creating your distributable `electron` application. The only concern of this module is to create a `webpack` output that functions properly and is optimized for the `electron` environment. 4 | 5 | ### Compiling your code 6 | 7 | Once you have `electron-webpack` configured for your project, you can simply run the `electron-webpack` command to compile your source code. To make things easier, make use of setting up a script in your `package.json`. 8 | 9 | ```json tab="package.json" 10 | { 11 | "scripts": { 12 | "compile": "electron-webpack" 13 | } 14 | } 15 | ``` 16 | 17 | After running the above script with `yarn compile`, you now have a `webpack` output located in the `dist/` directory. 18 | 19 | ### Building a distribution 20 | 21 | Now that `electron-webpack` has created your `webpack` bundle, you can simply implement any build tool you would like. One thing to note is that additional optimizations have been made to work with [`electron-builder`](https://github.com/electron-userland/electron-builder). This build tool is perfect for any sized application, providing many features from creating installable executables to providing "auto update" support. `electron-webpack` also interally provides a base configuration for [`electron-builder`](https://github.com/electron-userland/electron-builder). 22 | 23 | #### Using `electron-builder` 24 | 25 | ```bash 26 | yarn add electron-builder --dev 27 | ``` 28 | 29 | ##### Add additional `package.json` scripts 30 | ```json 31 | { 32 | "scripts": { 33 | /* compile source code and create webpack output */ 34 | "compile": "electron-webpack", 35 | 36 | /* `yarn compile` & create distribution */ 37 | "dist": "yarn compile && electron-builder", 38 | 39 | /* `yarn dist` & create unpacked distribution */ 40 | "dist:dir": "yarn dist -- --dir -c.compression=store -c.mac.identity=null" 41 | } 42 | } 43 | ``` 44 | 45 | Further configurations can be made in accordance to [`electron-builder`'s documentation](https://www.electron.build/). 46 | 47 | ### Final Notes 48 | 49 | When configuring your build tool of choice, just be sure to point to the `dist/` directory where your compiled application lives (this is already the default of `electron-builder`). Any questions or issues that may arise during the build step should be directed towards the respected build tool, and not `electron-webpack`. 50 | -------------------------------------------------------------------------------- /docs/en/technical-differences-of-electron-compile.md: -------------------------------------------------------------------------------- 1 | # Technical Differences of `electron-compile` 2 | > Electron supporting package to compile JS and CSS in Electron applications 3 | 4 | `electron-compile` is a great package that offers a *zero configuration* setup without a predefine project structure. It surely has its place in the community, but just isn't comparable to `webpack` in terms of flexibility, development experience, and community extensions. Below are a few topics on why `electron-webpack` can be considerably better. 5 | 6 | ### Hot Module Replacement 7 | `electron-webpack` provides HMR for virtually all code in both `main` and `renderer` processes. `electron-compile` is currently limited to [live reloading](https://github.com/electron/electron-compile#live-reload--hot-module-reloading), which is enough for certain enviroments, but works for only a handful of file types. If you've worked with HMR before, then you definitly know it is something you can not live without afterwards. 8 | 9 | ### Runtime Dependencies 10 | Since `electron-compile` "compiles JS and CSS on the fly", it currently adds extra dependencies into the scope of your project. Sure, a small handful of modules isn't a big deal, but keeping production size down in the end is **always** important when distributing `electron` applications. 11 | 12 | ### Faster Build Times 13 | Internally, `electron-webpack` takes advantage of [`happypack`](https://github.com/amireh/happypack) to create multiple worker *threads*. Combined with Dll support, build times can be [significantly faster](https://github.com/amireh/happypack#benchmarks). It even has some additional optimizations for TypeScript users. And let's be honest, especially for large-scale applications, who doesn't want faster build times? 14 | 15 | ### Webpack Community 16 | When using `electron-compile` you are limited to a specific set of features. Sure you have most of the important ones covered, you just don't get an amazing and large community that `webpack` can provide. There are countless loaders and plugins to cover just about everything you may ever need. Not only is the community constantly implementing new tools, `electron` is explicitly support by the `webpack` team. 17 | 18 | In the end, `electron-compile` is still a fantastic tool when you need to quickly prototype a project together. It can even be enough for smaller scale applications that may not need all the fancy bells and whistles. But for those accustomed to HMR or developing larger scale applications, `electron-webpack` has you covered. 19 | -------------------------------------------------------------------------------- /docs/en/dll-bundle-splitting.md: -------------------------------------------------------------------------------- 1 | One great feature of using `webpack` is being able to take advantage of the `webpack.DllPlugin` plugin. If you are unfamiliar with the `webpack.DllPlugin` plugin, make sure to read its [documentation](https://webpack.js.org/plugins/dll-plugin/) to better understand what problem it attempts to solve. In short, this can be used to [drastically improve](https://robertknight.github.io/posts/webpack-dll-plugins/) build time performance. 2 | 3 | Through custom configurations `electron-webpack` provides a place to define specific packages you would like to create a separate bundle for. You can treat this feature as a way to bundle all of your *vendor* libraries together. 4 | 5 | Once you have defined your modules, you can simply run `electron-webpack dll` to produce a `vendor` bundle that will be referenced in your final compilation. Let's go through that step-by-step. 6 | 7 | ### Define your modules 8 | Here we are defining our modules in `package.json`. If you prefer, you can also create a separate file for your configurations ([more info](./configuration.md)). 9 | ```json 10 | { 11 | "electronWebpack": { 12 | "renderer": { 13 | "dll": [ 14 | "fooModule", 15 | "barModule" 16 | ] 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | ### Create the `vendor` bundle 23 | Once you have choosen your selected modules, you can run the below command to create your `vendor` bundle. 24 | 25 | ```bash 26 | electron-webpack dll 27 | ``` 28 | 29 | It may be beneficial to create a `package.json` script for this command if you plan to use it often. 30 | 31 | ```json 32 | { 33 | "scripts": { 34 | "compile:dll": "electron-webpack dll" 35 | } 36 | } 37 | ``` 38 | 39 | ## Final Notes 40 | Now that your `vendor` bundle is created, you can move forward with the standard `electron-webpack` compilation process. 41 | 42 | Please know that the `vendor` bundle is only created when you explicity run `electron-webpack dll`. For most cases, you may only need to run this one time. But as you add/remove modules or even update those modules, you will need to re-run the command to create an updated bundle. 43 | 44 | #### Using a `postinstall` hook 45 | One common practice when using Dll bundle splitting to define a `postinstall` command to create the `vendor` bundle whenever new dependecies are installed. This can help ensure everything is up to date so you don't have to manually run this yourself. 46 | 47 | ```json tab="package.json" 48 | { 49 | "scripts": { 50 | "postinstall": "electron-webpack dll" 51 | } 52 | } 53 | ``` 54 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/configurators/ts.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path" 2 | import { WebpackConfigurator } from "../main" 3 | import { getFirstExistingFile } from "../util" 4 | 5 | export async function configureTypescript(configurator: WebpackConfigurator) { 6 | const hasTsChecker = configurator.hasDevDependency("fork-ts-checker-webpack-plugin") || configurator.hasDevDependency("electron-webpack-ts") 7 | if (!(hasTsChecker || configurator.hasDevDependency("ts-loader"))) { 8 | return 9 | } 10 | 11 | // add after js 12 | configurator.extensions.splice(1, 0, ".ts", ".tsx") 13 | 14 | const isTranspileOnly = configurator.isTest || (hasTsChecker && !configurator.isProduction) 15 | 16 | const tsConfigFile = await getFirstExistingFile([path.join(configurator.sourceDir, "tsconfig.json"), path.join(configurator.projectDir, "tsconfig.json")], null) 17 | // check to produce clear error message if no tsconfig.json 18 | if (tsConfigFile == null) { 19 | throw new Error(`Please create tsconfig.json in the "${configurator.projectDir}":\n\n{\n "extends": "./node_modules/electron-webpack/tsconfig-base.json"\n}\n\n`) 20 | } 21 | 22 | if (configurator.debug.enabled) { 23 | configurator.debug(`Using ${tsConfigFile}`) 24 | } 25 | 26 | // no sense to use fork-ts-checker-webpack-plugin for production build 27 | if (isTranspileOnly && !configurator.isTest) { 28 | const hasVue = configurator.hasDependency("vue") 29 | const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin") 30 | configurator.plugins.push(new ForkTsCheckerWebpackPlugin({ 31 | tsconfig: tsConfigFile, 32 | logger: configurator.env.forkTsCheckerLogger || { 33 | info: () => { 34 | // ignore 35 | }, 36 | 37 | warn: console.warn.bind(console), 38 | error: console.error.bind(console), 39 | }, 40 | vue: hasVue 41 | })) 42 | } 43 | 44 | const tsLoaderOptions: any = { 45 | // use transpileOnly mode to speed-up compilation 46 | // in the test mode also, because checked during dev or production build 47 | transpileOnly: isTranspileOnly, 48 | appendTsSuffixTo: [/\.vue$/], 49 | configFile: tsConfigFile, 50 | } 51 | 52 | if (configurator.debug.enabled) { 53 | configurator.debug(`ts-loader options: ${JSON.stringify(tsLoaderOptions, null, 2)}`) 54 | } 55 | 56 | configurator.rules.push({ 57 | test: /\.tsx?$/, 58 | exclude: /node_modules/, 59 | use: [ 60 | { 61 | loader: "ts-loader", 62 | options: tsLoaderOptions 63 | }, 64 | ], 65 | }) 66 | } -------------------------------------------------------------------------------- /docs/en/project-structure.md: -------------------------------------------------------------------------------- 1 | Everybody likes to structure their projects differently, and `electron-webpack` is aware of that. For the most part, the only configurations needed are [entry points](https://webpack.js.org/concepts/entry-points/) for `webpack` to step in. 2 | 3 | If you are looking to follow common electron practices or wanting an example template, make sure to check out [`electron-webpack-quick-start`](https://github.com/electron-userland/electron-webpack-quick-start). It has the bare minimun setup, but can be easily modified to meet many needs. The use of this template, of course, is optional. 4 | 5 | ### Default Structure 6 | Below is the default file tree that `electron-webpack` expects without any custom configuration. Please know that source directories can be adjusted to meet your needs ([more info](./configuration.md#source-directories)). 7 | 8 | ``` 9 | my-project/ 10 | ├─ src/ 11 | │ ├─ main/ 12 | │ │ └─ index.js 13 | │ ├─ renderer/ 14 | │ │ └─ index.js 15 | │ └─ common/ 16 | └─ static/ 17 | ``` 18 | 19 | ##### Main Process (`src/main/`) 20 | Here you can add all of your `main` process code. 21 | 22 | ##### Renderer Process (`src/renderer/`) [optional] 23 | Here you can add all of your `renderer` process code. Bundling of the `renderer` process is optional for cases where you may want to use an external tool such as [`electron-next`](https://github.com/leo/electron-next). Notice that there isn't a entry `index.html`, that's because it is created for you ([more info](./development.md#use-of-html-webpack-plugin)). 24 | 25 | ##### Common Scripts (`src/common/`) [optional] 26 | This is a convenient place where you can place utility type files that you expect to use between both processes. Thanks to `webpack` aliasing, you can easily import files from here using the `common` alias. 27 | 28 | ##### Static Assets (`static/`) [optional] 29 | There are some instances were we may not want `webpack` to bundle particular assets, like those being consumed by modules like `fs`. Here is where we can put them and then reliably access them in both development and production ([more info](./using-static-assets.md)). 30 | 31 | ### `webpack` entry points 32 | One important thing to notice is that `electron-webpack` expects that your `main` process and `renderer` process source code is separated using different directories. 33 | Within each process's directory, one of the following entry files is expected... 34 | 35 | * `index.js` / `main.js` 36 | * `index.ts` / `main.ts` (when using the [TypeScript](./add-ons.md#typescript) add-on) 37 | 38 | The [entry files](https://webpack.js.org/concepts/entry-points/) are the main starting point of your `webpack` bundle, so everything found within its dependency tree will be bundled. 39 | -------------------------------------------------------------------------------- /test/src/test.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto" 2 | import { mkdir, move, outputJson, writeFile } from "fs-extra" 3 | import * as path from "path" 4 | import { doTest, getMutableProjectDir, rootDir, testWebpack, tmpDir } from "./helpers/helper" 5 | 6 | afterEach(() => tmpDir.cleanup()) 7 | 8 | test("app", () => doTest("webpack.app.config.js")) 9 | 10 | test("main production", () => doTest("webpack.main.config.js")) 11 | 12 | test("renderer custom sourceDirectory", async () => { 13 | const projectDir = await getMutableProjectDir() 14 | await move(path.join(projectDir, "src/renderer"), path.join(projectDir, "customRenderer")) 15 | const configuration = await require("electron-webpack/webpack.renderer.config.js")({production: true, configuration: { 16 | projectDir, 17 | renderer: { 18 | sourceDirectory: "customRenderer" 19 | }, 20 | }}) 21 | await testWebpack(configuration, projectDir) 22 | }) 23 | 24 | test("main extra entry point and custom source dir", async () => { 25 | const projectDir = await getMutableProjectDir() 26 | await move(path.join(projectDir, "src/main"), path.join(projectDir, "customMain")) 27 | const configuration = await require("electron-webpack/webpack.main.config.js")({production: true, configuration: { 28 | projectDir, 29 | main: { 30 | extraEntries: ["@/foo.js"], 31 | sourceDirectory: "customMain" 32 | }, 33 | }}) 34 | await testWebpack(configuration, projectDir) 35 | }) 36 | 37 | test("renderer production", async () => { 38 | const projectDir = await getMutableProjectDir() 39 | 40 | function createTestAsset(dirName: string, fileExt = "png") { 41 | const dir = path.join(projectDir, dirName) 42 | return mkdir(dir).then(() => writeFile(path.join(dir, `foo.${fileExt}`), randomBytes(100 * 1024))) 43 | } 44 | 45 | // size of file must be greater than url-loader limit 46 | await Promise.all([ 47 | createTestAsset("a"), 48 | createTestAsset("b"), 49 | createTestAsset("static", "txt"), 50 | ]) 51 | 52 | const configuration = await require("electron-webpack/webpack.renderer.config.js")({configuration: {projectDir}, production: true}) 53 | await testWebpack(configuration, projectDir) 54 | }) 55 | 56 | test("typescript", async () => { 57 | const projectDir = await getMutableProjectDir("typescript") 58 | // noinspection ReservedWordAsName 59 | await outputJson(path.join(projectDir, "tsconfig.json"), { 60 | extends: rootDir.replace(/\\/g, "/") + "/packages/electron-webpack/tsconfig-base.json", 61 | compilerOptions: { 62 | baseUrl: "src", 63 | }, 64 | }) 65 | const configuration = await require("electron-webpack/webpack.main.config.js")({production: true, configuration: { 66 | projectDir, 67 | }}) 68 | await testWebpack(configuration, projectDir) 69 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-webpack", 3 | "private": true, 4 | "license": "MIT", 5 | "author": "Vladimir Krivosheev ", 6 | "scripts": { 7 | "compile": "cross-env ts-babel packages/electron-webpack test && yarn schema", 8 | "lint": "tslint -c ./node_modules/electron-builder-tslint-config/tslint.json -p packages/electron-webpack --exclude \"**/*.js\"", 9 | "release": "BABEL_ENV=production yarn compile && ./npm-publish.sh && conventional-changelog -p angular -i CHANGELOG.md -s", 10 | "test": "yarn compile && yarn lint && jest", 11 | "serve-docs": "mkdocs serve", 12 | "deploy-docs": "mkdocs build --clean && netlifyctl deploy --publish-directory site", 13 | "schema": "ts-json-schema-generator --path \"packages/electron-webpack/src/**/*.ts\" --no-top-ref --no-type-check --type ElectronWebpackConfiguration --expose export > packages/electron-webpack/scheme.json", 14 | "//": "update-deps task intended only for core maintainers", 15 | "update-deps": "npm-check-updates -u && npx lerna exec -- npm-check-updates -u" 16 | }, 17 | "devDependencies": { 18 | "@types/bluebird": "^3.5.30", 19 | "@types/electron-devtools-installer": "^2.2.0", 20 | "@types/fs-extra": "^8.1.0", 21 | "@types/jest": "^25.2.1", 22 | "@types/memory-fs": "^0.3.2", 23 | "@types/node": "^13.11.0", 24 | "@types/semver": "^7.1.0", 25 | "@types/webpack": "^4.41.10", 26 | "@types/webpack-merge": "^4.1.5", 27 | "babel-preset-jest": "^25.2.6", 28 | "babel-preset-ts-node8": "^4.0.3", 29 | "cross-env": "^7.0.2", 30 | "electron-builder-tslint-config": "^1.1.0", 31 | "fs-extra": "^9.0.0", 32 | "jest-cli": "^25.2.7", 33 | "jest-junit": "^10.0.0", 34 | "memory-fs": "^0.5.0", 35 | "temp-file": "^3.3.7", 36 | "terser-webpack-plugin": "^2.3.5", 37 | "ts-babel": "^6.1.7", 38 | "ts-json-schema-generator": "^0.65.0", 39 | "tslint": "^6.1.1", 40 | "typescript": "^3.8.3", 41 | "webpack": "^4.42.1" 42 | }, 43 | "workspaces": [ 44 | "packages/*" 45 | ], 46 | "resolutions": { 47 | "js-yaml": "^3.13.1" 48 | }, 49 | "jest": { 50 | "globals": { 51 | "ts-jest": { 52 | "tsConfigFile": "test/tsconfig.json" 53 | } 54 | }, 55 | "transform": { 56 | "^.+\\.js$": "/test/babel-jest.js" 57 | }, 58 | "moduleFileExtensions": [ 59 | "ts", 60 | "tsx", 61 | "js", 62 | "json" 63 | ], 64 | "testRegex": "(/test/out/.*|\\.(test|spec))\\.(ts|tsx|js)$", 65 | "testEnvironment": "node", 66 | "testPathIgnorePatterns": [ 67 | "[\\/]{1}helpers[\\/]{1}" 68 | ], 69 | "roots": [ 70 | "test/out" 71 | ], 72 | "modulePaths": [ 73 | "/packages/electron-webpack/node_modules", 74 | "/packages" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/electron-main-hmr/HmrClient.ts: -------------------------------------------------------------------------------- 1 | import Crocket from "crocket" 2 | 3 | const debug = require("debug")("electron-webpack:main-client-hmr") 4 | 5 | interface BuiltMessage { 6 | hash: string 7 | } 8 | 9 | export class HmrClient { 10 | lastHash: string | null = null 11 | 12 | private readonly ipc = new Crocket() 13 | 14 | constructor(socketPath: string, private hot: __WebpackModuleApi.Hot, private readonly currentHashGetter: () => string) { 15 | if (hot == null) { 16 | throw new Error(`[HMR] Hot Module Replacement is disabled.`) 17 | } 18 | 19 | this.ipc.connect({path: socketPath}, error => { 20 | if (error != null) { 21 | console.error(error.stack || error.toString()) 22 | } 23 | if (debug.enabled) { 24 | debug(`Connected to server (${socketPath})`) 25 | } 26 | }) 27 | 28 | this.ipc.on("error", error => { 29 | console.error(error.stack || error.toString()) 30 | }) 31 | 32 | this.ipc.on("/built", data => { 33 | this.lastHash = data.hash 34 | if (this.isUpToDate()) { 35 | if (debug.enabled) { 36 | debug(`Up to date, hash ${data.hash}`) 37 | } 38 | return 39 | } 40 | 41 | const status = hot.status() 42 | if (status === "idle") { 43 | this.check() 44 | } 45 | else if (status === "abort" || status === "fail") { 46 | console.warn(`[HMR] Cannot apply update as a previous update ${status}ed. Need to do a full reload!`) 47 | } 48 | else if (debug.enabled) { 49 | debug(`Cannot check changes, status ${status}`) 50 | } 51 | }) 52 | } 53 | 54 | private isUpToDate() { 55 | return this.lastHash === this.currentHashGetter() 56 | } 57 | 58 | private check() { 59 | this.hot.check(true) 60 | .then(outdatedModules => { 61 | if (outdatedModules == null) { 62 | console.warn(`[HMR] Cannot find update. Need to do a full reload!`) 63 | console.warn(`[HMR] (Probably because of restarting the webpack-dev-server)`) 64 | return 65 | } 66 | 67 | require("webpack/hot/log-apply-result")(outdatedModules, outdatedModules) 68 | 69 | if (this.isUpToDate()) { 70 | console.log(`[HMR] App is up to date.`) 71 | } 72 | }) 73 | .catch(error => { 74 | const status = this.hot.status() 75 | if (status === "abort" || status === "fail") { 76 | console.warn(`[HMR] ${error.stack || error.toString()}`) 77 | console.warn("[HMR] Cannot apply update. Need to do a full reload - application will be restarted") 78 | require("electron").app.exit(100) 79 | } 80 | else { 81 | console.warn(`[HMR] Update failed: ${error.stack || error.message}`) 82 | } 83 | }) 84 | } 85 | } -------------------------------------------------------------------------------- /packages/electron-webpack/src/dev/devUtil.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { ChildProcess } from "child_process" 3 | 4 | export interface LineFilter { 5 | filter(line: string): boolean 6 | } 7 | 8 | function filterText(s: string, lineFilter: LineFilter | null) { 9 | const lines = s 10 | .trim() 11 | .split(/\r?\n/) 12 | .filter(it => { 13 | if (lineFilter != null && !lineFilter.filter(it)) { 14 | return false 15 | } 16 | 17 | // https://github.com/electron/electron/issues/4420 18 | // this warning can be safely ignored 19 | if (it.includes("Couldn't set selectedTextBackgroundColor from default ()")) { 20 | return false 21 | } 22 | if (it.includes("Use NSWindow's -titlebarAppearsTransparent=YES instead.")) { 23 | return false 24 | } 25 | return !it.includes("Warning: This is an experimental feature and could change at any time.") 26 | && !it.includes("No type errors found") 27 | && !it.includes("webpack: Compiled successfully.") 28 | }) 29 | 30 | if (lines.length === 0) { 31 | return null 32 | } 33 | return " " + lines.join(`\n `) + "\n" 34 | } 35 | 36 | export function logProcessErrorOutput(label: "Electron" | "Renderer" | "Main", childProcess: ChildProcess) { 37 | childProcess.stderr!!.on("data", data => { 38 | logProcess(label, data.toString(), chalk.red) 39 | }) 40 | } 41 | 42 | export function logError(label: "Electron" | "Renderer" | "Main", error: Error) { 43 | logProcess(label, error.stack || error.toString(), chalk.red) 44 | } 45 | 46 | const LABEL_LENGTH = 28 47 | 48 | export function logProcess(label: "Electron" | "Renderer" | "Main", data: string | Buffer, labelColor: any, lineFilter: LineFilter | null = null) { 49 | const log = filterText(data.toString(), lineFilter) 50 | if (log == null || log.length === 0) { 51 | return 52 | } 53 | 54 | process.stdout.write( 55 | labelColor.bold(`┏ ${label} ${"-".repeat(LABEL_LENGTH - label.length - 1)}`) + 56 | "\n\n" + log + "\n" + 57 | labelColor.bold(`┗ ${"-".repeat(LABEL_LENGTH)}`) + 58 | "\n" 59 | ) 60 | } 61 | 62 | export class DelayedFunction { 63 | private readonly executor: () => void 64 | private handle: NodeJS.Timer | null = null 65 | 66 | constructor(executor: () => void) { 67 | this.executor = () => { 68 | this.handle = null 69 | executor() 70 | } 71 | } 72 | 73 | schedule() { 74 | this.cancel() 75 | this.handle = setTimeout(this.executor, 5000) 76 | } 77 | 78 | cancel() { 79 | const handle = this.handle 80 | if (handle != null) { 81 | this.handle = null 82 | clearTimeout(handle) 83 | } 84 | } 85 | } 86 | 87 | export function getCommonEnv() { 88 | return { 89 | ...process.env, 90 | NODE_ENV: "development", 91 | // to force debug colors in the child process 92 | DEBUG_COLORS: true, 93 | DEBUG_FD: "1", 94 | } 95 | } -------------------------------------------------------------------------------- /packages/electron-webpack/src/dev/ChildProcessManager.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn, SpawnOptions } from "child_process" 2 | import * as path from "path" 3 | 4 | const debug = require("debug")("electron-webpack") 5 | 6 | export function run(program: string, args: Array, options: SpawnOptions) { 7 | const isWin = process.platform === "win32" 8 | return spawn(isWin ? path.join(__dirname, "../../vendor/runnerw.exe") : program, isWin ? [program].concat(args) : args, options) 9 | } 10 | 11 | export class ChildProcessManager { 12 | // noinspection TypeScriptFieldCanBeMadeReadonly 13 | private mainProcessExitCleanupCallback: (() => void) | null = null 14 | // noinspection TypeScriptFieldCanBeMadeReadonly 15 | private child: ChildProcess | null 16 | 17 | constructor(child: ChildProcess, debugLabel: string, promiseNotifier: PromiseNotifier | null) { 18 | this.child = child 19 | 20 | require("async-exit-hook")((callback: () => void) => { 21 | this.mainProcessExitCleanupCallback = callback 22 | const child = this.child 23 | if (child == null) { 24 | return 25 | } 26 | 27 | this.child = null 28 | 29 | if (promiseNotifier != null) { 30 | promiseNotifier.resolve() 31 | } 32 | 33 | if (debug.enabled) { 34 | debug(`Send SIGINT to ${debugLabel}`) 35 | } 36 | 37 | if (process.platform === "win32") { 38 | child.stdin!!.end(Buffer.from([5, 5])) 39 | } 40 | else { 41 | child.kill("SIGINT") 42 | } 43 | }) 44 | 45 | child.on("close", code => { 46 | const mainProcessExitCleanupCallback = this.mainProcessExitCleanupCallback 47 | if (mainProcessExitCleanupCallback != null) { 48 | this.mainProcessExitCleanupCallback = null 49 | mainProcessExitCleanupCallback() 50 | } 51 | 52 | const child = this.child 53 | if (child == null) { 54 | return 55 | } 56 | 57 | this.child = null 58 | 59 | const message = `${debugLabel} exited with code ${code}` 60 | 61 | if (promiseNotifier != null) { 62 | promiseNotifier.reject(new Error(message)) 63 | } 64 | 65 | if (code === 0) { 66 | if (debug.enabled) { 67 | debug(message) 68 | // otherwise no newline in the terminal 69 | process.stderr.write("\n") 70 | } 71 | } 72 | else { 73 | process.stderr.write(`${message}\n`) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | export class PromiseNotifier { 80 | constructor(private _resolve: (() => void) | null, private _reject: ((error: Error) => void) | null) { 81 | } 82 | 83 | resolve() { 84 | const r = this._resolve 85 | if (r != null) { 86 | this._resolve = null 87 | r() 88 | } 89 | } 90 | 91 | reject(error: Error) { 92 | if (this._resolve != null) { 93 | this._resolve = null 94 | } 95 | 96 | const _reject = this._reject 97 | if (_reject != null) { 98 | this._reject = null 99 | _reject(error) 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /packages/electron-webpack/src/plugins/WatchMatchPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Compiler } from "webpack" 2 | 3 | export class WatchFilterPlugin { 4 | constructor(private readonly filter: WatchFileSystemFilter, private readonly debug: any) { 5 | } 6 | 7 | apply(compiler: Compiler) { 8 | compiler.hooks.afterEnvironment.tap("WatchFilterPlugin", () => { 9 | (compiler as any).watchFileSystem = new IgnoringWatchFileSystem((compiler as any).watchFileSystem, this.filter, this.debug) 10 | }) 11 | } 12 | } 13 | 14 | interface WatchFileSystem { 15 | watch(files: Array, dirs: Array, missing: Array, startTime: number, options: any, callback: WatchFileSystemCallback, callbackUndelayed: () => void): void 16 | } 17 | 18 | // include or not 19 | export type WatchFileSystemFilter = (file: string) => boolean 20 | 21 | export type WatchFileSystemCallback = (error: Error | null, filesModified?: Array, dirsModified?: Array, missingModified?: Array, fileTimestamps?: { [key: string]: number }, dirTimestamps?: { [key: string]: number }) => void 22 | 23 | class IgnoringWatchFileSystem { 24 | constructor(private readonly wfs: WatchFileSystem, private readonly filter: WatchFileSystemFilter, private readonly debug: any) { 25 | } 26 | 27 | watch(files: Array, dirs: Array, missing: Array, startTime: number, options: any, callback: WatchFileSystemCallback, callbackUndelayed: () => void) { 28 | const includedFiles: Array = [] 29 | const includedDirs: Array = [] 30 | const excludedFiles: Array = [] 31 | const excludedDirs: Array = [] 32 | separate(this.filter, files, includedFiles, excludedFiles) 33 | separate(this.filter, dirs, includedDirs, excludedDirs) 34 | 35 | if (this.debug.enabled) { 36 | this.debug(`files:${stringifyList(files)}\ndirs:${stringifyList(dirs)}\nmissing:${stringifyList(missing)}`) 37 | this.debug(`includedFiles:${stringifyList(includedFiles)}\nincludedDirs:${stringifyList(includedDirs)}\nexcludedFiles:${stringifyList(excludedFiles)}\nexcludedDirs:${stringifyList(excludedDirs)}`) 38 | } 39 | 40 | return this.wfs.watch(includedFiles, includedDirs, missing, startTime, options, (error, filesModified, dirsModified, missingModified, fileTimestamps, dirTimestamps) => { 41 | if (error != null) { 42 | callback(error) 43 | return 44 | } 45 | 46 | for (const p of excludedFiles) { 47 | fileTimestamps![p] = 1 48 | } 49 | 50 | for (const p of excludedDirs) { 51 | dirTimestamps![p] = 1 52 | } 53 | 54 | callback(null, filesModified, dirsModified, missingModified, fileTimestamps, dirTimestamps) 55 | }, callbackUndelayed) 56 | } 57 | } 58 | 59 | function separate(filter: WatchFileSystemFilter, list: Array, included: Array, excluded: Array) { 60 | for (const file of list) { 61 | (filter(file) ? included : excluded).push(file) 62 | } 63 | } 64 | 65 | function stringifyList(list: Array) { 66 | return `\n ${list.map(it => it.startsWith(process.cwd()) ? it.substring(process.cwd().length + 1) : it).join(",\n ")}` 67 | } -------------------------------------------------------------------------------- /docs/en/index.md: -------------------------------------------------------------------------------- 1 | # electron-webpack [![npm version](https://img.shields.io/npm/v/electron-webpack.svg)](https://npmjs.org/package/electron-webpack) 2 | 3 | > Because setting up `webpack` in the `electron` environment shouldn't be difficult. 4 | 5 | ## Overview 6 | Modern web development practices today require a lot of setup with things like `webpack` to bundle your code, `babel` for transpiling, `eslint` for linting, and so much more that the list just goes on. Unfortunately when creating `electron` applications, all of that setup just became much more difficult. The primary aim of `electron-webpack` is to eliminate all preliminary setup with one simple install so you can get back to developing your application. 7 | 8 | > Why create a module and not a full boilerplate? 9 | 10 | If you've been in the JavaScript world for even a short period of time, you are very aware that things are always changing, and development setup is no exclusion. Putting all development scripts into a single **updatable** module just makes sense. Sure a full featured boilerplate works too, but doing also involves needing to manually update those pesky `webpack` configuration files that some may call *magic* when something new comes out. 11 | 12 | Here are some of the awesome features you'll find using `electron-webpack`: 13 | 14 | * Detailed [documentation](https://webpack.electron.build) 15 | * Use of [`webpack`](https://webpack.js.org/) for source code bundling 16 | * Use of [`webpack-dev-server`](https://github.com/webpack/webpack-dev-server) for development 17 | * HMR for both `renderer` and `main` processes 18 | * Use of [`@babel/preset-env`](https://github.com/babel/babel/tree/master/packages/babel-preset-env) that is automatically configured based on your `electron` version 19 | * Ability to add custom `webpack` loaders, plugins, etc. 20 | * [Add-ons](./add-ons.md) to support items like [TypeScript](http://www.typescriptlang.org/), [Less](http://lesscss.org/), [EJS](http://www.embeddedjs.com/), etc. 21 | 22 | ## Quick Start 23 | Get started fast with [electron-webpack-quick-start](https://github.com/electron-userland/electron-webpack-quick-start). 24 | ```bash 25 | # create a directory of your choice, and copy template using curl 26 | mkdir my-project && cd my-project 27 | curl -fsSL https://github.com/electron-userland/electron-webpack-quick-start/archive/master.tar.gz | tar -xz --strip-components 1 28 | 29 | # or copy template using git clone 30 | git clone https://github.com/electron-userland/electron-webpack-quick-start.git 31 | cd electron-webpack-quick-start 32 | rm -rf .git 33 | 34 | # install dependencies 35 | yarn 36 | ``` 37 | 38 | If you already have an existing project, or are looking for a custom approach outside of the quick start template, make sure to read over the [Core Concepts](./core-concepts.md), [Project Structure](./project-structure.md), and [Development](./development.md) sections of `electron-webpack`'s documentation. 39 | 40 | ### Next Steps 41 | Make sure to take advantage of the detailed [documentation](https://webpack.electron.build) that `electron-webpack` provides. It covers everything from how things work internally, adding custom configurations, and building your application. 42 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 77 | -------------------------------------------------------------------------------- /test/babel-jest.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | let babel 4 | const crypto = require("crypto") 5 | const fs = require("fs") 6 | const jestPreset = require("babel-preset-jest") 7 | const path = require("path") 8 | 9 | const convert = require("convert-source-map") 10 | 11 | let babelRc; 12 | 13 | function getBabelRcDigest() { 14 | if (babelRc == null) { 15 | let configFile 16 | try { 17 | configFile = fs.readFileSync(path.join(__dirname, ".babelrc")) 18 | } 19 | catch (e) { 20 | configFile = fs.readFileSync(path.join(__dirname, "..", ".babelrc")) 21 | } 22 | 23 | babelRc = crypto 24 | .createHash("md5") 25 | .update(configFile) 26 | .digest("base64") 27 | } 28 | return babelRc; 29 | } 30 | 31 | // compiled by ts-babel - do not transform 32 | function isFullyCompiled(fileData) { 33 | return fileData.startsWith(`"use strict";`) && fileData.includes("var _") 34 | } 35 | 36 | function createTransformer(options) { 37 | options = Object.assign({}, options, { 38 | presets: (options && options.presets || []).concat([jestPreset]), 39 | }) 40 | 41 | delete options.cacheDirectory 42 | 43 | return { 44 | canInstrument: true, 45 | getCacheKey(fileData, filename, configString, _ref2) { 46 | return crypto.createHash("md5") 47 | .update(fileData) 48 | .update(isFullyCompiled(fileData) ? "f": "p") 49 | .update(configString) 50 | .update(getBabelRcDigest()) 51 | .update(_ref2.instrument ? "instrument" : "") 52 | .digest("hex") 53 | }, 54 | process(src, filename, config, transformOptions) { 55 | // allow ~/Documents/electron-builder/node_modules/electron-builder/out/targets/nsis.js:1 56 | 57 | const nodeModulesIndexOf = filename.indexOf("node_modules") 58 | if ((nodeModulesIndexOf > 0 && !filename.includes("electron-builder", nodeModulesIndexOf)) || !(filename.includes("/out/") || filename.includes("\\out\\"))) { 59 | // console.log(`Skip ${filename}`) 60 | return src 61 | } 62 | 63 | // console.log(`Do ${filename}`) 64 | 65 | if (babel == null) { 66 | babel = require('@babel/core') 67 | } 68 | 69 | if (isFullyCompiled(src)) { 70 | // console.log(`!canCompile!o ${filename}`) 71 | return src 72 | } 73 | 74 | let plugins = options.plugins || [] 75 | 76 | const lastLine = src.lastIndexOf("\n") + 1 77 | if (lastLine > 0 && lastLine < src.length) { 78 | if (src.substring(lastLine).startsWith("//#")) { 79 | src = src.substring(0, lastLine - 1) 80 | } 81 | } 82 | 83 | const finalOptions = Object.assign({}, options, { 84 | filename, 85 | plugins, 86 | inputSourceMap: JSON.parse(fs.readFileSync(filename + ".map", "utf-8")), 87 | sourceMaps: "inline", 88 | }) 89 | if (transformOptions && transformOptions.instrument) { 90 | finalOptions.auxiliaryCommentBefore = ' istanbul ignore next ' 91 | finalOptions.plugins = plugins.concat(require('babel-plugin-istanbul').default); 92 | } 93 | 94 | const result = babel.transform(src, finalOptions) 95 | return result.code + "\n//# sourceMappingURL=data:application/json;base64," + convert.fromObject(result.map).toBase64() 96 | } 97 | } 98 | } 99 | 100 | module.exports = createTransformer() 101 | module.exports.createTransformer = createTransformer -------------------------------------------------------------------------------- /packages/electron-webpack/src/plugins/WebpackRemoveOldAssetsPlugin.ts: -------------------------------------------------------------------------------- 1 | import * as bluebird from "bluebird" 2 | import { lstat, readdir, remove, Stats } from "fs-extra" 3 | import * as path from "path" 4 | import { Compiler } from "webpack" 5 | import { orNullIfFileNotExist } from "../util" 6 | 7 | export const MAX_FILE_REQUESTS = 8 8 | export const CONCURRENCY = {concurrency: MAX_FILE_REQUESTS} 9 | 10 | const debug = require("debug")("electron-webpack:clean") 11 | 12 | export async function walk(initialDirPath: string, filter?: Filter | null): Promise> { 13 | const result: Array = [] 14 | const queue: Array = [initialDirPath] 15 | let addDirToResult = false 16 | while (queue.length > 0) { 17 | const dirPath = queue.pop()! 18 | 19 | const childNames = await orNullIfFileNotExist(readdir(dirPath)) 20 | if (childNames == null) { 21 | continue 22 | } 23 | 24 | if (addDirToResult) { 25 | result.push(dirPath) 26 | } 27 | else { 28 | addDirToResult = true 29 | } 30 | 31 | childNames.sort() 32 | 33 | const dirs: Array = [] 34 | // our handler is async, but we should add sorted files, so, we add file to result not in the mapper, but after map 35 | const sortedFilePaths = await bluebird.map(childNames, name => { 36 | const filePath = dirPath + path.sep + name 37 | return lstat(filePath) 38 | .then(stat => { 39 | if (filter != null && !filter(filePath, stat)) { 40 | return null 41 | } 42 | 43 | if (stat.isDirectory()) { 44 | dirs.push(name) 45 | return null 46 | } 47 | else { 48 | return filePath 49 | } 50 | }) 51 | }, CONCURRENCY) 52 | 53 | for (const child of sortedFilePaths) { 54 | if (child != null) { 55 | result.push(child) 56 | } 57 | } 58 | 59 | dirs.sort() 60 | for (const child of dirs) { 61 | queue.push(dirPath + path.sep + child) 62 | } 63 | } 64 | 65 | return result 66 | } 67 | 68 | export type Filter = (file: string, stat: Stats) => boolean 69 | 70 | export class WebpackRemoveOldAssetsPlugin { 71 | constructor(private readonly dllManifest: string | null) { 72 | } 73 | 74 | apply(compiler: Compiler) { 75 | compiler.hooks.afterEmit.tapAsync("WebpackRemoveOldAssetsPlugin", (compilation: any, callback: (error?: Error) => void) => { 76 | const newlyCreatedAssets = compilation.assets 77 | const outDir = compiler.options.output!.path! 78 | walk(outDir, (file, stat) => { 79 | // dll plugin 80 | if (file === this.dllManifest) { 81 | return false 82 | } 83 | 84 | const relativePath = file.substring(outDir.length + 1) 85 | if (stat.isFile()) { 86 | return newlyCreatedAssets[relativePath] == null 87 | } 88 | else if (stat.isDirectory()) { 89 | for (const p of Object.keys(newlyCreatedAssets)) { 90 | if (p.length > relativePath.length && (p[relativePath.length] === "/" || p[relativePath.length] === "\\") && p.startsWith(relativePath)) { 91 | return false 92 | } 93 | } 94 | return true 95 | } 96 | return false 97 | }) 98 | .then((it): any => { 99 | if (it.length === 0) { 100 | return null 101 | } 102 | 103 | if (debug.enabled) { 104 | debug(`Remove outdated files:\n ${it.join("\n ")}`) 105 | } 106 | return bluebird.map(it, it => remove(it), CONCURRENCY) 107 | }) 108 | .then(() => callback()) 109 | .catch(callback) 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /docs/en/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ### Installation 4 | Since `electron-webpack` is a module, not a boilerplate, let's install that and few other dependencies. 5 | 6 | ```bash 7 | yarn add electron-webpack electron webpack --dev 8 | yarn add source-map-support 9 | ``` 10 | 11 | ### Starting in Development Mode 12 | By default, `electron-webpack` expects, at minimum, a `main` process entry point in `src/main/index.js` ([more info](./project-structure.md)). Once you have your entry files setup, you can run `electron-webpack dev` to get started. To make things easier, make use of setting up a script in your `package.json` to start your application in development mode. 13 | 14 | ```json tab="package.json" 15 | { 16 | "scripts": { 17 | "dev": "electron-webpack dev" 18 | } 19 | } 20 | ``` 21 | 22 | If you are using [`electron-webpack-quick-start`](https://github.com/electron-userland/electron-webpack-quick-start) and have already installed dependecies, simply run the follwing to open your application in development mode. 23 | ```bash 24 | yarn dev 25 | ``` 26 | 27 | --- 28 | 29 | ### Source Aliases 30 | 31 | `electron-webpack` provides a few [source aliases](https://webpack.js.org/configuration/resolve/#resolve-alias), or [path mappings](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) as TypeScript would call it, that allow you to import your modules easier. 32 | 33 | * `@`: Provides path to corresponding process (`main` or `renderer`) 34 | * `common`: Provides path to common source directory 35 | 36 | 37 | --- 38 | 39 | ### A little note on the *magic* 40 | `electron-webpack` may seem like a **magical** module to some from the outside, but let's cover a few topics about what's going on *under the hood*. 41 | 42 | #### Hot Module Replacement 43 | Webpack HMR has been setup to support both the `renderer` and `main` process. This allows for faster development ensuring your application is only restarted when necessary. All you need to do is accept hot updates in your entry files: 44 | 45 | ```js 46 | // in main/index.js, renderer/index.js or in both 47 | if (module.hot) { 48 | module.hot.accept(); 49 | } 50 | ``` 51 | Check out [this article](https://medium.com/@develar/electron-very-fast-developer-workflow-with-webpack-hmr-e2a2e23590ad) for more details on HMR in `electron-webpack` and the official [webpack docs](https://webpack.js.org/api/hot-module-replacement/) for details and options on the HMR API. 52 | 53 | #### Bundling for both `renderer` and `main` processes 54 | `webpack` is already setup to have an entry point for each process. In addition, you can also easily add further entry points to the `main` process when needed. In the other cases where you just need support for the `main` process, you can even skip the `renderer` process when needed ([more info](./configuration.md#source-directories)). 55 | 56 | #### Use of `html-webpack-plugin` 57 | You might notice that you don't need an `index.html` to get started on your application. That's because it is created for you, as it adds in a few extra configurations needed for the `electron` environment. If you are creating an `electron` application with `webpack`, you are most likely creating a Single Page Application anyways. So because of that, there is already a `
` provided in the markup that you can mount your application onto. 58 | 59 | ### A Note for Windows Users 60 | 61 | If you run into errors while installing dependencies, related to `node-gyp`, then you most likely do not have the proper build tools installed on your system. Build tools include items like Python and Visual Studio. 62 | 63 | You can quickly install these build tools by globally installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools). It provides many items like Visual C++ packages, Python, and much more. 64 | -------------------------------------------------------------------------------- /docs/en/configuration.md: -------------------------------------------------------------------------------- 1 | As cool as `electron-webpack` can be a simple module to cover a majority of your development needs, you can also throw in your own configurations as needed. Please know that when using expected defaults, no configurations are neccessary. 2 | 3 | Configurations can be applied in `package.json` at `electronWebpack` or in a separate `electron-webpack.(json|json5|yml)`. For the purposes of this documentation, we will use the `package.json` approach and examples will show defaults when applicable. 4 | 5 | **Overview of available options** 6 | ```json 7 | "electronWebpack": { 8 | "commonSourceDirectory": "src/common", 9 | "staticSourceDirectory": "static", 10 | "title": true, 11 | "whiteListedModules": ["foo-ui-library"], 12 | 13 | "main": { 14 | "extraEntries": ["@/preload.js"], 15 | "sourceDirectory": "src/main", 16 | "webpackConfig": "custom.webpack.additions.js" 17 | }, 18 | 19 | "renderer": { 20 | "dll": ["fooModule"], 21 | "sourceDirectory": "src/renderer", 22 | "template": "src/renderer/index.html", 23 | "webpackConfig": "custom.webpack.additions.js", 24 | "webpackDllConfig": "custom.webpackDll.additions.js" 25 | } 26 | } 27 | ``` 28 | 29 | --- 30 | 31 | ### Source Directories 32 | 33 | Defines the path to a process's or common usage directory, relative to the project's root directory. `renderer.sourceDirectory` can be `null` if you don't desire `electron-webpack` to handle bundling. 34 | 35 | ```json 36 | "electronWebpack": { 37 | "commonSourceDirectory": "src/common", 38 | "staticSourceDirectory": "src/static", 39 | "main": { 40 | "sourceDirectory": "src/main" 41 | }, 42 | "renderer": { 43 | "sourceDirectory": "src/renderer" 44 | } 45 | } 46 | ``` 47 | 48 | ### BrowserWindow Title 49 | 50 | Defines default BrowserWindow title. 51 | * `true` (default): Title is automatically set based on `package.json` name or `productName` when using `electron-builder` 52 | * **String**: Use a custom string for title 53 | 54 | ```json 55 | "electronWebpack": { 56 | "title": true, 57 | 58 | /* or */ 59 | 60 | "title": "My Custom Title" 61 | } 62 | ``` 63 | 64 | ### Additional Entry Points for `main` process 65 | For those situtations where you need additional [entry points](https://webpack.js.org/concepts/entry-points/). Can be useful when you need a preload script for a BrowserWindow. 66 | 67 | ```json 68 | "electronWebpack": { 69 | "main": { 70 | "extraEntries": ["@/preload.js"] 71 | } 72 | } 73 | ``` 74 | Note that you can use the `@` alias to reference your `main.sourceDirectory`. 75 | 76 | ### Dll bundle splitting 77 | See [Dll Bundle Splitting](./dll-bundle-splitting.md) for more info. 78 | 79 | ```json 80 | "electronWebpack": { 81 | "renderer": { 82 | "dll": ["fooModule"] 83 | } 84 | } 85 | ``` 86 | 87 | ### White-listing Externals 88 | Since `webpack` is set to target the `electron` environment, all modules are treated as [externals](https://webpack.js.org/configuration/externals/). Unfortunately, there can be a few situations where this behavior may not be expected by some modules. For the case of some Vue UI libraries that provide raw `*.vue` components, they will needed to be white-listed. This ensures that `vue-loader` is able to compile them as the UI library originally expected. 89 | 90 | ```json 91 | "electronWebpack": { 92 | "whiteListedModules": ["foo-ui-library"] 93 | } 94 | ``` 95 | 96 | ### Modified Webpack Configurations 97 | See [Modifying Webpack Configurations](modifying-webpack-configurations.md) for more information. 98 | 99 | ```json 100 | "electronWebpack": { 101 | "main": { 102 | "webpackConfig": "custom.additions.webpack.js" 103 | }, 104 | 105 | "renderer": { 106 | "webpackConfig": "custom.additions.webpack.js", 107 | "webpackDllConfig": "custom.additions.webpack.js" 108 | } 109 | } 110 | ``` 111 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/dev/WebpackDevServerManager.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import { ChildProcess } from "child_process" 3 | import * as path from "path" 4 | import { createConfigurator } from "../main" 5 | import { statOrNull } from "../util" 6 | import { ChildProcessManager, PromiseNotifier, run } from "./ChildProcessManager" 7 | import { LineFilter, logError, logProcess, logProcessErrorOutput } from "./devUtil" 8 | 9 | const debug = require("debug")("electron-webpack") 10 | 11 | function runWds(projectDir: string, env: any) { 12 | const isWin = process.platform === "win32" 13 | const webpackDevServerPath = require.resolve(path.join(".bin", "webpack-dev-server" + (isWin ? ".cmd" : ""))) 14 | debug(`Start renderer WDS ${webpackDevServerPath} on ${env.ELECTRON_WEBPACK_WDS_PORT} port`) 15 | return run(webpackDevServerPath, ["--color", "--env.autoClean=false", "--config", path.join(__dirname, "../../webpack.renderer.config.js")], { 16 | env, 17 | cwd: projectDir, 18 | }) 19 | } 20 | 21 | // 1. in another process to speedup compilation 22 | // 2. some loaders detect webpack-dev-server hot mode only if run as CLI 23 | export async function startRenderer(projectDir: string, env: any) { 24 | const webpackConfigurator = await createConfigurator("renderer", {production: false, configuration: {projectDir}}) 25 | const sourceDir = webpackConfigurator.sourceDir 26 | // explicitly set to null - do not handle at all and do not show info message 27 | if (sourceDir === null) { 28 | return 29 | } 30 | 31 | const dirStat = await statOrNull(sourceDir) 32 | if (dirStat == null || !dirStat.isDirectory()) { 33 | logProcess("Renderer", `No renderer source directory (${path.relative(projectDir, sourceDir)})`, chalk.blue) 34 | return 35 | } 36 | 37 | if (webpackConfigurator.hasDependency("electron-next")) { 38 | debug(`Renderer WDS is not started - there is electron-next dependency`) 39 | return 40 | } 41 | 42 | const lineFilter = new CompoundRendererLineFilter([ 43 | new OneTimeLineFilter("Project is running at "), 44 | new OneTimeLineFilter("webpack output is served from "), 45 | ]) 46 | return await new Promise((resolve: (() => void) | null, reject: ((error: Error) => void) | null) => { 47 | let devServerProcess: ChildProcess | null 48 | try { 49 | devServerProcess = runWds(projectDir, env) 50 | } 51 | catch (e) { 52 | reject!(e) 53 | return 54 | } 55 | 56 | //tslint:disable-next-line:no-unused-expression 57 | new ChildProcessManager(devServerProcess, "Renderer WDS", new PromiseNotifier(resolve, reject)) 58 | devServerProcess.on("error", error => { 59 | if (reject == null) { 60 | logError("Renderer", error) 61 | } 62 | else { 63 | reject(error) 64 | reject = null 65 | } 66 | }) 67 | 68 | devServerProcess.stdout!!.on("data", (data: string) => { 69 | logProcess("Renderer", data, chalk.blue, lineFilter) 70 | 71 | const r = resolve 72 | // we must resolve only after compilation, otherwise devtools disconnected 73 | if (r != null && (data.includes(": Compiled successfully.") || data.includes(": Compiled with warnings."))) { 74 | resolve = null 75 | r() 76 | } 77 | }) 78 | 79 | logProcessErrorOutput("Renderer", devServerProcess) 80 | }) 81 | } 82 | 83 | class OneTimeLineFilter implements LineFilter { 84 | private filtered = false 85 | 86 | constructor(private readonly prefix: string) { 87 | } 88 | 89 | filter(line: string) { 90 | if (!this.filtered && line.startsWith(this.prefix)) { 91 | this.filtered = true 92 | return false 93 | 94 | } 95 | return true 96 | } 97 | } 98 | 99 | class CompoundRendererLineFilter implements LineFilter { 100 | constructor(private readonly filters: Array) { 101 | } 102 | 103 | filter(line: string) { 104 | return !this.filters.some(it => !it.filter(line)) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # electron-webpack [![npm version](https://img.shields.io/npm/v/electron-webpack.svg)](https://npmjs.org/package/electron-webpack) [![Maintainers Wanted](https://img.shields.io/badge/maintainers-wanted-red.svg)](https://github.com/electron-userland/electron-webpack) 2 | 3 | 4 | Project Status: maintenance mode. **Consider using [Vite](https://github.com/cawa-93/vite-electron-builder) instead of Webpack.** 5 | 6 | > Because setting up `webpack` in the `electron` environment shouldn't be difficult. 7 | 8 | ## Maintainer Needed 9 | This project is looking for active maintainers. Feel free to post [here](https://github.com/electron-userland/electron-webpack/issues/428). 10 | 11 | ## Overview 12 | Modern web development practices today require a lot of setup with things like `webpack` to bundle your code, `babel` for transpiling, `eslint` for linting, and so much more that the list just goes on. Unfortunately when creating `electron` applications, all of that setup just became much more difficult. The primary aim of `electron-webpack` is to eliminate all preliminary setup with one simple install so you can get back to developing your application. 13 | 14 | > Why create a module and not a full boilerplate? 15 | 16 | If you've been in the JavaScript world for even a short period of time, you are very aware that things are always changing, and development setup is no exclusion. Putting all development scripts into a single **updatable** module just makes sense. Sure a full featured boilerplate works too, but doing also involves needing to manually update those pesky `webpack` configuration files that some may call *magic* when something new comes out. 17 | 18 | Here are some of the awesome features you'll find using `electron-webpack`... 19 | 20 | * Detailed [documentation](https://webpack.electron.build) 21 | * Use of [`webpack`](https://webpack.js.org/) for source code bundling 22 | * Use of [`webpack-dev-server`](https://github.com/webpack/webpack-dev-server) for development 23 | * HMR for both `renderer` and `main` processes 24 | * Use of [`@babel/preset-env`](https://github.com/babel/babel/tree/master/packages/babel-preset-env) that is automatically configured based on your `electron` version 25 | * Ability to add custom `webpack` loaders, plugins, etc. 26 | * [Add-ons](https://webpack.electron.build/add-ons) to support items like [TypeScript](http://www.typescriptlang.org/), [Less](http://lesscss.org/), [EJS](http://www.embeddedjs.com/), etc. 27 | 28 | ## Quick Start 29 | Get started fast with [electron-webpack-quick-start](https://github.com/electron-userland/electron-webpack-quick-start). 30 | ```bash 31 | # create a directory of your choice, and copy template using curl 32 | mkdir my-project && cd my-project 33 | curl -fsSL https://github.com/electron-userland/electron-webpack-quick-start/archive/master.tar.gz | tar -xz --strip-components 1 34 | 35 | # or copy template using git clone 36 | git clone https://github.com/electron-userland/electron-webpack-quick-start.git 37 | cd electron-webpack-quick-start 38 | rm -rf .git 39 | 40 | # install dependencies 41 | yarn 42 | ``` 43 | 44 | If you already have an existing project, or are looking for a custom approach outside of the quick start template, make sure to read over the [Core Concepts](https://webpack.electron.build/core-concepts), [Project Structure](https://webpack.electron.build/project-structure), and [Development](https://webpack.electron.build/development) sections of `electron-webpack`'s documentation. 45 | 46 | ### Next Steps 47 | Make sure to take advantage of the detailed [documentation](https://webpack.electron.build) that `electron-webpack` provides. It covers everything from how things work internally, adding custom configurations, and building your application. 48 | 49 | ### Contributing 50 | Feel free to grab an issue and fix it or to share your features and improvements - PRs are always welcome! 51 | However, in order for your contribution to be property included in the automatically generated release notes, please use our [standard format](https://gist.github.com/develar/273e2eb938792cf5f86451fbac2bcd51) for your commit messages. 52 | -------------------------------------------------------------------------------- /packages/electron-webpack/scheme.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "additionalProperties": false, 4 | "definitions": { 5 | "ElectronWebpackConfigurationMain": { 6 | "additionalProperties": false, 7 | "properties": { 8 | "extraEntries": { 9 | "anyOf": [ 10 | { 11 | "items": { 12 | "type": "string" 13 | }, 14 | "type": "array" 15 | }, 16 | { 17 | "additionalProperties": { 18 | "anyOf": [ 19 | { 20 | "type": "string" 21 | }, 22 | { 23 | "items": { 24 | "type": "string" 25 | }, 26 | "type": "array" 27 | } 28 | ] 29 | }, 30 | "type": "object" 31 | }, 32 | { 33 | "type": "string" 34 | } 35 | ], 36 | "description": "The extra [entry points](https://webpack.js.org/concepts/entry-points/)." 37 | }, 38 | "sourceDirectory": { 39 | "type": [ 40 | "string", 41 | "null" 42 | ] 43 | }, 44 | "webpackConfig": { 45 | "type": [ 46 | "string", 47 | "null" 48 | ] 49 | } 50 | }, 51 | "type": "object" 52 | }, 53 | "ElectronWebpackConfigurationRenderer": { 54 | "additionalProperties": false, 55 | "properties": { 56 | "dll": { 57 | "anyOf": [ 58 | { 59 | "items": { 60 | "type": "string" 61 | }, 62 | "type": "array" 63 | }, 64 | { 65 | "type": "object" 66 | }, 67 | { 68 | "type": "null" 69 | } 70 | ] 71 | }, 72 | "sourceDirectory": { 73 | "type": [ 74 | "string", 75 | "null" 76 | ] 77 | }, 78 | "template": { 79 | "type": [ 80 | "string", 81 | "null" 82 | ] 83 | }, 84 | "webpackConfig": { 85 | "type": [ 86 | "string", 87 | "null" 88 | ] 89 | }, 90 | "webpackDllConfig": { 91 | "type": [ 92 | "string", 93 | "null" 94 | ] 95 | } 96 | }, 97 | "type": "object" 98 | } 99 | }, 100 | "properties": { 101 | "commonDistDirectory": { 102 | "type": [ 103 | "string", 104 | "null" 105 | ] 106 | }, 107 | "commonSourceDirectory": { 108 | "type": [ 109 | "string", 110 | "null" 111 | ] 112 | }, 113 | "electronVersion": { 114 | "type": "string" 115 | }, 116 | "externals": { 117 | "items": { 118 | "type": "string" 119 | }, 120 | "type": "array" 121 | }, 122 | "main": { 123 | "anyOf": [ 124 | { 125 | "$ref": "#/definitions/ElectronWebpackConfigurationMain" 126 | }, 127 | { 128 | "type": "null" 129 | } 130 | ] 131 | }, 132 | "projectDir": { 133 | "type": [ 134 | "string", 135 | "null" 136 | ] 137 | }, 138 | "renderer": { 139 | "anyOf": [ 140 | { 141 | "$ref": "#/definitions/ElectronWebpackConfigurationRenderer" 142 | }, 143 | { 144 | "type": "null" 145 | } 146 | ] 147 | }, 148 | "staticSourceDirectory": { 149 | "type": [ 150 | "string", 151 | "null" 152 | ] 153 | }, 154 | "title": { 155 | "type": [ 156 | "string", 157 | "boolean", 158 | "null" 159 | ] 160 | }, 161 | "whiteListedModules": { 162 | "items": { 163 | "type": "string" 164 | }, 165 | "type": "array" 166 | } 167 | }, 168 | "type": "object" 169 | } -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 87 | 89 | -------------------------------------------------------------------------------- /test/out/__snapshots__/loaderDetectionTest.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`nunjucks 1`] = ` 4 | "./src/main/index.js 5 | Module not found: Error: Can't resolve 'nunjucks-loader' in '' 6 | resolve 'nunjucks-loader' in '' 7 | Parsed request is a module 8 | using description file: /package.json (relative path: .) 9 | resolve as module 10 | looking for modules in /node_modules 11 | using description file: /package.json (relative path: ./node_modules) 12 | using description file: /package.json (relative path: ./node_modules/nunjucks-loader) 13 | no extension 14 | /node_modules/nunjucks-loader doesn't exist 15 | .js 16 | /node_modules/nunjucks-loader.js doesn't exist 17 | .json 18 | /node_modules/nunjucks-loader.json doesn't exist 19 | as directory 20 | /node_modules/nunjucks-loader doesn't exist 21 | looking for modules in /packages/electron-webpack/node_modules 22 | using description file: /packages/electron-webpack/package.json (relative path: ./node_modules) 23 | using description file: /packages/electron-webpack/package.json (relative path: ./node_modules/nunjucks-loader) 24 | no extension 25 | /packages/electron-webpack/node_modules/nunjucks-loader doesn't exist 26 | .js 27 | /packages/electron-webpack/node_modules/nunjucks-loader.js doesn't exist 28 | .json 29 | /packages/electron-webpack/node_modules/nunjucks-loader.json doesn't exist 30 | as directory 31 | /packages/electron-webpack/node_modules/nunjucks-loader doesn't exist 32 | [/node_modules/nunjucks-loader] 33 | [/node_modules/nunjucks-loader.js] 34 | [/node_modules/nunjucks-loader.json] 35 | [/packages/electron-webpack/node_modules/nunjucks-loader] 36 | [/packages/electron-webpack/node_modules/nunjucks-loader.js] 37 | [/packages/electron-webpack/node_modules/nunjucks-loader.json] 38 | @ ./src/main/index.js 3:0-21 39 | @ multi ./src/main/index.js" 40 | `; 41 | 42 | exports[`react 1`] = `"Cannot find module '@babel/preset-react' from 'js.js'"`; 43 | 44 | exports[`sass 1`] = ` 45 | "./src/renderer/index.js 46 | Module not found: Error: Can't resolve 'sass-loader' in '' 47 | resolve 'sass-loader' in '' 48 | Parsed request is a module 49 | using description file: /package.json (relative path: .) 50 | resolve as module 51 | looking for modules in /node_modules 52 | using description file: /package.json (relative path: ./node_modules) 53 | using description file: /package.json (relative path: ./node_modules/sass-loader) 54 | no extension 55 | /node_modules/sass-loader doesn't exist 56 | .js 57 | /node_modules/sass-loader.js doesn't exist 58 | .json 59 | /node_modules/sass-loader.json doesn't exist 60 | as directory 61 | /node_modules/sass-loader doesn't exist 62 | looking for modules in /packages/electron-webpack/node_modules 63 | using description file: /packages/electron-webpack/package.json (relative path: ./node_modules) 64 | using description file: /packages/electron-webpack/package.json (relative path: ./node_modules/sass-loader) 65 | no extension 66 | /packages/electron-webpack/node_modules/sass-loader doesn't exist 67 | .js 68 | /packages/electron-webpack/node_modules/sass-loader.js doesn't exist 69 | .json 70 | /packages/electron-webpack/node_modules/sass-loader.json doesn't exist 71 | as directory 72 | /packages/electron-webpack/node_modules/sass-loader doesn't exist 73 | [/node_modules/sass-loader] 74 | [/node_modules/sass-loader.js] 75 | [/node_modules/sass-loader.json] 76 | [/packages/electron-webpack/node_modules/sass-loader] 77 | [/packages/electron-webpack/node_modules/sass-loader.js] 78 | [/packages/electron-webpack/node_modules/sass-loader.json] 79 | @ ./src/renderer/index.js 3:0-22 80 | @ multi ./src/renderer/index.js" 81 | `; 82 | -------------------------------------------------------------------------------- /docs/en/modifying-webpack-configurations.md: -------------------------------------------------------------------------------- 1 | Another great benefit of using `electron-wepback` is that you are never restricted to an abstracted API. Of course there isn't a configuration that fits everybody's need, and `electron-webpack` is aware of that. 2 | 3 | Custom modifications can be made for the `renderer`, `renderer-dll`, and `main` processes. 4 | Notice that the full `webpack` API is usable and not blocked. You could add any loaders and plugins, but keep in mind that making major modifications may break expected behavior. 5 | 6 | There are currently two methods of modifying the webpack configuration: 7 | 8 | - [Using a config object](#using-a-config-object) 9 | - [Using a config function](#using-a-config-function) 10 | 11 | The benefit of using either method, as opposed to [Extending as a Library](extending-as-a-library.md), is that you don't lose access to the `electron-webpack` CLI, which is very beneficial for development. 12 | 13 | ## Using a config object 14 | 15 | Thanks to the power of [webpack-merge](https://github.com/survivejs/webpack-merge), it is possible to add your own webpack loaders, plugins, etc, by simply providing a config object that will be automatically merged on top of the predefined configuration using the [smart merging strategy](https://github.com/survivejs/webpack-merge#smart-merging). 16 | 17 | 18 | ### Use Case 19 | 20 | Let's say our project needs to be able to import `*.txt` files in the `renderer` process. We can use `raw-loader` to get that done. 21 | 22 | 23 | #### Install `raw-loader` 24 | 25 | ```bash 26 | yarn add raw-loader --dev 27 | ``` 28 | 29 | #### Configure `electron-webpack` 30 | 31 | Provide a configuration for `electron-webpack`, for example in your `package.json` file, and point it to a custom webpack config file for the renderer process: 32 | ```json 33 | { 34 | "electronWebpack": { 35 | "renderer": { 36 | "webpackConfig": "webpack.renderer.additions.js" 37 | } 38 | } 39 | } 40 | ``` 41 | See [Configuration](configuration.md) for more information. 42 | 43 | #### Configure `raw-loader` 44 | 45 | ```js tab="webpack.renderer.additions.js" 46 | module.exports = { 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.txt$/, 51 | use: 'raw-loader' 52 | } 53 | ] 54 | } 55 | } 56 | ``` 57 | 58 | 59 | Now when running `electron-webpack`'s CLI, your custom additions will be loaded and you can use them as expected. 60 | 61 | This is a very convenient way to add configuration on top of the defaults, but it has some limitations when it comes to modifying defaults or even removing parts of the configuration. 62 | 63 | If you need more control over the configuration, then maybe a [config function](#using-a-config-function) or [Extending as a Library](extending-as-a-library.md) can better suit your needs. 64 | 65 | ## Using a config function 66 | 67 | Beginning with version 2.7.0, `electron-webpack` supports configuration modules that export a function instead of an object. 68 | 69 | In this case, the config function you provide will be invoked with the default config object as an argument, and it is expected to return the final config object. 70 | 71 | You may modify the default object using any method you like, for example you could iterate over loaders or plugins and remove or replace some of them. You could use a tool like `webpack-merge` manually, or you could even create and return an entirely new object if that suits your needs. 72 | 73 | 74 | ### Use Case 75 | 76 | Let's say you would like the `css-loader` to treat everything as [CSS modules](https://github.com/webpack-contrib/css-loader#modules) and implicitly convert all class names to hashed variants, unless you defined them as `:global` explicitly. 77 | 78 | First, configure `electron-webpack` to use your custom config file. 79 | 80 | Note that while the filename technically doesn't matter, we do not use "additions" in the name as we did before, as this config file provides more than just additions. 81 | 82 | ```json 83 | { 84 | "electronWebpack": { 85 | "renderer": { 86 | "webpackConfig": "webpack.renderer.js" 87 | } 88 | } 89 | } 90 | ``` 91 | See [Configuration](configuration.md) for more information. 92 | 93 | Next, provide your custom configuration file: 94 | 95 | ```js tab="webpack.renderer.js" 96 | module.exports = function(config) { 97 | const styleRules = config.module.rules.filter(rule => 98 | rule.test.toString().match(/css|less|s\(\[ac\]\)ss/) 99 | ) 100 | 101 | styleRules.forEach(rule => { 102 | const cssLoader = rule.use.find(use => use.loader === 'css-loader') 103 | // this is the actual modification we make: 104 | cssLoader.options.modules = 'local' 105 | }) 106 | 107 | return config 108 | } 109 | ``` 110 | 111 | As you can see, instead of exporting an object, we export a function. 112 | The function receives the webpack config object, simply modifies it in certain places and then returns it again. 113 | 114 | When in doubt about the structure of the config object, you can either get familiar with the [sources that define it](https://github.com/electron-userland/electron-webpack/tree/master/packages/electron-webpack/src/targets) or simply try a quick `console.log(config)` inside of the function. 115 | Or better yet, try `console.log(JSON.stringify(config, null, 4))` to reveal all the nested values in a readable format. 116 | 117 | If you need even more control over the configuration, then maybe [Extending as a Library](extending-as-a-library.md) can better suit your needs. 118 | -------------------------------------------------------------------------------- /docs/en/add-ons.md: -------------------------------------------------------------------------------- 1 | Although `electron-webpack` is provided as a single module, you can also install add-ons. Add-ons are made available to help setup certain frameworks that may require a lot of extra configuration or dependencies, such as TypeScript or Vue.js. 2 | 3 | These add-ons are completely optional and may not support all use cases. If you need something more custom, please know that the entirety of `webpack`'s documentation still applies to `electron-webpack` ([more info](./extending-as-a-library.md)). 4 | 5 | Current Add-ons: 6 | 7 | * [JavaScript Frameworks](#javascript-frameworks) 8 | * [Vue.js](#vuejs) 9 | * [React JSX](#react-jsx) 10 | * [Pre-processors](#pre-processors) 11 | * [ESLint](#eslint) 12 | * [TypeScript](#typescript) 13 | * [Less](#less) 14 | * [Sass/SCSS](#sassscss) 15 | * [EJS](#ejs) 16 | * [Nunjucks](#nunjucks) 17 | * [UI Libraries](#ui-libraries) 18 | * [iView](#iview) 19 | * [Miscellaneous](#miscellaneous) 20 | * [Build Notifications](#build-notifications) 21 | 22 | --- 23 | 24 | ## JavaScript Frameworks 25 | 26 | ### Vue.js 27 | Adds support for Vue component files through the use of `vue-loader`. In addition, `vue-devtools` will also be installed for development purposes. 28 | 29 | ```bash 30 | yarn add vue electron-webpack-vue --dev 31 | ``` 32 | 33 | #### Adding TypeScript support 34 | Install the [TypeScript](#typescript) add-on, followed by adding the below file to shim Vue component files. 35 | 36 | ```ts tab="src/renderer/vue-shims.d.ts" 37 | declare module '*.vue' { 38 | import Vue from 'vue' 39 | export default Vue 40 | } 41 | ``` 42 | 43 | And of course, make sure to let `vue-loader` know you want to use TypeScript in your component file using the `lang="ts"` attribute. 44 | 45 | ```html 46 | 47 | 48 | 51 | 52 | 53 | ``` 54 | 55 | #### Adding ESLint support 56 | Install the [ESLint](#eslint) add-on, install `eslint-plugin-html`, and add the following additional configurations. 57 | 58 | Install `html` plugin to lint Vue component files: 59 | ```bash 60 | yarn add eslint-plugin-html --dev 61 | ``` 62 | 63 | ```js tab=".eslintrc.js" 64 | module.exports = { 65 | plugins: [ 66 | 'html' 67 | ] 68 | } 69 | ``` 70 | 71 | ### React JSX 72 | Add support for compiling JSX files: 73 | 74 | ```bash 75 | yarn add @babel/preset-react --dev 76 | ``` 77 | 78 | ### Adding TypeScript support for JSX (`.tsx` files) 79 | Install the [TypeScript](#typescript) add-on, followed by extending your `tsconfig.json` to include the jsx compiler option as below: 80 | 81 | ```json tab="tsconfig.json" 82 | { 83 | "extends": "./node_modules/electron-webpack/tsconfig-base.json", 84 | "compilerOptions": { 85 | "jsx": "react" 86 | } 87 | } 88 | ``` 89 | 90 | --- 91 | 92 | ## Pre-processors 93 | 94 | ### Babel 95 | 96 | [@babel/preset-env](https://github.com/babel/babel/tree/master/packages/babel-preset-env) is automatically configured based on your `electron` version. 97 | 98 | All direct dev dependencies `babel-preset-*` and `babel-plugin-*` are automatically configured also. 99 | 100 | ### ESLint 101 | Add support for script file linting using `eslint`. Internally uses `eslint`, `eslint-loader`, `eslint-friendly-formatter`, and makes `babel-eslint` available if needed. 102 | 103 | ```bash 104 | yarn add electron-webpack-eslint --dev 105 | ``` 106 | 107 | Create `.eslintrc.js` in the root directory: 108 | 109 | ```js tab=".eslintrc.js" 110 | module.exports = { 111 | /* your base configuration of choice */ 112 | extends: 'eslint:recommended', 113 | 114 | parser: 'babel-eslint', 115 | parserOptions: { 116 | sourceType: 'module' 117 | }, 118 | env: { 119 | browser: true, 120 | node: true 121 | }, 122 | globals: { 123 | __static: true 124 | } 125 | } 126 | ``` 127 | 128 | ### TypeScript 129 | Add support for compiling TypeScript script files. Internally uses both `ts-loader` and `fork-ts-checker-webpack-plugin` to compile `*.ts`. Note that entry files can also use the `*.ts` extension. 130 | 131 | ```bash 132 | yarn add typescript electron-webpack-ts --dev 133 | ``` 134 | 135 | Create `tsconfig.json` in the root directory: 136 | ```json 137 | { 138 | "extends": "./node_modules/electron-webpack/tsconfig-base.json" 139 | } 140 | ``` 141 | 142 | ### Less 143 | Add support for compiling Less style files. 144 | 145 | ```bash 146 | yarn add less less-loader --dev 147 | ``` 148 | 149 | ### Sass/SCSS 150 | Add support for compiling Sass/SCSS style files. 151 | 152 | ```bash 153 | yarn add node-sass sass-loader --dev 154 | ``` 155 | 156 | ### EJS 157 | Add support for compiling EJS template files. 158 | 159 | ```bash 160 | yarn add ejs ejs-html-loader --dev 161 | ``` 162 | 163 | ### Nunjucks 164 | Add support for compiling Nunjucks template files: 165 | 166 | ```bash 167 | yarn add nunjucks nunjucks-loader --dev 168 | ``` 169 | 170 | --- 171 | 172 | ## UI Libraries 173 | 174 | ### iView 175 | Once you have the [Vue.js](#vuejs) add-on installed, `electron-webpack` will internally support iView's "import on demand" feature. No further setup is necessary. 176 | 177 | ### Element 178 | Once you have the [element-ui](https://github.com/ElemeFE/element) installed, `electron-webpack` will internally support Element's "import on demand" feature. No further setup is necessary. 179 | 180 | --- 181 | 182 | ## Miscellaneous 183 | 184 | ### Build Notifications 185 | Provide OS-level notifications from `webpack` during development. 186 | 187 | ```bash 188 | yarn add webpack-build-notifier --dev 189 | ``` 190 | -------------------------------------------------------------------------------- /test/src/helpers/helper.ts: -------------------------------------------------------------------------------- 1 | import { ElectronWebpackConfiguration } from "electron-webpack" 2 | import { copy, emptyDir } from "fs-extra" 3 | import MemoryFS from "memory-fs" 4 | import * as path from "path" 5 | import { TmpDir } from "temp-file" 6 | import webpack, { Configuration, Stats } from "webpack" 7 | 8 | export const tmpDir = new TmpDir() 9 | 10 | export const rootDir = path.join(__dirname, "..", "..", "..") 11 | 12 | export async function doTest(configurationFile: string, electronWebpackConfiguration?: ElectronWebpackConfiguration) { 13 | const projectDir = await getMutableProjectDir() 14 | const finalConfiguration = {projectDir, ...electronWebpackConfiguration} 15 | const configuration = await require(`electron-webpack/${configurationFile}`)({configuration: finalConfiguration, production: true}) 16 | await testWebpack(configuration, projectDir) 17 | } 18 | 19 | export async function getMutableProjectDir(fixtureName = "simple") { 20 | const projectDir = process.env.TEST_APP_TMP_DIR || await tmpDir.getTempDir() 21 | await emptyDir(projectDir) 22 | await copy(path.join(rootDir, "test/fixtures", fixtureName), projectDir) 23 | return projectDir 24 | } 25 | 26 | export async function testWebpack(configuration: Configuration, projectDir: string, checkCompilation = true) { 27 | if (Array.isArray(configuration)) { 28 | configuration.forEach(addCustomResolver) 29 | } 30 | else { 31 | addCustomResolver(configuration) 32 | } 33 | 34 | const fs = new MemoryFS() 35 | const stats = await new Promise((resolve, reject) => { 36 | compile(fs, configuration, resolve, reject) 37 | }) 38 | 39 | if (checkCompilation) { 40 | expect(statToMatchObject(stats, projectDir, fs)).toMatchSnapshot() 41 | expect(bufferToString(fs.meta(projectDir), projectDir)).toMatchSnapshot() 42 | } 43 | return fs 44 | } 45 | 46 | function addCustomResolver(configuration: Configuration) { 47 | expect(configuration.resolveLoader).toBeUndefined() 48 | configuration.resolveLoader = { 49 | modules: [path.join(rootDir, "node_modules"), path.join(rootDir, "packages/electron-webpack/node_modules")] 50 | } 51 | } 52 | 53 | function statToMatchObject(stats: Stats, projectDir: string, fs: MemoryFS) { 54 | if (stats.hasErrors()) { 55 | console.log(stats.toString({colors: true})) 56 | // console.log("FS data: " + util.inspect(fs, {colors: true})) 57 | throw new Error(stats.toJson().errors.join("\n")) 58 | } 59 | 60 | // skip first 3 lines - Hash, Version and Time 61 | return stats.toString() 62 | .split(/\r?\n/) 63 | .filter(it => { 64 | const trimmed = it.trim() 65 | return !trimmed.startsWith("Time:") && !trimmed.startsWith("Hash:") && !trimmed.startsWith("Version:") && !trimmed.startsWith("Built at:") 66 | }) 67 | .join("\n") 68 | .replace(/\\/g, "/") 69 | .replace(new RegExp(`[./]*${projectDir}`, "g"), "") 70 | // no idea why failed on CI - in any case we validate file content 71 | .replace(/\/style\.css \d+ bytes/g, "/style.css") 72 | .replace(new RegExp(`[./]*${process.cwd()}`, "g"), "") 73 | .replace(new RegExp("[.]*/[^\n]+node_modules", "g"), "/node_modules") 74 | } 75 | 76 | function compile(fs: any, configuration: Configuration, resolve: (stats: Stats) => void, reject: (error?: Error) => void) { 77 | const compiler = webpack(configuration) 78 | compiler.outputFileSystem = fs 79 | compiler.run((error, stats) => { 80 | if (error != null) { 81 | reject(error) 82 | return 83 | } 84 | 85 | resolve(stats) 86 | }) 87 | } 88 | 89 | export function bufferToString(host: any, projectDir: string) { 90 | for (const key of Object.getOwnPropertyNames(host)) { 91 | if (key === "") { 92 | delete host[key] 93 | } 94 | 95 | const value = host[key] 96 | if (value == null) { 97 | continue 98 | } 99 | 100 | if (Buffer.isBuffer(value)) { 101 | host[key] = removeNotStableValues(value.toString()) 102 | .replace(new RegExp(projectDir, "g"), "") 103 | .replace(new RegExp(rootDir, "g"), "") 104 | } 105 | else if (typeof value === "object") { 106 | bufferToString(value, projectDir) 107 | } 108 | } 109 | return host 110 | } 111 | 112 | function removeNotStableValues(value: string) { 113 | const bootstrap = "webpack:///webpack/bootstrap" 114 | const index = value.indexOf(bootstrap) 115 | if (index > 0) { 116 | return value.substring(0, index + bootstrap.length) + value.substring(value.indexOf('"', index)) 117 | } 118 | 119 | return value 120 | } 121 | 122 | export function assertThat(actual: any): Assertions { 123 | return new Assertions(actual) 124 | } 125 | 126 | class Assertions { 127 | constructor(private actual: any) { 128 | } 129 | 130 | containsAll(expected: Iterable) { 131 | expect(this.actual.slice().sort()).toEqual(Array.from(expected).slice().sort()) 132 | } 133 | 134 | async throws(projectDir: string) { 135 | let actualError: Error | null = null 136 | let result: any 137 | try { 138 | result = await this.actual 139 | } 140 | catch (e) { 141 | actualError = e 142 | } 143 | 144 | let m 145 | if (actualError == null) { 146 | m = result 147 | } 148 | else { 149 | m = actualError.message 150 | m = m.toString() 151 | .replace(new RegExp(projectDir, "g"), "") 152 | .replace(new RegExp(rootDir, "g"), "") 153 | } 154 | try { 155 | expect(m).toMatchSnapshot() 156 | } 157 | catch (matchError) { 158 | throw new Error(matchError + " " + actualError) 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /packages/electron-webpack/src/targets/BaseTarget.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path" 2 | import { DefinePlugin, EnvironmentPlugin, HotModuleReplacementPlugin, LoaderOptionsPlugin, version as webpackVersion } from "webpack" 3 | import { getDefaultRelativeSystemDependentCommonSource } from "../config" 4 | import { configureDll } from "../configurators/dll" 5 | import { configureEslint } from "../configurators/eslint" 6 | import { createBabelLoader } from "../configurators/js" 7 | import { WebpackConfigurator } from "../main" 8 | import { WatchFilterPlugin } from "../plugins/WatchMatchPlugin" 9 | import { WebpackRemoveOldAssetsPlugin } from "../plugins/WebpackRemoveOldAssetsPlugin" 10 | 11 | export class BaseTarget { 12 | configureRules(configurator: WebpackConfigurator): void { 13 | const rules = configurator.rules 14 | 15 | const babelLoader = createBabelLoader(configurator) 16 | // noinspection SpellCheckingInspection 17 | if (configurator.type !== "main" && configurator.hasDependency("iview")) { 18 | rules.push({ 19 | test: /iview.src.*?js$/, 20 | use: babelLoader 21 | }) 22 | } 23 | 24 | rules.push( 25 | { 26 | test: /\.js$/, 27 | exclude: /(node_modules|bower_components)/, 28 | use: babelLoader 29 | }, 30 | { 31 | test: /\.node$/, 32 | use: "node-loader" 33 | }, 34 | ) 35 | 36 | if (configurator.hasDevDependency("nunjucks-loader")) { 37 | rules.push({ 38 | test: /\.(njk|nunjucks)$/, 39 | loader: "nunjucks-loader" 40 | }) 41 | } 42 | 43 | configureEslint(configurator) 44 | } 45 | 46 | async configurePlugins(configurator: WebpackConfigurator): Promise { 47 | const plugins = configurator.plugins 48 | 49 | const dllManifest = await configureDll(configurator) 50 | const mode = configurator.isProduction ? "production" : "development" 51 | 52 | let optimization = configurator.config.optimization 53 | if (optimization == null) { 54 | optimization = {} 55 | configurator.config.optimization = optimization 56 | } 57 | 58 | optimization.nodeEnv = mode 59 | configurator.config.mode = mode 60 | 61 | if (configurator.isProduction) { 62 | if (configurator.env.minify !== false) { 63 | const TerserPlugin = require("terser-webpack-plugin") 64 | // noinspection SpellCheckingInspection 65 | optimization.minimizer = [new TerserPlugin({ 66 | parallel: true, 67 | sourceMap: true, 68 | terserOptions: { 69 | keep_fnames: true, 70 | }, 71 | })] 72 | } 73 | optimization.minimize = true 74 | plugins.push(new LoaderOptionsPlugin({minimize: true})) 75 | 76 | // do not use ModuleConcatenationPlugin for HMR 77 | // https://github.com/webpack/webpack-dev-server/issues/949 78 | optimization.concatenateModules = true 79 | } 80 | else { 81 | configureDevelopmentPlugins(configurator) 82 | } 83 | 84 | if (configurator.env.autoClean !== false) { 85 | plugins.push(new WebpackRemoveOldAssetsPlugin(dllManifest)) 86 | } 87 | 88 | optimization.noEmitOnErrors = true 89 | 90 | const additionalEnvironmentVariables = Object.keys(process.env).filter(it => it.startsWith("ELECTRON_WEBPACK_")) 91 | if (additionalEnvironmentVariables.length > 0) { 92 | plugins.push(new EnvironmentPlugin(additionalEnvironmentVariables)) 93 | } 94 | } 95 | } 96 | 97 | export function configureFileLoader(prefix: string, limit = 10 * 1024) { 98 | return { 99 | limit, 100 | name: `${prefix}/[name]--[folder].[ext]` 101 | } 102 | } 103 | 104 | function isAncestor(file: string, dir: string) { 105 | if (file === dir) { 106 | return true 107 | } 108 | return file.length > dir.length && file[dir.length] === path.sep && file.startsWith(dir) 109 | } 110 | 111 | function configureDevelopmentPlugins(configurator: WebpackConfigurator) { 112 | const optimization = configurator.config.optimization!! 113 | if (parseInt(String(webpackVersion), 10) >= 5) { 114 | optimization.moduleIds = 'named' 115 | } 116 | else { 117 | optimization.namedModules = true 118 | } 119 | 120 | const plugins = configurator.plugins 121 | plugins.push(new DefinePlugin({ 122 | __static: `"${path.join(configurator.projectDir, configurator.staticSourceDirectory).replace(/\\/g, "\\\\")}"`, 123 | "process.env.NODE_ENV": configurator.isProduction ? "\"production\"" : "\"development\"" 124 | })) 125 | 126 | plugins.push(new HotModuleReplacementPlugin()) 127 | 128 | if (configurator.hasDevDependency("webpack-build-notifier")) { 129 | const WebpackNotifierPlugin = require("webpack-build-notifier") 130 | plugins.push(new WebpackNotifierPlugin({ 131 | title: `Webpack - ${configurator.type}`, 132 | suppressSuccess: "initial", 133 | sound: false, 134 | })) 135 | } 136 | 137 | if (configurator.hasDevDependency("webpack-notifier")) { 138 | const WebpackNotifierPlugin = require("webpack-notifier") 139 | plugins.push(new WebpackNotifierPlugin({title: `Webpack - ${configurator.type}`})) 140 | } 141 | 142 | // watch common code 143 | let commonSourceDir = configurator.commonSourceDirectory 144 | if (commonSourceDir.endsWith(path.sep + getDefaultRelativeSystemDependentCommonSource())) { 145 | // not src/common, because it is convenient to just put some code into src to use it 146 | commonSourceDir = path.dirname(commonSourceDir) 147 | } 148 | 149 | const alienSourceDir = configurator.getSourceDirectory(configurator.type === "main" ? "renderer" : "main") 150 | const sourceDir = configurator.getSourceDirectory(configurator.type) 151 | configurator.plugins.push(new WatchFilterPlugin(file => { 152 | if (sourceDir != null && isAncestor(file, sourceDir)) { 153 | return true 154 | } 155 | else if (file === commonSourceDir || isAncestor(file, commonSourceDir!!)) { 156 | return alienSourceDir == null || !isAncestor(file, alienSourceDir) 157 | } 158 | else { 159 | return false 160 | } 161 | }, require("debug")(`electron-webpack:watch-${configurator.type}`))) 162 | } 163 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/dev/dev-runner.ts: -------------------------------------------------------------------------------- 1 | import * as BluebirdPromise from "bluebird" 2 | import chalk from "chalk" 3 | import { spawn } from "child_process" 4 | import { readdir, remove } from "fs-extra" 5 | import * as path from "path" 6 | import "source-map-support/register" 7 | import webpack, { Compiler } from "webpack" 8 | import { getElectronWebpackConfiguration, getPackageMetadata } from "../config" 9 | import { HmrServer } from "../electron-main-hmr/HmrServer" 10 | import { configure } from "../main" 11 | import { getFreePort, orNullIfFileNotExist } from "../util" 12 | import { DelayedFunction, getCommonEnv, logError, logProcess, logProcessErrorOutput } from "./devUtil" 13 | import { startRenderer } from "./WebpackDevServerManager" 14 | 15 | const projectDir = process.cwd() 16 | 17 | let socketPath: string | null = null 18 | 19 | const debug = require("debug")("electron-webpack") 20 | 21 | async function getOutDir() { 22 | const electronWebpackConfig = await getElectronWebpackConfiguration({ 23 | projectDir, 24 | packageMetadata: getPackageMetadata(projectDir), 25 | }) 26 | return path.join(electronWebpackConfig.commonDistDirectory!!, "main") 27 | } 28 | 29 | // do not remove main.js to allow IDE to keep breakpoints 30 | async function emptyMainOutput() { 31 | const outDir = await getOutDir() 32 | const files = await orNullIfFileNotExist(readdir(outDir)) 33 | if (files == null) { 34 | return 35 | } 36 | 37 | await BluebirdPromise.map(files.filter(it => !it.startsWith(".") && it !== "main.js"), it => remove(outDir + path.sep + it)) 38 | } 39 | 40 | class DevRunner { 41 | async start() { 42 | const wdsHost = "localhost" 43 | const wdsPort = await getFreePort(wdsHost, 9080) 44 | const env = { 45 | ...getCommonEnv(), 46 | ELECTRON_WEBPACK_WDS_HOST: wdsHost, 47 | ELECTRON_WEBPACK_WDS_PORT: wdsPort, 48 | } 49 | 50 | const hmrServer = new HmrServer() 51 | await Promise.all([ 52 | startRenderer(projectDir, env), 53 | hmrServer.listen() 54 | .then(it => { 55 | socketPath = it 56 | }), 57 | emptyMainOutput() 58 | .then(() => this.startMainCompilation(hmrServer)), 59 | ]) 60 | 61 | hmrServer.ipc.on("error", (error: Error) => { 62 | logError("Main", error) 63 | }) 64 | 65 | const electronArgs = process.env.ELECTRON_ARGS 66 | const outDir = await getOutDir() 67 | const args = electronArgs != null && electronArgs.length > 0 ? JSON.parse(electronArgs) : [`--inspect=${await getFreePort("127.0.0.1", 5858)}`] 68 | args.push(path.join(outDir, "/main.js")) 69 | // Pass remaining arguments to the application. Remove 3 instead of 2, to remove the `dev` argument as well. 70 | args.push(...process.argv.slice(3)) 71 | // we should start only when both start and main are started 72 | startElectron(args, env) 73 | } 74 | 75 | async startMainCompilation(hmrServer: HmrServer) { 76 | const mainConfig = await configure("main", { 77 | production: false, 78 | autoClean: false, 79 | forkTsCheckerLogger: { 80 | info: () => { 81 | // ignore 82 | }, 83 | 84 | warn: (message: string) => { 85 | logProcess("Main", message, chalk.yellow) 86 | }, 87 | 88 | error: (message: string) => { 89 | logProcess("Main", message, chalk.red) 90 | }, 91 | }, 92 | }) 93 | 94 | await new Promise((resolve: (() => void) | null, reject: ((error: Error) => void) | null) => { 95 | const compiler: Compiler = webpack(mainConfig!!) 96 | 97 | const printCompilingMessage = new DelayedFunction(() => { 98 | logProcess("Main", "Compiling...", chalk.yellow) 99 | }) 100 | compiler.hooks.compile.tap("electron-webpack-dev-runner", () => { 101 | hmrServer.beforeCompile() 102 | printCompilingMessage.schedule() 103 | }) 104 | 105 | let watcher: Compiler.Watching | null = compiler.watch({}, (error, stats) => { 106 | printCompilingMessage.cancel() 107 | 108 | if (watcher == null) { 109 | return 110 | } 111 | 112 | if (error != null) { 113 | if (reject == null) { 114 | logError("Main", error) 115 | } 116 | else { 117 | reject(error) 118 | reject = null 119 | } 120 | return 121 | } 122 | 123 | logProcess("Main", stats.toString({ 124 | colors: true, 125 | }), chalk.yellow) 126 | 127 | if (resolve != null) { 128 | resolve() 129 | resolve = null 130 | return 131 | } 132 | 133 | hmrServer.built(stats) 134 | }) 135 | 136 | require("async-exit-hook")((callback: () => void) => { 137 | debug(`async-exit-hook: ${callback == null}`) 138 | const w = watcher 139 | if (w == null) { 140 | return 141 | } 142 | 143 | watcher = null 144 | w.close(() => callback()) 145 | }) 146 | }) 147 | } 148 | } 149 | 150 | async function main() { 151 | const devRunner = new DevRunner() 152 | await devRunner.start() 153 | } 154 | 155 | main() 156 | .catch(error => { 157 | console.error(error) 158 | }) 159 | 160 | function startElectron(electronArgs: Array, env: any) { 161 | const electronProcess = spawn(require("electron").toString(), electronArgs, { 162 | env: { 163 | ...env, 164 | ELECTRON_HMR_SOCKET_PATH: socketPath, 165 | } 166 | }) 167 | 168 | // required on windows 169 | require("async-exit-hook")(() => { 170 | electronProcess.kill("SIGINT") 171 | }) 172 | 173 | let queuedData: string | null = null 174 | electronProcess.stdout.on("data", data => { 175 | data = data.toString() 176 | // do not print the only line - doesn't make sense 177 | if (data.trim() === "[HMR] Updated modules:") { 178 | queuedData = data 179 | return 180 | } 181 | 182 | if (queuedData != null) { 183 | data = queuedData + data 184 | queuedData = null 185 | } 186 | 187 | logProcess("Electron", data, chalk.blue) 188 | }) 189 | 190 | logProcessErrorOutput("Electron", electronProcess) 191 | 192 | electronProcess.on("close", exitCode => { 193 | debug(`Electron exited with exit code ${exitCode}`) 194 | if (exitCode === 100) { 195 | setImmediate(() => { 196 | startElectron(electronArgs, env) 197 | }) 198 | } 199 | else { 200 | (process as any).emit("message", "shutdown") 201 | } 202 | }) 203 | } 204 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/targets/RendererTarget.ts: -------------------------------------------------------------------------------- 1 | import { outputFile, readFile } from "fs-extra" 2 | import { Lazy } from "lazy-val" 3 | import * as path from "path" 4 | import { getConfig } from "read-config-file" 5 | import { DefinePlugin } from "webpack" 6 | import { getDllAssets } from "../configurators/dll" 7 | import { configureVueRenderer } from "../configurators/vue/vue" 8 | import { WebpackConfigurator } from "../main" 9 | import { statOrNull } from "../util" 10 | import { BaseTarget, configureFileLoader } from "./BaseTarget" 11 | 12 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") 13 | 14 | export class BaseRendererTarget extends BaseTarget { 15 | constructor() { 16 | super() 17 | } 18 | 19 | configureRules(configurator: WebpackConfigurator): void { 20 | super.configureRules(configurator) 21 | 22 | configurator.extensions.push(".css") 23 | 24 | const miniLoaders = [MiniCssExtractPlugin.loader, { loader: "css-loader", options: { modules: "global" } }] 25 | const cssHotLoader = configurator.isProduction ? miniLoaders : ["css-hot-loader"].concat(miniLoaders) 26 | if (!configurator.isProduction) { 27 | // https://github.com/shepherdwind/css-hot-loader/issues/37 28 | configurator.entryFiles.unshift("css-hot-loader/hotModuleReplacement") 29 | } 30 | 31 | configurator.rules.push( 32 | { 33 | test: /\.css$/, 34 | use: cssHotLoader, 35 | }, 36 | { 37 | test: /\.less$/, 38 | use: cssHotLoader.concat("less-loader"), 39 | }, 40 | { 41 | test: /\.s([ac])ss$/, 42 | use: cssHotLoader.concat("sass-loader"), 43 | }, 44 | { 45 | test: /\.(png|jpe?g|gif)(\?.*)?$/, 46 | use: { 47 | loader: "url-loader", 48 | options: configureFileLoader("imgs") 49 | } 50 | }, 51 | { 52 | test: /\.svg$/, 53 | use: [ 54 | { 55 | loader: '@svgr/webpack', 56 | }, 57 | { 58 | loader: "url-loader", 59 | options: configureFileLoader("imgs") 60 | } 61 | ] 62 | }, 63 | { 64 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 65 | loader: "url-loader", 66 | options: configureFileLoader("media"), 67 | }, 68 | { 69 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 70 | use: { 71 | loader: "url-loader", 72 | options: configureFileLoader("fonts") 73 | } 74 | }, 75 | ) 76 | 77 | if (configurator.hasDevDependency("ejs-html-loader")) { 78 | configurator.rules.push({ 79 | test: /\.ejs$/, 80 | loader: "ejs-html-loader", 81 | }) 82 | } 83 | 84 | if (configurator.hasDependency("vue")) { 85 | configureVueRenderer(configurator) 86 | } 87 | else { 88 | configurator.rules.push({ 89 | test: /\.(html)$/, 90 | use: { 91 | loader: "html-loader", 92 | } 93 | }) 94 | } 95 | } 96 | 97 | async configurePlugins(configurator: WebpackConfigurator): Promise { 98 | configurator.debug("Add ExtractTextPlugin plugin") 99 | configurator.plugins.push(new MiniCssExtractPlugin({filename: `${configurator.type === "renderer-dll" ? "vendor" : "styles"}.css`})) 100 | 101 | await BaseTarget.prototype.configurePlugins.call(this, configurator) 102 | } 103 | } 104 | 105 | export class RendererTarget extends BaseRendererTarget { 106 | constructor() { 107 | super() 108 | } 109 | 110 | async configurePlugins(configurator: WebpackConfigurator): Promise { 111 | // not configurable for now, as in the electron-vue 112 | const customTemplateFile = path.join(configurator.projectDir, configurator.rendererTemplate) 113 | const HtmlWebpackPlugin = require("html-webpack-plugin") 114 | const nodeModulePath = configurator.isProduction ? null : path.resolve(require.resolve("electron"), "..", "..") 115 | 116 | let template 117 | if (await statOrNull(customTemplateFile)) { 118 | template = await readFile(customTemplateFile, {encoding: "utf8"}) 119 | } 120 | else { 121 | template = getDefaultIndexTemplate() 122 | } 123 | 124 | configurator.plugins.push(new HtmlWebpackPlugin({ 125 | filename: "index.html", 126 | template: await generateIndexFile(configurator, nodeModulePath, template), 127 | minify: false, 128 | nodeModules: nodeModulePath 129 | })) 130 | 131 | if (configurator.isProduction) { 132 | configurator.plugins.push(new DefinePlugin({ 133 | __static: `process.resourcesPath + "/${configurator.staticSourceDirectory}"` 134 | })) 135 | } 136 | else { 137 | const contentBase = [path.join(configurator.projectDir, configurator.staticSourceDirectory), path.join(configurator.commonDistDirectory, "renderer-dll")]; 138 | (configurator.config as any).devServer = { 139 | contentBase, 140 | host: process.env.ELECTRON_WEBPACK_WDS_HOST || "localhost", 141 | port: process.env.ELECTRON_WEBPACK_WDS_PORT || 9080, 142 | hot: true, 143 | overlay: true, 144 | } 145 | } 146 | 147 | await BaseRendererTarget.prototype.configurePlugins.call(this, configurator) 148 | } 149 | } 150 | 151 | async function computeTitle(configurator: WebpackConfigurator): Promise { 152 | const titleFromOptions = configurator.electronWebpackConfiguration.title 153 | if (titleFromOptions == null || titleFromOptions === false) { 154 | return null 155 | } 156 | 157 | if (titleFromOptions !== true) { 158 | return titleFromOptions 159 | } 160 | 161 | let title: string | null | undefined = (configurator.metadata as any).productName 162 | if (title == null) { 163 | const electronBuilderConfig = await getConfig({ 164 | packageKey: "build", 165 | configFilename: "electron-builder", 166 | projectDir: configurator.projectDir, 167 | packageMetadata: new Lazy(() => Promise.resolve(configurator.metadata)) 168 | }) 169 | if (electronBuilderConfig != null) { 170 | title = electronBuilderConfig.result.productName 171 | } 172 | } 173 | 174 | if (title == null) { 175 | title = configurator.metadata.name 176 | } 177 | return title 178 | } 179 | 180 | function getDefaultIndexTemplate() { 181 | return ` 182 | 183 | 184 | 185 | 186 | 187 |
188 | 189 | ` 190 | } 191 | 192 | async function generateIndexFile(configurator: WebpackConfigurator, nodeModulePath: string | null, template: string) { 193 | // do not use add-asset-html-webpack-plugin - no need to copy vendor files to output (in dev mode will be served directly, in production copied) 194 | const assets = await getDllAssets(path.join(configurator.commonDistDirectory, "renderer-dll"), configurator) 195 | const scripts: Array = [] 196 | const css: Array = [] 197 | for (const asset of assets) { 198 | if (asset.endsWith(".js")) { 199 | scripts.push(``) 200 | } 201 | else { 202 | css.push(``) 203 | } 204 | } 205 | 206 | const title = await computeTitle(configurator) 207 | const filePath = path.join(configurator.commonDistDirectory, ".renderer-index-template.html") 208 | 209 | let html = template 210 | if (title) { 211 | html = html.replace("", `${title}`) 212 | } 213 | 214 | if (nodeModulePath) { 215 | html = html.replace("", ``) 216 | } 217 | 218 | html = html.replace("", '') 219 | 220 | if (scripts.length) { 221 | html = html.replace("", `${scripts.join("")}`) 222 | } 223 | 224 | if (css.length) { 225 | html = html.replace("", `${css.join("")}`) 226 | } 227 | 228 | await outputFile(filePath, html) 229 | 230 | return `!!html-loader?minimize=false&attributes=false!${filePath}` 231 | } 232 | -------------------------------------------------------------------------------- /packages/electron-webpack/typings/yargs.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace yargs { 2 | interface Yargs { 3 | argv: any; 4 | 5 | (...args: any[]): any; 6 | 7 | parse(...args: any[]): any; 8 | 9 | reset(): Yargs; 10 | 11 | locale(): string; 12 | 13 | locale(loc: string): Yargs; 14 | 15 | detectLocale(detect: boolean): Yargs; 16 | 17 | terminalWidth(): number; 18 | 19 | alias(shortName: string, longName: string): Yargs; 20 | 21 | alias(aliases: { [shortName: string]: string }): Yargs; 22 | 23 | alias(aliases: { [shortName: string]: string[] }): Yargs; 24 | 25 | array(key: string): Yargs; 26 | 27 | array(keys: string[]): Yargs; 28 | 29 | default(key: string, value: any, description?: string): Yargs; 30 | 31 | default(defaults: { [key: string]: any }, description?: string): Yargs; 32 | 33 | /** 34 | * @deprecated since version 6.6.0 35 | */ 36 | demand(key: string, msg: string): Yargs; 37 | 38 | demand(key: string, required?: boolean): Yargs; 39 | 40 | demand(keys: string[], msg: string): Yargs; 41 | 42 | demand(keys: string[], required?: boolean): Yargs; 43 | 44 | demand(positionals: number, required?: boolean): Yargs; 45 | 46 | demand(positionals: number, msg: string): Yargs; 47 | 48 | demand(positionals: number, max: number, msg?: string): Yargs; 49 | 50 | demandCommand(min: number, minMsg?: string): Yargs; 51 | 52 | demandCommand(min: number, max?: number, minMsg?: string, maxMsg?: string): Yargs; 53 | 54 | demandOption(key: string | string[], msg?: string): Yargs; 55 | 56 | demandOption(key: string | string[], demand?: boolean): Yargs; 57 | 58 | /** 59 | * @deprecated since version 6.6.0 60 | */ 61 | require(key: string, msg: string): Yargs; 62 | 63 | require(key: string, required: boolean): Yargs; 64 | 65 | require(keys: number[], msg: string): Yargs; 66 | 67 | require(keys: number[], required: boolean): Yargs; 68 | 69 | require(positionals: number, required: boolean): Yargs; 70 | 71 | require(positionals: number, msg: string): Yargs; 72 | 73 | /** 74 | * @deprecated since version 6.6.0 75 | */ 76 | required(key: string, msg: string): Yargs; 77 | 78 | required(key: string, required: boolean): Yargs; 79 | 80 | required(keys: number[], msg: string): Yargs; 81 | 82 | required(keys: number[], required: boolean): Yargs; 83 | 84 | required(positionals: number, required: boolean): Yargs; 85 | 86 | required(positionals: number, msg: string): Yargs; 87 | 88 | requiresArg(key: string): Yargs; 89 | 90 | requiresArg(keys: string[]): Yargs; 91 | 92 | describe(key: string, description: string): Yargs; 93 | 94 | describe(descriptions: { [key: string]: string }): Yargs; 95 | 96 | option(key: string, options: Options): Yargs; 97 | 98 | option(options: { [key: string]: Options }): Yargs; 99 | 100 | options(key: string, options: Options): Yargs; 101 | 102 | options(options: { [key: string]: Options }): Yargs; 103 | 104 | usage(message: string, options?: { [key: string]: Options }): Yargs; 105 | 106 | usage(options?: { [key: string]: Options }): Yargs; 107 | 108 | command(command: string, description: string): Yargs; 109 | 110 | command(command: string | Array, description: string, builder: (args: Yargs) => Yargs): Yargs; 111 | 112 | command(command: string | Array, description: string, builder: { [optionName: string]: Options }): Yargs; 113 | 114 | // command(command: string, description: string, builder: { [optionName: string]: Options }, handler: (args: any) => void): Yargs; 115 | 116 | command(command: string | Array, description: string, builder: (args: Yargs) => Yargs, handler: (args: any) => void): Yargs; 117 | 118 | command(command: string | Array, description: string, module: CommandModule): Yargs; 119 | 120 | command(module: CommandModule): Yargs; 121 | 122 | commandDir(dir: string, opts?: RequireDirectoryOptions): Yargs; 123 | 124 | completion(): Yargs; 125 | 126 | completion(cmd: string, fn?: AsyncCompletionFunction): Yargs; 127 | 128 | completion(cmd: string, fn?: SyncCompletionFunction): Yargs; 129 | 130 | completion(cmd: string | undefined, description?: string, fn?: AsyncCompletionFunction): Yargs; 131 | 132 | completion(cmd: string, description?: string, fn?: SyncCompletionFunction): Yargs; 133 | 134 | example(command: string, description: string): Yargs; 135 | 136 | check(func: (Yargs: any, aliases: { [alias: string]: string }) => any): Yargs; 137 | 138 | boolean(key: string): Yargs; 139 | 140 | boolean(keys: string[]): Yargs; 141 | 142 | string(key: string): Yargs; 143 | 144 | string(keys: string[]): Yargs; 145 | 146 | number(key: string): Yargs; 147 | 148 | number(keys: string[]): Yargs; 149 | 150 | choices(choices: Object): Yargs; 151 | 152 | choices(key: string, values: any[]): Yargs; 153 | 154 | config(): Yargs; 155 | 156 | config(explicitConfigurationObject: Object): Yargs; 157 | 158 | config(key: string, description?: string, parseFn?: (configPath: string) => Object): Yargs; 159 | 160 | config(keys: string[], description?: string, parseFn?: (configPath: string) => Object): Yargs; 161 | 162 | config(key: string, parseFn: (configPath: string) => Object): Yargs; 163 | 164 | config(keys: string[], parseFn: (configPath: string) => Object): Yargs; 165 | 166 | conflicts(key: string, value: string): Yargs; 167 | 168 | conflicts(conflicts: { [key: string]: string }): Yargs; 169 | 170 | wrap(columns: number): Yargs; 171 | 172 | strict(): Yargs; 173 | 174 | help(): Yargs; 175 | 176 | help(enableExplicit: boolean): Yargs; 177 | 178 | help(option: string, enableExplicit: boolean): Yargs; 179 | 180 | help(option: string, description?: string, enableExplicit?: boolean): Yargs; 181 | 182 | env(prefix?: string): Yargs; 183 | 184 | env(enable: boolean): Yargs; 185 | 186 | epilog(msg: string): Yargs; 187 | 188 | epilogue(msg: string): Yargs; 189 | 190 | version(version?: string, option?: string, description?: string): Yargs; 191 | 192 | version(version: () => string, option?: string, description?: string): Yargs; 193 | 194 | showHelpOnFail(enable: boolean, message?: string): Yargs; 195 | 196 | showHelp(consoleLevel?: string): Yargs; 197 | 198 | exitProcess(enabled: boolean): Yargs; 199 | 200 | global(key: string): Yargs; 201 | 202 | global(keys: string[]): Yargs; 203 | 204 | group(key: string, groupName: string): Yargs; 205 | 206 | group(keys: string[], groupName: string): Yargs; 207 | 208 | nargs(key: string, count: number): Yargs; 209 | 210 | nargs(nargs: { [key: string]: number }): Yargs; 211 | 212 | normalize(key: string): Yargs; 213 | 214 | normalize(keys: string[]): Yargs; 215 | 216 | implies(key: string, value: string): Yargs; 217 | 218 | implies(implies: { [key: string]: string }): Yargs; 219 | 220 | count(key: string): Yargs; 221 | 222 | count(keys: string[]): Yargs; 223 | 224 | fail(func: (msg: string, err: Error) => any): Yargs; 225 | 226 | coerce(key: string | string[], func: (arg: T) => U): Yargs; 227 | 228 | coerce(opts: { [key: string]: (arg: T) => U; }): Yargs; 229 | 230 | getCompletion(args: string[], done: (completions: string[]) => void): Yargs; 231 | 232 | pkgConf(key: string, cwd?: string): Yargs; 233 | 234 | pkgConf(keys: string[], cwd?: string): Yargs; 235 | 236 | recommendCommands(): Yargs; 237 | 238 | showCompletionScript(): Yargs; 239 | 240 | skipValidation(key: string): Yargs; 241 | 242 | skipValidation(keys: string[]): Yargs; 243 | 244 | updateLocale(obj: Object): Yargs; 245 | 246 | updateStrings(obj: { [key: string]: string }): Yargs; 247 | } 248 | 249 | interface RequireDirectoryOptions { 250 | recurse?: boolean; 251 | extensions?: string[]; 252 | visit?: (commandObject: any, pathToFile?: string, filename?: string) => any; 253 | include?: RegExp | ((pathToFile: string) => boolean); 254 | exclude?: RegExp | ((pathToFile: string) => boolean); 255 | } 256 | 257 | interface Options { 258 | alias?: string | string[]; 259 | array?: boolean; 260 | boolean?: boolean; 261 | choices?: string[]; 262 | coerce?: (arg: any) => any; 263 | config?: boolean; 264 | configParser?: (configPath: string) => Object; 265 | count?: boolean; 266 | default?: any; 267 | defaultDescription?: string; 268 | /** @deprecated since version 6.6.0 */ 269 | demand?: boolean | string; 270 | demandOption?: boolean | string; 271 | desc?: string; 272 | describe?: string; 273 | description?: string; 274 | global?: boolean; 275 | group?: string; 276 | nargs?: number; 277 | normalize?: boolean; 278 | number?: boolean; 279 | require?: boolean | string; 280 | required?: boolean | string; 281 | requiresArg?: boolean | string; 282 | skipValidation?: boolean; 283 | string?: boolean; 284 | type?: "array" | "boolean" | "count" | "number" | "string"; 285 | } 286 | 287 | interface CommandModule { 288 | aliases?: string[] | string; 289 | builder?: CommandBuilder; 290 | command?: string[] | string; 291 | describe?: string | false; 292 | handler: (args: any) => void; 293 | } 294 | 295 | type CommandBuilder = { [key: string]: Options } | ((args: Yargs) => Yargs); 296 | type SyncCompletionFunction = (current: string, Yargs: any) => string[]; 297 | type AsyncCompletionFunction = (current: string, Yargs: any, done: (completion: string[]) => void) => void; 298 | } 299 | 300 | declare module "yargs" { 301 | const yargs: yargs.Yargs; 302 | export default yargs; 303 | } 304 | -------------------------------------------------------------------------------- /packages/electron-webpack/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as BluebirdPromise from "bluebird" 2 | import { config as dotEnvConfig } from "dotenv" 3 | import dotEnvExpand from "dotenv-expand" 4 | import { pathExists, readJson } from "fs-extra" 5 | import { Lazy } from "lazy-val" 6 | import * as path from "path" 7 | import { validateConfig } from "read-config-file" 8 | import { deepAssign } from "read-config-file/out/deepAssign" 9 | import "source-map-support/register" 10 | import { Configuration, Plugin, RuleSetRule } from "webpack" 11 | import merge from "webpack-merge" 12 | import { getElectronWebpackConfiguration, getPackageMetadata } from "./config" 13 | import { configureTypescript } from "./configurators/ts" 14 | import { configureVue } from "./configurators/vue/vue" 15 | import { ConfigurationEnv, ConfigurationType, ElectronWebpackConfiguration, PackageMetadata, PartConfiguration } from "./core" 16 | import { BaseTarget } from "./targets/BaseTarget" 17 | import { MainTarget } from "./targets/MainTarget" 18 | import { BaseRendererTarget, RendererTarget } from "./targets/RendererTarget" 19 | import { getFirstExistingFile } from "./util" 20 | 21 | export { ElectronWebpackConfiguration } from "./core" 22 | 23 | const _debug = require("debug") 24 | 25 | // noinspection JSUnusedGlobalSymbols 26 | export function getAppConfiguration(env: ConfigurationEnv) { 27 | return BluebirdPromise.filter([configure("main", env), configure("renderer", env)], it => it != null) 28 | } 29 | 30 | // noinspection JSUnusedGlobalSymbols 31 | export function getMainConfiguration(env: ConfigurationEnv) { 32 | return configure("main", env) 33 | } 34 | 35 | // noinspection JSUnusedGlobalSymbols 36 | export function getRendererConfiguration(env: ConfigurationEnv) { 37 | return configure("renderer", env) 38 | } 39 | 40 | // in the future, if need, isRenderer = true arg can be added 41 | // noinspection JSUnusedGlobalSymbols 42 | export function getDllConfiguration(env: ConfigurationEnv) { 43 | return configure("renderer-dll", env) 44 | } 45 | 46 | // noinspection JSUnusedGlobalSymbols 47 | export async function getTestConfiguration(env: ConfigurationEnv) { 48 | const configurator = await createConfigurator("test", env) 49 | return await configurator.configure({ 50 | testComponents: path.join(process.cwd(), "src/renderer/components/testComponents.ts"), 51 | }) 52 | } 53 | 54 | export class WebpackConfigurator { 55 | readonly projectDir: string 56 | 57 | private electronVersionPromise = new Lazy(() => getInstalledElectronVersion(this.projectDir)) 58 | 59 | readonly isRenderer: boolean 60 | readonly isProduction: boolean 61 | readonly isTest = this.type === "test" 62 | 63 | readonly sourceDir: string 64 | readonly staticSourceDirectory: string 65 | readonly commonSourceDirectory: string 66 | readonly commonDistDirectory: string 67 | 68 | readonly rendererTemplate: string 69 | 70 | readonly debug = _debug(`electron-webpack:${this.type}`) 71 | 72 | private _configuration: Configuration | null = null 73 | 74 | get config(): Configuration { 75 | return this._configuration!! 76 | } 77 | 78 | readonly rules: Array = [] 79 | readonly plugins: Array = [] 80 | 81 | // js must be first - e.g. iView has two files loading-bar.js and loading-bar.vue - when we require "loading-bar", js file must be resolved and not vue 82 | readonly extensions: Array = [".js", ".json", ".node"] 83 | 84 | private _electronVersion: string | null = null 85 | 86 | get electronVersion(): string { 87 | return this._electronVersion!! 88 | } 89 | 90 | readonly entryFiles: Array = [] 91 | 92 | // electronWebpackConfiguration expected to be resolved (use getElectronWebpackConfiguration()) 93 | constructor(readonly type: ConfigurationType, readonly env: ConfigurationEnv, readonly electronWebpackConfiguration: ElectronWebpackConfiguration, readonly metadata: PackageMetadata) { 94 | if (metadata.dependencies == null) { 95 | metadata.dependencies = {} 96 | } 97 | if (metadata.devDependencies == null) { 98 | metadata.devDependencies = {} 99 | } 100 | 101 | this.projectDir = electronWebpackConfiguration.projectDir!! 102 | this.isRenderer = type.startsWith("renderer") 103 | process.env.BABEL_ENV = type 104 | 105 | this.isProduction = this.env.production == null ? process.env.NODE_ENV === "production" : this.env.production 106 | 107 | this.debug(`isProduction: ${this.isProduction}`) 108 | 109 | this.sourceDir = this.getSourceDirectory(this.type)!! 110 | 111 | this.staticSourceDirectory = this.electronWebpackConfiguration.staticSourceDirectory!! 112 | this.commonSourceDirectory = this.electronWebpackConfiguration.commonSourceDirectory!! 113 | this.commonDistDirectory = this.electronWebpackConfiguration.commonDistDirectory!! 114 | 115 | this.rendererTemplate = (this.electronWebpackConfiguration.renderer && this.electronWebpackConfiguration.renderer.template) || "src/index.ejs" 116 | } 117 | 118 | /** 119 | * Returns null if code processing for type is disabled. 120 | */ 121 | getSourceDirectory(type: ConfigurationType): string | null { 122 | const part = this.getPartConfiguration(type) 123 | if (part === null || (part != null && part.sourceDirectory === null)) { 124 | // part or sourceDirectory is explicitly set to null 125 | return null 126 | } 127 | 128 | const result = part == null ? null : part.sourceDirectory 129 | if (result == null) { 130 | return path.join(this.projectDir, "src", type.startsWith("renderer") || type === "test" ? "renderer" : type) 131 | } 132 | else { 133 | return path.resolve(this.projectDir, result) 134 | } 135 | } 136 | 137 | getPartConfiguration(type: ConfigurationType): PartConfiguration | null | undefined { 138 | if (type === "main") { 139 | return this.electronWebpackConfiguration.main 140 | } 141 | else { 142 | return this.electronWebpackConfiguration.renderer 143 | } 144 | } 145 | 146 | hasDependency(name: string) { 147 | return name in this.metadata.dependencies || this.hasDevDependency(name) 148 | } 149 | 150 | hasDevDependency(name: string) { 151 | return name in this.metadata.devDependencies 152 | } 153 | 154 | /** 155 | * Returns the names of devDependencies that match a given string or regex. 156 | * If no matching dependencies are found, an empty array is returned. 157 | * 158 | * @return list of matching dependency names, e.g. `["@babel/preset-react", "@babel/preset-stage-0"]` 159 | */ 160 | getMatchingDevDependencies(options: GetMatchingDevDependenciesOptions = {}) { 161 | const includes = options.includes || [] 162 | const excludes = new Set(options.excludes || []) 163 | return Object.keys(this.metadata.devDependencies) 164 | .filter(name => !excludes.has(name) && includes.some(prefix => name.startsWith(prefix))) 165 | } 166 | 167 | async configure(entry?: { [key: string]: any } | null) { 168 | // noinspection SpellCheckingInspection 169 | this._configuration = { 170 | context: this.projectDir, 171 | devtool: this.isProduction || this.isTest ? "nosources-source-map" : "eval-source-map", 172 | externals: this.computeExternals(), 173 | node: { 174 | __dirname: !this.isProduction, 175 | __filename: !this.isProduction, 176 | }, 177 | output: { 178 | filename: "[name].js", 179 | chunkFilename: "[name].bundle.js", 180 | libraryTarget: "commonjs2", 181 | path: path.join(this.commonDistDirectory, this.type) 182 | }, 183 | target: this.isTest ? "node" : `electron-${this.type === "renderer-dll" ? "renderer" : this.type}` as any, 184 | resolve: { 185 | alias: { 186 | "@": this.sourceDir, 187 | common: this.commonSourceDirectory, 188 | }, 189 | extensions: this.extensions, 190 | }, 191 | module: { 192 | rules: this.rules, 193 | }, 194 | plugins: this.plugins, 195 | } 196 | 197 | if (entry != null) { 198 | this._configuration.entry = entry 199 | } 200 | 201 | // if electronVersion not specified, use latest 202 | this._electronVersion = this.electronWebpackConfiguration.electronVersion || await this.electronVersionPromise.value || "3.0.7" 203 | const target = (() => { 204 | switch (this.type) { 205 | case "renderer": return new RendererTarget() 206 | case "renderer-dll": return new BaseRendererTarget() 207 | case "test": return new BaseRendererTarget() 208 | case "main": return new MainTarget() 209 | default: return new BaseTarget() 210 | } 211 | })() 212 | this.debug(`Target class: ${target.constructor.name}`) 213 | target.configureRules(this) 214 | await Promise.all([target.configurePlugins(this), configureTypescript(this)]) 215 | configureVue(this) 216 | 217 | if (this.debug.enabled) { 218 | this.debug(`\n\n${this.type} config:` + JSON.stringify(this._configuration, null, 2) + "\n\n") 219 | } 220 | 221 | if (this.config.entry == null) { 222 | this.entryFiles.push((await computeEntryFile(this.sourceDir, this.projectDir))!!) 223 | this.config.entry = { 224 | [this.type]: this.entryFiles, 225 | } 226 | 227 | const mainConfiguration = this.electronWebpackConfiguration.main || {} 228 | let extraEntries = mainConfiguration.extraEntries 229 | if (this.type === "main" && extraEntries != null) { 230 | if (typeof extraEntries === "string") { 231 | extraEntries = [extraEntries] 232 | } 233 | 234 | if (Array.isArray(extraEntries)) { 235 | for (const p of extraEntries) { 236 | this.config.entry[path.basename(p, path.extname(p))] = p 237 | } 238 | } 239 | else { 240 | Object.assign(this.config.entry, extraEntries) 241 | } 242 | } 243 | } 244 | 245 | // noinspection ES6RedundantAwait 246 | this._configuration = await Promise.resolve(this.applyCustomModifications(this.config)) 247 | 248 | return this.config 249 | } 250 | 251 | private applyCustomModifications(config: Configuration): Configuration { 252 | const { renderer, main } = this.electronWebpackConfiguration 253 | 254 | const applyCustom = (configPath: string) => { 255 | const customModule = require(path.join(this.projectDir, configPath)) 256 | if (typeof customModule === "function") { 257 | return customModule(config, this) 258 | } 259 | else { 260 | return merge.smart(config, customModule) 261 | } 262 | } 263 | 264 | if (this.type === "renderer" && renderer && renderer.webpackConfig) { 265 | return applyCustom(renderer.webpackConfig) 266 | } 267 | else if (this.type === "renderer-dll" && renderer && renderer.webpackDllConfig) { 268 | return applyCustom(renderer.webpackDllConfig) 269 | } 270 | else if (this.type === "main" && main && main.webpackConfig) { 271 | return applyCustom(main.webpackConfig) 272 | } 273 | else { 274 | return config 275 | } 276 | } 277 | 278 | private computeExternals() { 279 | const whiteListedModules = new Set(this.electronWebpackConfiguration.whiteListedModules || []) 280 | if (this.isRenderer) { 281 | whiteListedModules.add("react") 282 | whiteListedModules.add("react-dom") 283 | whiteListedModules.add("vue") 284 | } 285 | 286 | const filter = (name: string) => !name.startsWith("@types/") && (whiteListedModules == null || !whiteListedModules.has(name)) 287 | const externals: Array = Object.keys(this.metadata.dependencies).filter(filter) 288 | externals.push("electron") 289 | externals.push("webpack") 290 | // because electron-devtools-installer specified in the devDependencies, but required in the index.dev 291 | externals.push("electron-devtools-installer") 292 | if (this.type === "main") { 293 | externals.push("webpack/hot/log-apply-result") 294 | externals.push("electron-webpack/out/electron-main-hmr/HmrClient") 295 | externals.push("source-map-support/source-map-support.js") 296 | } 297 | 298 | if (this.electronWebpackConfiguration.externals != null) { 299 | return externals.concat(this.electronWebpackConfiguration.externals) 300 | } 301 | 302 | return externals 303 | } 304 | } 305 | 306 | const schemeDataPromise = new Lazy(() => readJson(path.join(__dirname, "..", "scheme.json"))) 307 | 308 | export async function createConfigurator(type: ConfigurationType, env: ConfigurationEnv | null) { 309 | if (env != null) { 310 | // allow to pass as `--env.autoClean=false` webpack arg 311 | const _env: any = env 312 | for (const name of ["minify", "autoClean", "production"]) { 313 | if (_env[name] === "true") { 314 | _env[name] = true 315 | } 316 | else if (_env[name] === "false") { 317 | _env[name] = false 318 | } 319 | } 320 | } 321 | 322 | if (env == null) { 323 | env = {} 324 | } 325 | 326 | const projectDir = (env.configuration || {}).projectDir || process.cwd() 327 | const packageMetadata = getPackageMetadata(projectDir) 328 | const electronWebpackConfig = await getElectronWebpackConfiguration({ 329 | packageMetadata, 330 | projectDir, 331 | }) 332 | if (env.configuration != null) { 333 | deepAssign(electronWebpackConfig, env.configuration) 334 | } 335 | 336 | await validateConfig(electronWebpackConfig, schemeDataPromise, message => { 337 | return `${message} 338 | 339 | How to fix: 340 | 1. Open https://webpack.electron.build/configuration 341 | 2. Search the option name on the page. 342 | * Not found? The option was deprecated or not exists (check spelling). 343 | * Found? Check that the option in the appropriate place. e.g. "sourceDirectory" only in the "main" or "renderer", not in the root. 344 | ` 345 | }) 346 | return new WebpackConfigurator(type, env, electronWebpackConfig, await packageMetadata.value) 347 | } 348 | 349 | export async function configure(type: ConfigurationType, env: ConfigurationEnv | null): Promise { 350 | const configurator = await createConfigurator(type, env) 351 | const sourceDir = configurator.sourceDir 352 | // explicitly set to null - do not handle at all and do not show info message 353 | if (sourceDir === null) { 354 | return null 355 | } 356 | 357 | const processEnv = configurator.isProduction ? "production" : "development" 358 | const dotEnvPath = path.resolve(configurator.projectDir, ".env") 359 | const dotenvFiles = [ 360 | `${dotEnvPath}.${processEnv}.local`, 361 | `${dotEnvPath}.${processEnv}`, 362 | `${dotEnvPath}.local`, 363 | dotEnvPath, 364 | ] 365 | 366 | for (const file of dotenvFiles) { 367 | const exists = await pathExists(file) 368 | if (exists) { 369 | dotEnvExpand( 370 | dotEnvConfig({ 371 | path: file 372 | }) 373 | ) 374 | } 375 | } 376 | return await configurator.configure() 377 | } 378 | 379 | async function computeEntryFile(srcDir: string, projectDir: string): Promise { 380 | const candidates: Array = [] 381 | for (const ext of ["ts", "js", "tsx", "jsx"]) { 382 | for (const name of ["index", "main", "app"]) { 383 | candidates.push(`${name}.${ext}`) 384 | } 385 | } 386 | 387 | const file = await getFirstExistingFile(candidates, srcDir) 388 | if (file == null) { 389 | throw new Error(`Cannot find entry file ${path.relative(projectDir, path.join(srcDir, "index.ts"))} (or main.ts, or app.ts, or index.js, or main.js, or app.js)`) 390 | } 391 | return file 392 | } 393 | 394 | async function getInstalledElectronVersion(projectDir: string) { 395 | for (const name of ["electron", "electron-prebuilt", "electron-prebuilt-compile"]) { 396 | try { 397 | return (await readJson(path.join(projectDir, "node_modules", name, "package.json"))).version 398 | } 399 | catch (e) { 400 | if (e.code !== "ENOENT") { 401 | throw e 402 | } 403 | } 404 | } 405 | } 406 | 407 | export interface GetMatchingDevDependenciesOptions { 408 | /** 409 | * The list of prefixes to include, e.g. `["babel-preset-"]`. 410 | */ 411 | includes?: Array 412 | /** 413 | * The list of names to exclude. 414 | */ 415 | excludes?: Array 416 | } 417 | --------------------------------------------------------------------------------