Skip to content

Commit

Permalink
Require Node.js 18 and Electron 30 and move to ESM
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed May 1, 2024
1 parent a302ae5 commit 37d7c9b
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 154 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ jobs:
fail-fast: false
matrix:
node-version:
- 16
- 14
- 12
- 20
- 18
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand Down
2 changes: 1 addition & 1 deletion example-preload.js → example-preload.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const {logError} = require('.');
import {logError} from './index.js';

window.logError = logError;

Expand Down
23 changes: 13 additions & 10 deletions example.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
'use strict';
const {app, BrowserWindow} = require('electron');
const path = require('path');
const {openNewGitHubIssue, debugInfo} = require('electron-util');
const unhandled = require('.');
import {app, BrowserWindow} from 'electron';
import path from 'node:path';
import {openNewGitHubIssue} from 'electron-util';
import {debugInfo} from 'electron-util/main';
import unhandled from './index.js';

unhandled({
showDialog: true,
reportButton: error => {
reportButton(error) {
openNewGitHubIssue({
user: 'sindresorhus',
repo: 'electron-unhandled',
body: `\`\`\`\n${error.stack}\n\`\`\`\n\n---\n\n${debugInfo()}`
body: `\`\`\`\n${error.stack}\n\`\`\`\n\n---\n\n${debugInfo()}`,
});
}
},
});

let mainWindow;

// eslint-disable-next-line unicorn/prefer-top-level-await
(async () => {
await app.whenReady();

mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'example-preload.js')
}
preload: path.join(import.meta.dirname, 'example-preload.mjs'),
},
});

mainWindow.openDevTools();

await mainWindow.loadURL('https://google.com');
})();
13 changes: 6 additions & 7 deletions fixture-error.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
'use strict';
const assert = require('assert');
const electron = require('electron');
const unhandled = require('.');
import assert from 'node:assert';
import {app} from 'electron';
import unhandled from './index.js';

const fixture = new Error('foo');

unhandled({
showDialog: false,
logger: error => {
logger(error) {
assert.strictEqual(error.message, fixture.message);
electron.app.quit();
}
app.quit();
},
});

setTimeout(() => {
Expand Down
13 changes: 6 additions & 7 deletions fixture-rejection.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
'use strict';
const assert = require('assert');
const electron = require('electron');
const unhandled = require('.');
import assert from 'node:assert';
import {app} from 'electron';
import unhandled from './index.js';

const fixture = new Error('foo');

unhandled({
showDialog: false,
logger: error => {
logger(error) {
assert.strictEqual(error.message, fixture.message);
electron.app.quit();
}
app.quit();
},
});

Promise.reject(fixture);
133 changes: 67 additions & 66 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,79 @@
declare namespace unhandled {
/**
__Note:__ Options can only be specified in the `main` process.
*/
export type UnhandledOptions = {
/**
__Note:__ Options can only be specified in the `main` process.
Custom logger that receives the error.
Can be useful if you for example integrate with Sentry.
@default console.error
*/
interface UnhandledOptions {
/**
Custom logger that receives the error.
Can be useful if you for example integrate with Sentry.
@default console.error
*/
readonly logger?: (error: Error) => void;

/**
Present an error dialog to the user.
Default: [Only in production](https://github.com/sindresorhus/electron-is-dev).
*/
readonly showDialog?: boolean;

/**
When specified, the error dialog will include a `Report…` button, which when clicked, executes the given function with the error as the first argument.
@default undefined
@example
```
import unhandled = require('electron-unhandled');
import {openNewGitHubIssue, debugInfo} = require('electron-util');
unhandled({
reportButton: error => {
openNewGitHubIssue({
user: 'sindresorhus',
repo: 'electron-unhandled',
body: `\`\`\`\n${error.stack}\n\`\`\`\n\n---\n\n${debugInfo()}`
});
}
});
// Example of how the GitHub issue will look like: https://github.com/sindresorhus/electron-unhandled/issues/new?body=%60%60%60%0AError%3A+Test%0A++++at+%2FUsers%2Fsindresorhus%2Fdev%2Foss%2Felectron-unhandled%2Fexample.js%3A27%3A21%0A%60%60%60%0A%0A---%0A%0AExample+1.1.0%0AElectron+3.0.8%0Adarwin+18.2.0%0ALocale%3A+en-US
```
*/
readonly reportButton?: (error: Error) => void;
}

interface LogErrorOptions {
/**
The title of the error dialog.
@default `${appName} encountered an error`
*/
readonly title?: string;
}
}

declare const unhandled: {
/**
Catch unhandled errors and promise rejections in your [Electron](https://electronjs.org) app.
readonly logger?: (error: Error) => void;

You probably want to call this both in the `main` process and any `renderer` processes to catch all possible errors.
/**
Present an error dialog to the user.
__Note:__ At minimum, this function must be called in the `main` process.
Default: [Only in production](https://github.com/sindresorhus/electron-is-dev).
*/
(options?: unhandled.UnhandledOptions): void;
readonly showDialog?: boolean;

/**
Log an error. This does the same as with caught unhandled errors.
When specified, the error dialog will include a `Report…` button, which when clicked, executes the given function with the error as the first argument.
@default undefined
@example
```
import unhandled from 'electron-unhandled';
import {openNewGitHubIssue, debugInfo} from 'electron-util';
unhandled({
reportButton: error => {
openNewGitHubIssue({
user: 'sindresorhus',
repo: 'electron-unhandled',
body: `\`\`\`\n${error.stack}\n\`\`\`\n\n---\n\n${debugInfo()}`
});
}
});
// Example of how the GitHub issue will look like: https://github.com/sindresorhus/electron-unhandled/issues/new?body=%60%60%60%0AError%3A+Test%0A++++at+%2FUsers%2Fsindresorhus%2Fdev%2Foss%2Felectron-unhandled%2Fexample.js%3A27%3A21%0A%60%60%60%0A%0A---%0A%0AExample+1.1.0%0AElectron+3.0.8%0Adarwin+18.2.0%0ALocale%3A+en-US
```
*/
readonly reportButton?: (error: Error) => void;
};

It will use the same options specified in the `unhandled()` call or the defaults.
export type LogErrorOptions = {
/**
The title of the error dialog.
@param error - Error to log.
@default `${appName} encountered an error`
*/
logError(error: Error, options?: unhandled.LogErrorOptions): void;
readonly title?: string;
};

export = unhandled;
/**
Catch unhandled errors and promise rejections in your [Electron](https://electronjs.org) app.
You probably want to call this both in the `main` process and any `renderer` processes to catch all possible errors.
__Note:__ At minimum, this function must be called in the `main` process.
*/
export default function unhandled(options?: UnhandledOptions): void;

/**
Log an error. This does the same as with caught unhandled errors.
It will use the same options specified in the `unhandled()` call or the defaults.
@param error - The error to log.
@example
```
import {logError} from 'electron-unhandled';
logError(new Error('🦄'));
```
*/
export function logError(error: Error, options?: LogErrorOptions): void;
52 changes: 27 additions & 25 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
'use strict';
const {app, dialog, clipboard} = require('electron');
const cleanStack = require('clean-stack');
const ensureError = require('ensure-error');
const debounce = require('lodash.debounce');
const {serializeError} = require('serialize-error');
import process from 'node:process';
import {app, dialog, clipboard} from 'electron';
import cleanStack from 'clean-stack';
import ensureError from 'ensure-error';
import debounce from 'lodash.debounce';
import {serializeError} from 'serialize-error';

let appName;

let invokeErrorHandler;

const ERROR_HANDLER_CHANNEL = 'electron-unhandled.ERROR';

if (process.type === 'renderer') {
const {ipcRenderer} = require('electron');
// Default to 'App' because I don't think we can populate `appName` reliably here without remote or adding more IPC logic
invokeErrorHandler = async (title = 'App encountered an error', error) => {
const {ipcRenderer} = await import('electron');

try {
await ipcRenderer.invoke(ERROR_HANDLER_CHANNEL, title, error);
return;
} catch (invokeError) { // eslint-disable-line unicorn/catch-error-name
} catch (invokeError) {
if (invokeError.message === 'An object could not be cloned.') {
// 1. If serialization failed, force the passed arg to an error format
error = ensureError(error);
Expand All @@ -32,17 +31,20 @@ if (process.type === 'renderer') {
};
} else {
appName = 'name' in app ? app.name : app.getName();
const {ipcMain} = require('electron');
ipcMain.handle(ERROR_HANDLER_CHANNEL, async (evt, title, error) => {
const {ipcMain} = await import('electron');
ipcMain.handle(ERROR_HANDLER_CHANNEL, async (event_, title, error) => {
handleError(title, error);
});
}

let installed = false;
let isInstalled = false;

let options = {
logger: console.error,
showDialog: process.type !== 'renderer' && !require('electron-is-dev')
showDialog: process.type !== 'renderer' && (async () => { // eslint-disable-line unicorn/prefer-top-level-await
const {default: isDevelopment} = await import('electron-is-dev');
return isDevelopment;
})(),
};

// NOTE: The ES6 default for title will only be used if the error is invoked from the main process directly. When invoked via the renderer, it will use the ES6 default from invokeErrorHandler
Expand All @@ -51,7 +53,7 @@ const handleError = (title = `${appName} encountered an error`, error) => {

try {
options.logger(error);
} catch (loggerError) { // eslint-disable-line unicorn/catch-error-name
} catch (loggerError) {
dialog.showErrorBox('The `logger` option function in electron-unhandled threw an error', ensureError(loggerError).stack);
return;
}
Expand All @@ -62,7 +64,7 @@ const handleError = (title = `${appName} encountered an error`, error) => {
if (app.isReady()) {
const buttons = [
'OK',
process.platform === 'darwin' ? 'Copy Error' : 'Copy error'
process.platform === 'darwin' ? 'Copy Error' : 'Copy error',
];

if (options.reportButton) {
Expand All @@ -76,7 +78,7 @@ const handleError = (title = `${appName} encountered an error`, error) => {
defaultId: 0,
noLink: true,
message: title,
detail: cleanStack(error.stack, {pretty: true})
detail: cleanStack(error.stack, {pretty: true}),
});

if (buttonIndex === 1) {
Expand All @@ -92,16 +94,16 @@ const handleError = (title = `${appName} encountered an error`, error) => {
}
};

module.exports = inputOptions => {
if (installed) {
export default function unhandled(inputOptions) {
if (isInstalled) {
return;
}

installed = true;
isInstalled = true;

options = {
...options,
...inputOptions
...inputOptions,
};

if (process.type === 'renderer') {
Expand Down Expand Up @@ -130,16 +132,16 @@ module.exports = inputOptions => {
handleError('Unhandled Promise Rejection', error);
});
}
};
}

module.exports.logError = (error, options) => {
export function logError(error, options) {
options = {
...options
...options,
};

if (typeof invokeErrorHandler === 'function') {
invokeErrorHandler(options.title, error);
} else {
handleError(options.title, error);
}
};
}
10 changes: 0 additions & 10 deletions index.test-d.ts

This file was deleted.

Loading

0 comments on commit 37d7c9b

Please sign in to comment.