From 72a4b71bab1624f7887af9374ddbd6b55b9f21fa Mon Sep 17 00:00:00 2001 From: Diablohu Date: Fri, 29 Dec 2023 17:43:04 +0800 Subject: [PATCH] `koot-electron` - make the `main` file can be hot reloaded --- .vscode/settings.json | 2 +- packages/koot-electron/libs/modify-config.js | 297 ++++++++++++------ packages/koot-electron/package.json | 6 +- .../validate-config/add-default-values.js | 2 +- .../__snapshots__/i18n.test.js.snap | 10 - .../react-classic-import.test.js.snap | 3 - .../react-replace-memo-in-dev.test.js.snap | 4 - test/projects/simple/package.json | 2 +- test/projects/simple2/package.json | 2 +- test/projects/standard/package.json | 2 +- 10 files changed, 201 insertions(+), 129 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1bd2447f3..22c360dff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "editor.tabSize": 4, "editor.insertSpaces": true, "editor.codeActionsOnSave": { - "source.fixAll": true + "source.fixAll": "explicit" }, "eslint.codeActionsOnSave.mode": "problems", "eslint.validate": [ diff --git a/packages/koot-electron/libs/modify-config.js b/packages/koot-electron/libs/modify-config.js index 34f53b566..6814e43ac 100644 --- a/packages/koot-electron/libs/modify-config.js +++ b/packages/koot-electron/libs/modify-config.js @@ -3,9 +3,10 @@ const fs = require('fs-extra'); const path = require('path'); const os = require('os'); +const { exec } = require('child_process'); const webpack = require('webpack'); const sanitize = require('sanitize-filename'); -const resolve = require('resolve'); +const resolveFunc = require('resolve'); const inquirer = require('inquirer'); const merge = require('lodash/merge'); const sharp = require('sharp'); @@ -17,6 +18,7 @@ const { keyConfigBuildDll, keyConfigIcons, CLIENT_ROOT_PATH, + buildManifestFilename, } = require('koot/defaults/before-build'); const getDirDevTmp = require('koot/libs/get-dir-dev-tmp'); const getLogMsg = require('koot/libs/get-log-msg'); @@ -31,6 +33,9 @@ const defaultDistPackageJson = require('./defaults/dist-package-json'); // ============================================================================ +/** + * 修改 appConfig + */ const modifyConfig = async (appConfig) => { if (appConfig[keyConfigBuildDll]) return; @@ -54,10 +59,10 @@ const modifyConfig = async (appConfig) => { if (typeof webpackBefore === 'function') await webpackBefore(...args); }; - // after - 如果是开发环境,自动启动;如果是生产环境,Pack Electron App + // after - 如果是生产环境,Pack Electron App + // 开发环境的操作整合在 webpack 输出文件后自动执行 appConfig.webpackAfter = async (...args) => { if (process.env.WEBPACK_BUILD_ENV === 'dev') { - await afterBuildAutoOpen(...args); } else if (!isFromStartCommand()) { console.log(' '); const { doPackaging } = await inquirer.prompt({ @@ -84,63 +89,147 @@ module.exports = modifyConfig; // ============================================================================ const files = {}; +let electronMainProcess; +let electronMainProcessActive = false; const getElectronFilesFolder = (appConfig) => process.env.WEBPACK_BUILD_ENV === 'dev' ? getDirDevTmp('electron') : path.resolve(appConfig[CLIENT_ROOT_PATH]); -const buildElectronMain = async (appConfig) => { - const dest = getElectronFilesFolder(appConfig); - const { electron: electronConfig = {}, dist } = appConfig; - const { - main, - mainOutput, - /** electron-builder 配置对象,会写入到打包结果的 `package.json` 中 */ - build = {}, - } = electronConfig; - const msg = getLogMsg( - false, - 'electron', - __('building_main', { file: mainOutput }) - ); - const waiting = spinner(msg + '...'); - - files.main = path.resolve(dest, mainOutput); - - const cwd = getCwd(); - const projectPkg = require(path.resolve(cwd, 'package.json')); - const installedElectronDir = path.dirname( - resolve.sync('electron', { - basedir: cwd, - }) - ); - const installedElectronVersion = require(path.resolve( - installedElectronDir, - 'package.json' - )).version; - const webpackConfig = await newWebpackConfig( - merge({}, defaultWebpackConfigMain, { - entry: { - main, +/** + * - 生产环境: 仅打包 + * - 开发环境: 热更新打包,并自动打开主进程 + */ +const buildElectronMain = async (appConfig) => + new Promise(async (resolve, reject) => { + const dest = getElectronFilesFolder(appConfig); + const { electron: electronConfig = {}, dist } = appConfig; + const { + main, + mainOutput, + /** electron-builder 配置对象,会写入到打包结果的 `package.json` 中 */ + build = {}, + } = electronConfig; + const msg = getLogMsg( + false, + 'electron', + __('building_main', { file: mainOutput }) + ); + const waiting = spinner(msg + '...'); + + files.main = path.resolve(dest, mainOutput); + + const cwd = getCwd(); + const projectPkg = require(path.resolve(cwd, 'package.json')); + const installedElectronDir = path.dirname( + resolveFunc.sync('electron', { + basedir: cwd, + }) + ); + const installedElectronVersion = require(path.resolve( + installedElectronDir, + 'package.json' + )).version; + + const thisConfig = await newWebpackConfig( + merge({}, defaultWebpackConfigMain, { + mode: + process.env.WEBPACK_BUILD_ENV === 'dev' + ? 'development' + : 'production', + name: 'koot-electron-main', + entry: { + main, + }, + output: { + path: dest, + }, + node: { + global: true, + }, + watch: process.env.WEBPACK_BUILD_ENV === 'dev' ? true : false, + optimization: { + splitChunks: false, + removeAvailableModules: false, + mergeDuplicateChunks: false, + concatenateModules: false, + }, + performance: { + maxEntrypointSize: 100 * 1024 * 1024, + maxAssetSize: 100 * 1024 * 1024, + }, + stats: 'summary', + }), + appConfig + ); + + thisConfig.plugins.push({ + apply: (compiler) => { + // 输出文件后执行 + compiler.hooks.afterEmit.tap( + 'KootElectronBuildMainAfterEmitPlugin', + (compilation) => { + // 添加 package.json + fs.writeJsonSync( + path.resolve(dist, 'package.json'), + merge({}, defaultDistPackageJson, { + name: sanitize(appConfig.name || '') + .toLowerCase() + .replace(/ /g, '-'), + main: path.relative(dist, files.main), + description: + projectPkg.description || + defaultDistPackageJson.description, + version: + projectPkg.version || + defaultDistPackageJson.version, + author: + projectPkg.author || + defaultDistPackageJson.author, + build, + devDependencies: { + electron: installedElectronVersion, + }, + }), + { + spaces: 4, + } + ); + + // 如果是开发环境,自动杀死 Electron 主进程(如果有),并打开主进程 + if (process.env.WEBPACK_BUILD_ENV === 'dev') { + if (electronMainProcessActive) { + killAll(electronMainProcess.pid).then(() => { + electronMainProcess = undefined; + openElectronMainProcess(appConfig); + }); + } else { + // 等待 buildManifestFilename 文件出现 + const fileFlagBuilding = path.resolve( + getDirDevTmp(), + buildManifestFilename + ); + new Promise((resolve) => { + const wait = () => + setTimeout(() => { + if (fs.existsSync(fileFlagBuilding)) + return resolve(); + wait(); + }, 500); + wait(); + }).then(() => { + openElectronMainProcess(appConfig); + }); + } + } + } + ); }, - output: { - path: dest, - }, - }), - appConfig - ); - - // console.log('buildElectronMain', { - // dest, - // electronConfig, - // webpackConfig, - // appIcon, - // }); + }); - try { - await new Promise((resolve, reject) => { - webpack(webpackConfig, (err, stats) => { + try { + webpack(thisConfig, (err, stats) => { if (err) return reject(err); const info = stats.toJson(); @@ -160,73 +249,45 @@ const buildElectronMain = async (appConfig) => { spinner(msg).succeed(); resolve(stats); }); - }); - } catch (err) { - waiting.fail(); - console.error(err); - throw err; - } - - // 添加 package.json - await fs.writeJson( - path.resolve(dist, 'package.json'), - merge({}, defaultDistPackageJson, { - name: sanitize(appConfig.name || '') - .toLowerCase() - .replace(/ /g, '-'), - main: path.relative(dist, files.main), - description: - projectPkg.description || defaultDistPackageJson.description, - version: projectPkg.version || defaultDistPackageJson.version, - author: projectPkg.author || defaultDistPackageJson.author, - build, - devDependencies: { - electron: installedElectronVersion, - }, - }), - { - spaces: 4, + } catch (err) { + waiting.fail(); + console.error(err); + reject(err); } - ); -}; + }); -let opened = false; -// let doKill = false; -const afterBuildAutoOpen = async (appConfig) => { - if (opened) return; +/** + * 打开 Electron 主进程 + */ +const openElectronMainProcess = async (appConfig) => { + if (electronMainProcessActive) return; const { main } = files; const cwd = getCwd(); const cmd = `electron ${main}`; const chunks = cmd.split(' '); - const child = require('child_process').spawn(chunks.shift(), chunks, { - stdio: 'inherit', - shell: true, - cwd, - }); - opened = true; + electronMainProcess = require('child_process').spawn( + chunks.shift(), + chunks, + { + stdio: 'inherit', + shell: true, + cwd, + detached: process.platform !== 'win32', + } + ); + electronMainProcessActive = true; - child.on('close', () => { - // if (!doKill) + electronMainProcess.on('close', () => { console.log(' '); console.log(getLogMsg('warning', 'electron', __('dev_window_closed'))); console.log(' '); - opened = false; - // process.exit(1); + electronMainProcessActive = false; }); - child.on('error', (...args) => { + electronMainProcess.on('error', (...args) => { console.error(...args); }); - // const exitHandler = async (...args) => { - // doKill = true; - // // child.kill('SIGINT'); - // }; - // process.on('exit', exitHandler); - // process.on('SIGINT', exitHandler); - // process.on('SIGUSR1', exitHandler); - // process.on('SIGUSR2', exitHandler); - // process.on('uncaughtException', exitHandler); }; const packElectron = async (appConfig) => { @@ -327,3 +388,31 @@ const modifyWebpackConfig = async (webpackConfig, appConfig) => { return webpackConfig; }; + +/** + * https://medium.com/@almenon214/killing-processes-with-node-772ffdd19aad + * + * kills the process and all its children + * If you are on linux process needs to be launched in detached state + * @param pid process identifier + * @param signal kill signal + */ +const killAll = (pid, signal = 'SIGTERM') => + new Promise((resolve, reject) => { + if (process.platform === 'win32') { + exec(`taskkill /PID ${pid} /T /F`, (error, stdout, stderr) => { + // console.log("taskkill stdout: " + stdout) + // console.log("taskkill stderr: " + stderr) + if (error) { + // console.log("error: " + error.message) + return reject(error); + } + resolve(stdout); + }); + } else { + // see https://nodejs.org/api/child_process.html#child_process_options_detached + // If pid is less than -1, then sig is sent to every process in the process group whose ID is -pid. + process.kill(-pid, signal); + resolve(); + } + }); diff --git a/packages/koot-electron/package.json b/packages/koot-electron/package.json index 3c437993a..6e0b68059 100644 --- a/packages/koot-electron/package.json +++ b/packages/koot-electron/package.json @@ -24,11 +24,11 @@ }, "homepage": "https://github.com/cmux/koot", "engines": { - "node": ">= 8.9.0" + "node": ">= 18.0.0" }, "dependencies": { - "electron": "^19.0.8", - "electron-builder": "^23.1.0" + "electron": "^28.1.0", + "electron-builder": "^24.9.1" }, "devDependencies": { "koot": "^0.15.12", diff --git a/packages/koot/libs/validate-config/add-default-values.js b/packages/koot/libs/validate-config/add-default-values.js index 59da64001..f7e555abc 100644 --- a/packages/koot/libs/validate-config/add-default-values.js +++ b/packages/koot/libs/validate-config/add-default-values.js @@ -43,7 +43,7 @@ module.exports = async (projectDir, config) => { !Array.isArray(config[key]) && !Array.isArray(defaultValues[key]) ) { - config[key] = merge({}, config[key], defaultValues[key]); + config[key] = merge({}, defaultValues[key], config[key]); } }); diff --git a/test/cases/babel-plugins/__snapshots__/i18n.test.js.snap b/test/cases/babel-plugins/__snapshots__/i18n.test.js.snap index 60af28519..efe962f23 100644 --- a/test/cases/babel-plugins/__snapshots__/i18n.test.js.snap +++ b/test/cases/babel-plugins/__snapshots__/i18n.test.js.snap @@ -16,7 +16,6 @@ exports[`Babel Plugin: i18n 2 1`] = `"\\"O_VALUE_1\\";"`; exports[`Babel Plugin: i18n 3 1`] = ` "import __ from \\"koot/i18n/translate\\"; - __({ \\"value1\\": \\"O_VALUE_1\\", \\"value_with_parameter\\": \\"O_VALUE_PARA_\${insert}\\", @@ -30,7 +29,6 @@ exports[`Babel Plugin: i18n 4 1`] = `"\\"O_VALUE_PARA_\${insert}\\";"`; exports[`Babel Plugin: i18n 5 1`] = ` "import __ from \\"koot/i18n/translate\\"; - __({ \\"value1\\": \\"O_VALUE_1\\", \\"value_with_parameter\\": \\"O_VALUE_PARA_\${insert}\\", @@ -42,7 +40,6 @@ __({ exports[`Babel Plugin: i18n 6 1`] = ` "import __ from \\"koot/i18n/translate\\"; - __(\\"O_VALUE_PARA_\${insert}\\", { insert: 'TEST' });" @@ -50,7 +47,6 @@ __(\\"O_VALUE_PARA_\${insert}\\", { exports[`Babel Plugin: i18n 7 1`] = ` "import __ from \\"koot/i18n/translate\\"; - __({ \\"value1\\": \\"O_VALUE_1\\", \\"value_with_parameter\\": \\"O_VALUE_PARA_\${insert}\\", @@ -64,7 +60,6 @@ __({ exports[`Babel Plugin: i18n 8 1`] = ` "import __ from \\"koot/i18n/translate\\"; - __({ \\"value1\\": \\"O_VALUE_1\\", \\"value_with_parameter\\": \\"O_VALUE_PARA_\${insert}\\", @@ -79,7 +74,6 @@ __({ exports[`Babel Plugin: i18n 9 1`] = ` "import __ from \\"koot/i18n/translate\\"; const t = \\"value1\\"; - __({ \\"value1\\": \\"O_VALUE_1\\", \\"value_with_parameter\\": \\"O_VALUE_PARA_\${insert}\\", @@ -92,7 +86,6 @@ __({ exports[`Babel Plugin: i18n 10 1`] = ` "import __ from \\"koot/i18n/translate\\"; let t; - __(\\"O_VALUE_PARA_\${insert}\\", { insert: t || undefined });" @@ -101,7 +94,6 @@ __(\\"O_VALUE_PARA_\${insert}\\", { exports[`Babel Plugin: i18n 11 1`] = ` "import __ from \\"koot/i18n/translate\\"; let t; - __(\\"O_VALUE_PARA_\${insert}\\", { insert: t || 'undefined' });" @@ -110,11 +102,9 @@ __(\\"O_VALUE_PARA_\${insert}\\", { exports[`Babel Plugin: i18n 12 1`] = ` "import __ from \\"koot/i18n/translate\\"; let t; - __(\\"O_VALUE_PARA_\${insert}\\", { insert: t || undefined }); - __({ \\"value1\\": \\"O_VALUE_1\\", \\"value_with_parameter\\": \\"O_VALUE_PARA_\${insert}\\", diff --git a/test/cases/babel-plugins/__snapshots__/react-classic-import.test.js.snap b/test/cases/babel-plugins/__snapshots__/react-classic-import.test.js.snap index 7345a4721..6d754d914 100644 --- a/test/cases/babel-plugins/__snapshots__/react-classic-import.test.js.snap +++ b/test/cases/babel-plugins/__snapshots__/react-classic-import.test.js.snap @@ -21,18 +21,15 @@ console.log(ttt);" exports[`Babel Plugin: react-classic-import hasRequire 1`] = ` "const React = require('react'); - const ttt = 'aaa'; console.log(ttt);" `; exports[`Babel Plugin: react-classic-import hasRequire2 1`] = ` "import React from \\"react\\"; - const { memo } = require('react'); - const ttt = 'aaa'; console.log(ttt);" `; diff --git a/test/cases/babel-plugins/__snapshots__/react-replace-memo-in-dev.test.js.snap b/test/cases/babel-plugins/__snapshots__/react-replace-memo-in-dev.test.js.snap index 966c6055d..23f71bb36 100644 --- a/test/cases/babel-plugins/__snapshots__/react-replace-memo-in-dev.test.js.snap +++ b/test/cases/babel-plugins/__snapshots__/react-replace-memo-in-dev.test.js.snap @@ -2,24 +2,20 @@ exports[`Babel Plugin: react-replace-memo-in-dev test1 1`] = ` "import React from 'react'; - const A = () => 'abc';" `; exports[`Babel Plugin: react-replace-memo-in-dev test2 1`] = ` "import React2 from 'react'; - const A = () => 'abc';" `; exports[`Babel Plugin: react-replace-memo-in-dev test3 1`] = ` "import React, { memo } from 'react'; - const A = () => 'abc';" `; exports[`Babel Plugin: react-replace-memo-in-dev test4 1`] = ` "import React, { memo as memo2 } from 'react'; - const A = () => 'abc';" `; diff --git a/test/projects/simple/package.json b/test/projects/simple/package.json index a2a100607..43b59ea51 100644 --- a/test/projects/simple/package.json +++ b/test/projects/simple/package.json @@ -67,7 +67,7 @@ "private": true, "sideEffects": false, "koot": { - "version": "0.15.10" + "version": "0.15.12" }, "devDependencies": { "@types/inquirer": "^8.2.1", diff --git a/test/projects/simple2/package.json b/test/projects/simple2/package.json index e4c536060..834cc31f3 100644 --- a/test/projects/simple2/package.json +++ b/test/projects/simple2/package.json @@ -66,7 +66,7 @@ "private": true, "sideEffects": false, "koot": { - "version": "0.15.10" + "version": "0.15.12" }, "devDependencies": { "@types/inquirer": "^8.2.1", diff --git a/test/projects/standard/package.json b/test/projects/standard/package.json index f8fb479da..1f29311c8 100644 --- a/test/projects/standard/package.json +++ b/test/projects/standard/package.json @@ -85,7 +85,7 @@ "private": true, "sideEffects": false, "koot": { - "version": "0.15.10" + "version": "0.15.12" }, "devDependencies": { "@types/inquirer": "^8.2.1",