From 480af26b57b259968d7c86b8f2b3183b63430826 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 21 Mar 2026 13:05:24 +0700 Subject: [PATCH] feat(desktop): add React DevTools extension for Electron dev mode Downloads and loads React DevTools Chrome extension using Electron 41's session.extensions API directly, avoiding the deprecated session.loadExtension used by electron-devtools-installer. Extension is cached in userData after first download. Only loaded when !app.isPackaged. --- package-lock.json | 134 +++++++++++++++++- packages/desktop/package.json | 1 + .../desktop/src/features/react-devtools.ts | 43 ++++++ packages/desktop/src/main.ts | 2 + packages/desktop/src/vendor.d.ts | 4 + 5 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 packages/desktop/src/features/react-devtools.ts create mode 100644 packages/desktop/src/vendor.d.ts diff --git a/package-lock.json b/package-lock.json index 60ced9e20..ee419b0ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14904,8 +14904,7 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/cors": { "version": "2.8.6", @@ -22129,6 +22128,13 @@ "node": ">=16.x" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -24002,6 +24008,19 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/jwa": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", @@ -24162,6 +24181,16 @@ "@lezer/lr": "^1.3.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -27304,6 +27333,13 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -28021,6 +28057,13 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -29291,6 +29334,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -31071,6 +31144,23 @@ "node": ">=4" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/string-length": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", @@ -32811,6 +32901,31 @@ "node": ">=8" } }, + "node_modules/unzip-crx-3": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/unzip-crx-3/-/unzip-crx-3-0.2.0.tgz", + "integrity": "sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jszip": "^3.1.0", + "mkdirp": "^0.5.1", + "yaku": "^0.16.6" + } + }, + "node_modules/unzip-crx-3/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -32942,6 +33057,13 @@ "which-typed-array": "^1.1.2" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -33926,6 +34048,13 @@ "node": ">=10" } }, + "node_modules/yaku": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/yaku/-/yaku-0.16.7.tgz", + "integrity": "sha512-Syu3IB3rZvKvYk7yTiyl1bo/jiEFaaStrgv1V2TIJTqYPStSMQVO8EQjg/z+DRzLq/4LIIharNT3iH1hylEIRw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -34324,6 +34453,7 @@ "electron": "41.0.3", "electron-builder": "26.8.1", "typescript": "5.9.3", + "unzip-crx-3": "^0.2.0", "wait-on": "8.0.5" } }, diff --git a/packages/desktop/package.json b/packages/desktop/package.json index b000a066c..63e7ca0fd 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -22,6 +22,7 @@ "electron": "41.0.3", "electron-builder": "26.8.1", "typescript": "5.9.3", + "unzip-crx-3": "^0.2.0", "wait-on": "8.0.5" } } diff --git a/packages/desktop/src/features/react-devtools.ts b/packages/desktop/src/features/react-devtools.ts new file mode 100644 index 000000000..7c16e7a46 --- /dev/null +++ b/packages/desktop/src/features/react-devtools.ts @@ -0,0 +1,43 @@ +import path from "node:path"; +import { existsSync } from "node:fs"; +import { mkdir, writeFile, unlink } from "node:fs/promises"; +import { app, session, net } from "electron"; + +const REACT_DEVTOOLS_EXTENSION_ID = "fmkadmapgofadopljbjfkapdkoienihi"; + +export async function loadReactDevTools(): Promise { + const extensionsDir = path.join(app.getPath("userData"), "extensions"); + const extensionPath = path.join(extensionsDir, REACT_DEVTOOLS_EXTENSION_ID); + + if (!existsSync(extensionPath)) { + await mkdir(extensionsDir, { recursive: true }); + const crxUrl = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${REACT_DEVTOOLS_EXTENSION_ID}%26uc&prodversion=${process.versions.chrome}`; + const crxPath = `${extensionPath}.crx`; + + const buffer = await new Promise((resolve, reject) => { + const request = net.request(crxUrl); + request.on("response", (response) => { + const chunks: Buffer[] = []; + response.on("data", (chunk) => chunks.push(chunk)); + response.on("end", () => resolve(Buffer.concat(chunks))); + response.on("error", reject); + }); + request.on("error", reject); + request.end(); + }); + + await writeFile(crxPath, buffer); + const unzipCrx = (await import("unzip-crx-3")).default; + await unzipCrx(crxPath, extensionPath); + await unlink(crxPath); + } + + try { + const ext = await session.defaultSession.extensions.loadExtension(extensionPath, { + allowFileAccess: true, + }); + console.log(`[DevTools] Loaded: ${ext.name}`); + } catch (err) { + console.warn("[DevTools] Failed to load React DevTools:", err); + } +} diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index c8b515640..80d57a545 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -87,6 +87,8 @@ async function createMainWindow(): Promise { }); if (!app.isPackaged) { + const { loadReactDevTools } = await import("./features/react-devtools.js"); + await loadReactDevTools(); await mainWindow.loadURL(DEV_SERVER_URL); mainWindow.webContents.openDevTools({ mode: "detach" }); return; diff --git a/packages/desktop/src/vendor.d.ts b/packages/desktop/src/vendor.d.ts new file mode 100644 index 000000000..cc85c5a37 --- /dev/null +++ b/packages/desktop/src/vendor.d.ts @@ -0,0 +1,4 @@ +declare module "unzip-crx-3" { + function unzipCrx(crxPath: string, outputDir: string): Promise; + export default unzipCrx; +}