Don't use lodash in child-writer.js

Changelog-entry: Don't use lodash in child-writer.js
Change-type: patch
This commit is contained in:
Alexis Svinartchouk 2020-08-03 15:46:22 +02:00
parent 76fa698995
commit f2a37079eb
6 changed files with 62 additions and 309 deletions

View File

@ -244,7 +244,6 @@ export async function performWrite(
title: 'The writer process ended unexpectedly',
description:
'Please try again, and contact the Etcher team if the problem persists',
code: 'ECHILDDIED',
}),
);
return;

View File

@ -17,7 +17,6 @@
import { Drive as DrivelistDrive } from 'drivelist';
import * as sdk from 'etcher-sdk';
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import * as _ from 'lodash';
import * as ipc from 'node-ipc';
import { toJSON } from '../../shared/errors';
@ -223,14 +222,14 @@ ipc.connectTo(IPC_SERVER_ID, () => {
});
};
const destinations = _.map(options.destinations, 'device');
const destinations = options.destinations.map((d) => d.device);
log(`Image: ${options.imagePath}`);
log(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Validate on success: ${options.validateWriteOnSuccess}`);
log(`Auto blockmapping: ${options.autoBlockmapping}`);
log(`Decompress first: ${options.decompressFirst}`);
const dests = _.map(options.destinations, (destination) => {
const dests = options.destinations.map((destination) => {
return new sdk.sourceDestination.BlockDevice({
drive: destination,
unmountOnSuccess: options.unmountOnSuccess,
@ -261,7 +260,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
onFail,
});
log(`Finish: ${results.bytesWritten}`);
results.errors = _.map(results.errors, (error) => {
results.errors = results.errors.map((error) => {
return toJSON(error);
});
ipc.of[IPC_SERVER_ID].emit('done', { results });

View File

@ -14,48 +14,37 @@
* limitations under the License.
*/
import * as _ from 'lodash';
function createErrorDetails(options: {
title: string | ((error: Error & { path: string }) => string);
description: string;
}): {
title: (error: Error & { path: string }) => string;
description: (error: Error & { path: string }) => string;
} {
return _.pick(
_.mapValues(options, (value) => {
return _.isFunction(value) ? value : _.constant(value);
}),
['title', 'description'],
);
}
export type ErrorWithPath = Error & {
path?: string;
code?: keyof typeof HUMAN_FRIENDLY;
};
/**
* @summary Human-friendly error messages
*/
export const HUMAN_FRIENDLY = {
ENOENT: createErrorDetails({
title: (error: Error & { path: string }) => {
ENOENT: {
title: (error: ErrorWithPath) => {
return `No such file or directory: ${error.path}`;
},
description: "The file you're trying to access doesn't exist",
}),
EPERM: createErrorDetails({
title: "You're not authorized to perform this operation",
description: 'Please ensure you have necessary permissions for this task',
}),
EACCES: createErrorDetails({
title: "You don't have access to this resource",
description:
description: () => "The file you're trying to access doesn't exist",
},
EPERM: {
title: () => "You're not authorized to perform this operation",
description: () =>
'Please ensure you have necessary permissions for this task',
},
EACCES: {
title: () => "You don't have access to this resource",
description: () =>
'Please ensure you have necessary permissions to access this resource',
}),
ENOMEM: createErrorDetails({
title: 'Your system ran out of memory',
description:
},
ENOMEM: {
title: () => 'Your system ran out of memory',
description: () =>
'Please make sure your system has enough available memory for this task',
}),
};
},
} as const;
/**
* @summary Get user friendly property from an error
@ -71,19 +60,21 @@ export const HUMAN_FRIENDLY = {
* }
*/
function getUserFriendlyMessageProperty(
error: Error,
error: ErrorWithPath,
property: 'title' | 'description',
): string | null {
const code = _.get(error, ['code']);
if (_.isNil(code) || !_.isString(code)) {
return null;
): string | undefined {
if (typeof error.code !== 'string') {
return undefined;
}
return _.invoke(HUMAN_FRIENDLY, [code, property], error);
return HUMAN_FRIENDLY[error.code]?.[property]?.(error);
}
const isBlank = _.flow([_.trim, _.isEmpty]);
function isBlank(s: string | number | null | undefined) {
if (typeof s === 'number') {
s = s.toString();
}
return (s ?? '').trim() === '';
}
/**
* @summary Get the title of an error
@ -92,23 +83,19 @@ const isBlank = _.flow([_.trim, _.isEmpty]);
* Try to get as much information as possible about the error
* rather than falling back to generic messages right away.
*/
export function getTitle(error: Error): string {
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
return _.toString(error);
}
export function getTitle(error: ErrorWithPath): string {
const codeTitle = getUserFriendlyMessageProperty(error, 'title');
if (!_.isNil(codeTitle)) {
if (codeTitle !== undefined) {
return codeTitle;
}
const message = _.get(error, ['message']);
const message = error.message;
if (!isBlank(message)) {
return message;
}
const code = _.get(error, ['code']);
if (!_.isNil(code) && !isBlank(code)) {
const code = error.code;
if (!isBlank(code)) {
return `Error code: ${code}`;
}
@ -119,40 +106,19 @@ export function getTitle(error: Error): string {
* @summary Get the description of an error
*/
export function getDescription(
error: Error & { description?: string },
options: { userFriendlyDescriptionsOnly?: boolean } = {},
error: ErrorWithPath & { description?: string },
): string {
_.defaults(options, {
userFriendlyDescriptionsOnly: false,
});
if (!_.isError(error) && !_.isPlainObject(error)) {
return '';
}
if (!isBlank(error.description)) {
return error.description as string;
}
const codeDescription = getUserFriendlyMessageProperty(error, 'description');
if (!_.isNil(codeDescription)) {
if (codeDescription !== undefined) {
return codeDescription;
}
if (options.userFriendlyDescriptionsOnly) {
return '';
}
if (error.stack) {
return error.stack;
}
if (_.isEmpty(error)) {
return '';
}
const INDENTATION_SPACES = 2;
return JSON.stringify(error, null, INDENTATION_SPACES);
return JSON.stringify(error, null, 2);
}
/**
@ -162,24 +128,24 @@ export function createError(options: {
title: string;
description?: string;
report?: boolean;
code?: string;
}): Error & { description?: string; report?: boolean; code?: string } {
code?: keyof typeof HUMAN_FRIENDLY;
}): ErrorWithPath & { description?: string; report?: boolean } {
if (isBlank(options.title)) {
throw new Error(`Invalid error title: ${options.title}`);
}
const error: Error & {
const error: ErrorWithPath & {
description?: string;
report?: boolean;
code?: string;
} = new Error(options.title);
error.description = options.description;
if (!_.isNil(options.report) && !options.report) {
if (options.report === false) {
error.report = false;
}
if (!_.isNil(options.code)) {
if (options.code !== undefined) {
error.code = options.code;
}
@ -198,7 +164,7 @@ export function createError(options: {
export function createUserError(options: {
title: string;
description: string;
code?: string;
code?: keyof typeof HUMAN_FRIENDLY;
}): Error {
return createError({
title: options.title,
@ -208,13 +174,6 @@ export function createUserError(options: {
});
}
/**
* @summary Check if an error is an user error
*/
export function isUserError(error: Error & { report?: boolean }): boolean {
return _.isNil(error.report) ? false : !error.report;
}
/**
* @summary Convert an Error object to a JSON object
* @function
@ -260,5 +219,5 @@ export function toJSON(
* @summary Convert a JSON object to an Error object
*/
export function fromJSON(json: any): Error {
return _.assign(new Error(json.message), json);
return Object.assign(new Error(json.message), json);
}

7
npm-shrinkwrap.json generated
View File

@ -7019,9 +7019,9 @@
"dev": true
},
"etcher-sdk": {
"version": "4.1.20",
"resolved": "https://registry.npmjs.org/etcher-sdk/-/etcher-sdk-4.1.20.tgz",
"integrity": "sha512-cVjAifAKKYDc9SNMICnK4YWXHz3uGlwKgYOkRykPATxkwhVYfaW6wlhV2DFqbfkps+l4si7iC82Tz1RaV0VjHA==",
"version": "4.1.21",
"resolved": "https://registry.npmjs.org/etcher-sdk/-/etcher-sdk-4.1.21.tgz",
"integrity": "sha512-jQzKZxYkUA9+rsmelr6U1d2pBwiRw6AGM//a8EnqL1Aorq44xq2BZ59BomDwofzMJ5RCGEJjgOmi7/w+eUVVvg==",
"dev": true,
"requires": {
"@balena/udif": "^1.0.3",
@ -7035,7 +7035,6 @@
"drivelist": "^9.0.0",
"file-disk": "^8.0.0",
"file-type": "^8.0.0",
"lodash": "^4.17.19",
"lzma-native": "^6.0.0",
"mountutils": "^1.3.18",
"node-raspberrypi-usbboot": "^0.2.9",

View File

@ -75,7 +75,7 @@
"electron-notarize": "^1.0.0",
"electron-rebuild": "^1.11.0",
"electron-updater": "^4.3.2",
"etcher-sdk": "^4.1.20",
"etcher-sdk": "^4.1.21",
"file-loader": "^6.0.0",
"husky": "^4.2.5",
"immutable": "^3.8.1",

View File

@ -37,54 +37,12 @@ describe('Shared: Errors', function () {
});
describe('.getTitle()', function () {
it('should accept a string', function () {
const error = 'This is an error';
// @ts-ignore
expect(errors.getTitle(error)).to.equal('This is an error');
});
it('should accept a number 0', function () {
const error = 0;
// @ts-ignore
expect(errors.getTitle(error)).to.equal('0');
});
it('should accept a number 1', function () {
const error = 1;
// @ts-ignore
expect(errors.getTitle(error)).to.equal('1');
});
it('should accept a number -1', function () {
const error = -1;
// @ts-ignore
expect(errors.getTitle(error)).to.equal('-1');
});
it('should accept an array', function () {
const error = [0, 1, 2];
// @ts-ignore
expect(errors.getTitle(error)).to.equal('0,1,2');
});
it('should return a generic error message if the error is an empty object', function () {
const error = {};
// @ts-ignore
expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should return a generic error message if the error is undefined', function () {
const error = undefined;
// @ts-ignore
expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should return a generic error message if the error is null', function () {
const error = null;
// @ts-ignore
expect(errors.getTitle(error)).to.equal('An error ocurred');
});
it('should return the error message', function () {
const error = new Error('This is an error');
expect(errors.getTitle(error)).to.equal('This is an error');
@ -234,42 +192,6 @@ describe('Shared: Errors', function () {
});
describe('.getDescription()', function () {
it('should return an empty string if the error is a string', function () {
const error = 'My error';
// @ts-ignore
expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is a number', function () {
const error = 0;
// @ts-ignore
expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is an array', function () {
const error = [1, 2, 3];
// @ts-ignore
expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is undefined', function () {
const error = undefined;
// @ts-ignore
expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is null', function () {
const error = null;
// @ts-ignore
expect(errors.getDescription(error)).to.equal('');
});
it('should return an empty string if the error is an empty object', function () {
const error = {};
// @ts-ignore
expect(errors.getDescription(error)).to.equal('');
});
it('should understand an error-like object with a description', function () {
const error = {
description: 'My description',
@ -384,122 +306,26 @@ describe('Shared: Errors', function () {
describe('given userFriendlyDescriptionsOnly is false', function () {
it('should return the stack for a basic error', function () {
const error = new Error('Foo');
expect(
errors.getDescription(error, {
userFriendlyDescriptionsOnly: false,
}),
).to.equal(error.stack);
expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should return the stack if the description is an empty string', function () {
const error = new Error('Foo');
// @ts-ignore
error.description = '';
expect(
errors.getDescription(error, {
userFriendlyDescriptionsOnly: false,
}),
).to.equal(error.stack);
expect(errors.getDescription(error)).to.equal(error.stack);
});
it('should return the stack if the description is a blank string', function () {
const error = new Error('Foo');
// @ts-ignore
error.description = ' ';
expect(
errors.getDescription(error, {
userFriendlyDescriptionsOnly: false,
}),
).to.equal(error.stack);
});
});
describe('given userFriendlyDescriptionsOnly is true', function () {
it('should return an empty string for a basic error', function () {
const error = new Error('Foo');
expect(
errors.getDescription(error, {
userFriendlyDescriptionsOnly: true,
}),
).to.equal('');
});
it('should return an empty string if the description is an empty string', function () {
const error = new Error('Foo');
// @ts-ignore
error.description = '';
expect(
errors.getDescription(error, {
userFriendlyDescriptionsOnly: true,
}),
).to.equal('');
});
it('should return an empty string if the description is a blank string', function () {
const error = new Error('Foo');
// @ts-ignore
error.description = ' ';
expect(
errors.getDescription(error, {
userFriendlyDescriptionsOnly: true,
}),
).to.equal('');
expect(errors.getDescription(error)).to.equal(error.stack);
});
});
});
describe('.createError()', function () {
it('should not be a user error', function () {
const error = errors.createError({
title: 'Foo',
description: 'Something happened',
});
expect(errors.isUserError(error)).to.be.false;
});
it('should be a user error if `options.report` is false', function () {
const error = errors.createError({
title: 'Foo',
description: 'Something happened',
report: false,
});
expect(errors.isUserError(error)).to.be.true;
});
it('should be a user error if `options.report` evaluates to false', function () {
const error = errors.createError({
title: 'Foo',
description: 'Something happened',
// @ts-ignore
report: 0,
});
expect(errors.isUserError(error)).to.be.true;
});
it('should not be a user error if `options.report` is true', function () {
const error = errors.createError({
title: 'Foo',
description: 'Something happened',
report: true,
});
expect(errors.isUserError(error)).to.be.false;
});
it('should not be a user error if `options.report` evaluates to true', function () {
const error = errors.createError({
title: 'Foo',
description: 'Something happened',
// @ts-ignore
report: 1,
});
expect(errors.isUserError(error)).to.be.false;
});
it('should be an instance of Error', function () {
const error = errors.createError({
title: 'Foo',
@ -523,10 +349,10 @@ describe('Shared: Errors', function () {
const error = errors.createError({
title: 'Foo',
description: 'Something happened',
code: 'HELLO',
code: 'ENOENT',
});
expect(error.code).to.equal('HELLO');
expect(error.code).to.equal('ENOENT');
});
it('should correctly add only a title', function () {
@ -590,15 +416,6 @@ describe('Shared: Errors', function () {
});
describe('.createUserError()', function () {
it('should be a user error', function () {
const error = errors.createUserError({
title: 'Foo',
description: 'Something happened',
});
expect(errors.isUserError(error)).to.be.true;
});
it('should be an instance of Error', function () {
const error = errors.createUserError({
title: 'Foo',
@ -632,11 +449,11 @@ describe('Shared: Errors', function () {
// @ts-ignore
const error = errors.createUserError({
title: 'Foo',
code: 'HELLO',
code: 'ENOENT',
});
// @ts-ignore
expect(error.code).to.equal('HELLO');
expect(error.code).to.equal('ENOENT');
});
it('should ignore an empty description', function () {
@ -692,26 +509,6 @@ describe('Shared: Errors', function () {
});
});
describe('.isUserError()', function () {
_.each([0, '', false], (value) => {
it(`should return true if report equals ${value}`, function () {
const error = new Error('foo bar');
// @ts-ignore
error.report = value;
expect(errors.isUserError(error)).to.be.true;
});
});
_.each([undefined, null, true, 1, 3, 'foo'], (value) => {
it(`should return false if report equals ${value}`, function () {
const error = new Error('foo bar');
// @ts-ignore
error.report = value;
expect(errors.isUserError(error)).to.be.false;
});
});
});
describe('.toJSON()', function () {
it('should convert a simple error', function () {
const error = new Error('My error');