Skip to content

Commit

Permalink
feat: modernize (#254)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Removed some Chrome extensions that weren't supported, now require Electron >=18, `installExtension` now resolves with a full extension object instead of just the name

* Expanding API where it makes sense
  * Allow providing `session`
* Cleaning up old code style, moving to async/await etc.
* Cleaning up left over IDMap
* Removing dead chrome extensions

Closes #180
Closes #134
  • Loading branch information
MarshallOfSound authored Dec 18, 2024
1 parent 7422d39 commit 8e96039
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 3,339 deletions.
6 changes: 5 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,18 @@ workflows:
test_and_release:
# Run the test jobs first, then the release only when all the test jobs are successful
jobs:
- test-electron:
name: test-electron-30
electron_version: ^30.0.0
- test-electron:
name: test-electron-33
electron_version: ^33.0.0
- cfa/release:
requires:
- test-electron-30
- test-electron-33
filters:
branches:
only:
- master
- main
context: cfa-release
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ yarn add electron-devtools-installer -D
All you have to do now is this in the **main** process of your application.

```js
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer';
import { installExtension, REDUX_DEVTOOLS } from 'electron-devtools-installer';
// Or if you can not use ES6 imports
/**
const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer');
Expand All @@ -34,12 +34,31 @@ const { app } = require('electron');

app.whenReady().then(() => {
installExtension(REDUX_DEVTOOLS)
.then((name) => console.log(`Added Extension: ${name}`))
.then((ext) => console.log(`Added Extension: ${ext.name}`))
.catch((err) => console.log('An error occurred: ', err));
});
```

To install multiple extensions, `installExtension` takes an array.

```typescript
app.whenReady().then(() => {
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
.then(([redux, react]) => console.log(`Added Extensions: ${redux.name}, ${react.name}`))
.catch((err) => console.log('An error occurred: ', err));
});
```

### Local Files

If you want your DevTools extensions to work on local `file://` URLs (e.g. loaded via `browserWindow.loadFile()`), don't forget to set `allowFileAccess` in the options passed to `installExtension`.

```typescript
installExtension(REDUX_DEVTOOLS, { loadExtensionOptions: { allowFileAccess: true } })
```

For more information see the [Electron documentation](https://www.electronjs.org/docs/latest/api/session#sesloadextensionpath-options).

## What extensions can I use?

Technically you can use whatever extension you want. Simply find the ChromeStore ID
Expand All @@ -48,12 +67,12 @@ offer a few extension ID's inside the package so you can easily import them to i
having to find them yourselves.

```js
import installExtension, {
import {
installExtension,
EMBER_INSPECTOR, REACT_DEVELOPER_TOOLS,
BACKBONE_DEBUGGER, JQUERY_DEBUGGER,
ANGULARJS_BATARANG, VUEJS_DEVTOOLS,
VUEJS3_DEVTOOLS, REDUX_DEVTOOLS,
CYCLEJS_DEVTOOL, MOBX_DEVTOOLS,
VUEJS_DEVTOOLS, VUEJS_DEVTOOLS_BETA,
REDUX_DEVTOOLS, MOBX_DEVTOOLS,
APOLLO_DEVELOPER_TOOLS,
} from 'electron-devtools-installer';
```
Expand Down
11 changes: 4 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.0.0-development",
"description": "An easy way to install Dev Tools extensions into Electron applications",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prettier:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
"prettier:write": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
Expand Down Expand Up @@ -31,28 +32,24 @@
"url": "https://github.com/MarshallOfSound/electron-devtools-installer.git"
},
"devDependencies": {
"@continuous-auth/semantic-release-npm": "^2.0.0",
"@types/chai": "^4.2.14",
"@types/chai-as-promised": "^7.1.3",
"@types/chai-fs": "^2.0.2",
"@types/mocha": "^8.2.0",
"@types/node": "^12.0.0",
"@types/rimraf": "^3.0.0",
"@types/node": "^18.0.0",
"@types/semver": "^7.3.4",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"chai-fs": "chaijs/chai-fs",
"electron": "33.3.0",
"electron-mocha": "^13.0.1",
"mocha-testdata": "^1.2.0",
"prettier": "^2.0.4",
"prettier": "^3.4.2",
"spec-xunit-file": "0.0.1-3",
"ts-node": "^9.1.1",
"typescript": "^4.1.4"
"typescript": "^5.7.2"
},
"dependencies": {
"rimraf": "^3.0.2",
"tslib": "^2.1.0",
"unzip-crx-3": "^0.2.0"
},
"files": [
Expand Down
83 changes: 43 additions & 40 deletions src/downloadChromeExtension.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,59 @@
import * as fs from 'fs';
import * as path from 'path';
import * as rimraf from 'rimraf';

import { getPath, downloadFile, changePermissions } from './utils';

const unzip: any = require('unzip-crx-3');

const downloadChromeExtension = (
export const downloadChromeExtension = async (
chromeStoreID: string,
forceDownload?: boolean,
attempts = 5,
{
forceDownload = false,
attempts = 5,
}: {
forceDownload?: boolean;
attempts?: number;
} = {},
): Promise<string> => {
const extensionsStore = getPath();
if (!fs.existsSync(extensionsStore)) {
fs.mkdirSync(extensionsStore, { recursive: true });
await fs.promises.mkdir(extensionsStore, { recursive: true });
}
const extensionFolder = path.resolve(`${extensionsStore}/${chromeStoreID}`);
return new Promise((resolve, reject) => {
if (!fs.existsSync(extensionFolder) || forceDownload) {
if (fs.existsSync(extensionFolder)) {
rimraf.sync(extensionFolder);

if (!fs.existsSync(extensionFolder) || forceDownload) {
if (fs.existsSync(extensionFolder)) {
await fs.promises.rmdir(extensionFolder, {
recursive: true,
});
}
const fileURL = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${chromeStoreID}%26uc&prodversion=${process.versions.chrome}`; // eslint-disable-line
const filePath = path.resolve(`${extensionFolder}.crx`);
try {
await downloadFile(fileURL, filePath);

try {
await unzip(filePath, extensionFolder);
changePermissions(extensionFolder, 755);
return extensionFolder;
} catch (err) {
if (!fs.existsSync(path.resolve(extensionFolder, 'manifest.json'))) {
throw err;
}
}
} catch (err) {
console.error(`Failed to fetch extension, trying ${attempts - 1} more times`); // eslint-disable-line
if (attempts <= 1) {
throw err;
}
const fileURL = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${chromeStoreID}%26uc&prodversion=${process.versions.chrome}`; // eslint-disable-line
const filePath = path.resolve(`${extensionFolder}.crx`);
downloadFile(fileURL, filePath)
.then(() => {
unzip(filePath, extensionFolder)
.then(() => {
changePermissions(extensionFolder, 755);
resolve(extensionFolder);
})
.catch((err: Error) => {
if (!fs.existsSync(path.resolve(extensionFolder, 'manifest.json'))) {
return reject(err);
}
});
})
.catch((err) => {
console.log(`Failed to fetch extension, trying ${attempts - 1} more times`); // eslint-disable-line
if (attempts <= 1) {
return reject(err);
}
setTimeout(() => {
downloadChromeExtension(chromeStoreID, forceDownload, attempts - 1)
.then(resolve)
.catch(reject);
}, 200);
});
} else {
resolve(extensionFolder);
await new Promise<void>((resolve) => setTimeout(resolve, 200));

return await downloadChromeExtension(chromeStoreID, {
forceDownload,
attempts: attempts - 1,
});
}
});
};
}

export default downloadChromeExtension;
return extensionFolder;
};
105 changes: 56 additions & 49 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
import { BrowserWindow, LoadExtensionOptions, session } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { Extension, LoadExtensionOptions, Session, session } from 'electron';

import downloadChromeExtension from './downloadChromeExtension';
import { getPath } from './utils';
import { downloadChromeExtension } from './downloadChromeExtension';

let IDMap: Record<string, string> = {};
const getIDMapPath = () => path.resolve(getPath(), 'IDMap.json');
if (fs.existsSync(getIDMapPath())) {
try {
IDMap = JSON.parse(fs.readFileSync(getIDMapPath(), 'utf8'));
} catch (err) {
console.error('electron-devtools-installer: Invalid JSON present in the IDMap file');
}
}

interface ExtensionReference {
export interface ExtensionReference {
/**
* Extension ID
*/
id: string;
}

interface ExtensionOptions {
export interface InstallExtensionOptions {
/**
* Ignore whether the extension is already downloaded and redownload every time
*/
Expand All @@ -31,18 +18,35 @@ interface ExtensionOptions {
* Options passed to session.loadExtension
*/
loadExtensionOptions?: LoadExtensionOptions;
/**
* Optionally specify the session to install devtools into, by default devtools
* will be installed into the "defaultSession". See the Electron Session docs
* for more info.
*
* https://electronjs.org/docs/api/session
*/
session?: Session;
}

/**
* @param extensionReference Extension or extensions to install
* @param options Installation options
* @returns A promise resolving with the name or names of the extensions installed
*/
const install = (
export async function installExtension(
extensionReference: Array<ExtensionReference | string>,
options?: InstallExtensionOptions,
): Promise<Extension[]>;
export async function installExtension(
extensionReference: ExtensionReference | string,
options?: InstallExtensionOptions,
): Promise<Extension>;
export async function installExtension(
extensionReference: ExtensionReference | string | Array<ExtensionReference | string>,
options: ExtensionOptions = {},
): Promise<string> => {
const { forceDownload, loadExtensionOptions } = options;
options: InstallExtensionOptions = {},
): Promise<Extension | Extension[]> {
const { forceDownload, loadExtensionOptions, session: _session } = options;
const targetSession = _session || session.defaultSession;

if (process.type !== 'browser') {
return Promise.reject(
Expand All @@ -52,8 +56,12 @@ const install = (

if (Array.isArray(extensionReference)) {
return extensionReference.reduce(
(accum, extension) => accum.then(() => install(extension, options)),
Promise.resolve(''),
(accum, extension) =>
accum.then(async (result) => {
const inner = await installExtension(extension, options);
return [...result, inner];
}),
Promise.resolve([] as Extension[]),
);
}
let chromeStoreID: string;
Expand All @@ -62,37 +70,36 @@ const install = (
} else if (typeof extensionReference === 'string') {
chromeStoreID = extensionReference;
} else {
return Promise.reject(
new Error(`Invalid extensionReference passed in: "${extensionReference}"`),
);
throw new Error(`Invalid extensionReference passed in: "${extensionReference}"`);
}
const extensionName = IDMap[chromeStoreID];

const extensionInstalled =
!!extensionName &&
session.defaultSession.getAllExtensions().find((e) => e.name === extensionName);
const installedExtension = targetSession.getAllExtensions().find((e) => e.id === chromeStoreID);

if (!forceDownload && extensionInstalled) {
return Promise.resolve(IDMap[chromeStoreID]);
if (!forceDownload && installedExtension) {
return installedExtension;
}
return downloadChromeExtension(chromeStoreID, forceDownload || false).then((extensionFolder) => {
// Use forceDownload, but already installed
if (extensionInstalled) {
const extensionId = session.defaultSession.getAllExtensions().find((e) => e.name)?.id;
if (extensionId) {
session.defaultSession.removeExtension(extensionId);
}
}

return session.defaultSession
.loadExtension(extensionFolder, loadExtensionOptions)
.then((ext: { name: string }) => {
return Promise.resolve(ext.name);
});
const extensionFolder = await downloadChromeExtension(chromeStoreID, {
forceDownload: forceDownload || false,
});
};
// Use forceDownload, but already installed
if (installedExtension?.id) {
const unloadPromise = new Promise<void>((resolve) => {
const handler = (_: unknown, ext: Extension) => {
if (ext.id === installedExtension.id) {
targetSession.removeListener('extension-unloaded', handler);
resolve();
}
};
targetSession.on('extension-unloaded', handler);
});
targetSession.removeExtension(installedExtension.id);
await unloadPromise;
}

return targetSession.loadExtension(extensionFolder, loadExtensionOptions);
}

export default install;
export default installExtension;
export const EMBER_INSPECTOR: ExtensionReference = {
id: 'bmdblncegkenkacieihfhpjfppoconhi',
};
Expand All @@ -108,7 +115,7 @@ export const JQUERY_DEBUGGER: ExtensionReference = {
export const VUEJS_DEVTOOLS: ExtensionReference = {
id: 'nhdogjmejiglipccpnnnanhbledajbpd',
};
export const VUEJS3_DEVTOOLS: ExtensionReference = {
export const VUEJS_DEVTOOLS_BETA: ExtensionReference = {
id: 'ljjemllljcmogpfapbkkighbhhppjdbg',
};
export const REDUX_DEVTOOLS: ExtensionReference = {
Expand Down
4 changes: 2 additions & 2 deletions test/download_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as fs from 'fs';
import * as path from 'path';

// Actual Test Imports
import downloadChromeExtension from '../src/downloadChromeExtension';
import { downloadChromeExtension } from '../src/downloadChromeExtension';
import { REACT_DEVELOPER_TOOLS } from '../src/';

chai.use(chaiAsPromised);
Expand Down Expand Up @@ -43,7 +43,7 @@ describe('Extension Downloader', () => {
path.resolve(dir, 'manifest.json').should.be.a.file();
path.resolve(dir, 'old_ext.file').should.be.a.file();

downloadChromeExtension(REACT_DEVELOPER_TOOLS.id, true)
downloadChromeExtension(REACT_DEVELOPER_TOOLS.id, { forceDownload: true })
.then((newDir) => {
newDir.should.be.equal(dir);
newDir.should.be.a.directory();
Expand Down
Loading

0 comments on commit 8e96039

Please sign in to comment.