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', title: 'The writer process ended unexpectedly',
description: description:
'Please try again, and contact the Etcher team if the problem persists', 'Please try again, and contact the Etcher team if the problem persists',
code: 'ECHILDDIED',
}), }),
); );
return; return;

View File

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

View File

@ -14,48 +14,37 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash'; export type ErrorWithPath = Error & {
path?: string;
function createErrorDetails(options: { code?: keyof typeof HUMAN_FRIENDLY;
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'],
);
}
/** /**
* @summary Human-friendly error messages * @summary Human-friendly error messages
*/ */
export const HUMAN_FRIENDLY = { export const HUMAN_FRIENDLY = {
ENOENT: createErrorDetails({ ENOENT: {
title: (error: Error & { path: string }) => { title: (error: ErrorWithPath) => {
return `No such file or directory: ${error.path}`; return `No such file or directory: ${error.path}`;
}, },
description: "The file you're trying to access doesn't exist", description: () => "The file you're trying to access doesn't exist",
}), },
EPERM: createErrorDetails({ EPERM: {
title: "You're not authorized to perform this operation", title: () => "You're not authorized to perform this operation",
description: 'Please ensure you have necessary permissions for this task', description: () =>
}), 'Please ensure you have necessary permissions for this task',
EACCES: createErrorDetails({ },
title: "You don't have access to this resource", EACCES: {
description: title: () => "You don't have access to this resource",
description: () =>
'Please ensure you have necessary permissions to access this resource', 'Please ensure you have necessary permissions to access this resource',
}), },
ENOMEM: createErrorDetails({ ENOMEM: {
title: 'Your system ran out of memory', title: () => 'Your system ran out of memory',
description: description: () =>
'Please make sure your system has enough available memory for this task', 'Please make sure your system has enough available memory for this task',
}), },
}; } as const;
/** /**
* @summary Get user friendly property from an error * @summary Get user friendly property from an error
@ -71,19 +60,21 @@ export const HUMAN_FRIENDLY = {
* } * }
*/ */
function getUserFriendlyMessageProperty( function getUserFriendlyMessageProperty(
error: Error, error: ErrorWithPath,
property: 'title' | 'description', property: 'title' | 'description',
): string | null { ): string | undefined {
const code = _.get(error, ['code']); if (typeof error.code !== 'string') {
return undefined;
if (_.isNil(code) || !_.isString(code)) {
return null;
} }
return HUMAN_FRIENDLY[error.code]?.[property]?.(error);
return _.invoke(HUMAN_FRIENDLY, [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 * @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 * Try to get as much information as possible about the error
* rather than falling back to generic messages right away. * rather than falling back to generic messages right away.
*/ */
export function getTitle(error: Error): string { export function getTitle(error: ErrorWithPath): string {
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
return _.toString(error);
}
const codeTitle = getUserFriendlyMessageProperty(error, 'title'); const codeTitle = getUserFriendlyMessageProperty(error, 'title');
if (!_.isNil(codeTitle)) { if (codeTitle !== undefined) {
return codeTitle; return codeTitle;
} }
const message = _.get(error, ['message']); const message = error.message;
if (!isBlank(message)) { if (!isBlank(message)) {
return message; return message;
} }
const code = _.get(error, ['code']); const code = error.code;
if (!_.isNil(code) && !isBlank(code)) { if (!isBlank(code)) {
return `Error code: ${code}`; return `Error code: ${code}`;
} }
@ -119,40 +106,19 @@ export function getTitle(error: Error): string {
* @summary Get the description of an error * @summary Get the description of an error
*/ */
export function getDescription( export function getDescription(
error: Error & { description?: string }, error: ErrorWithPath & { description?: string },
options: { userFriendlyDescriptionsOnly?: boolean } = {},
): string { ): string {
_.defaults(options, {
userFriendlyDescriptionsOnly: false,
});
if (!_.isError(error) && !_.isPlainObject(error)) {
return '';
}
if (!isBlank(error.description)) { if (!isBlank(error.description)) {
return error.description as string; return error.description as string;
} }
const codeDescription = getUserFriendlyMessageProperty(error, 'description'); const codeDescription = getUserFriendlyMessageProperty(error, 'description');
if (!_.isNil(codeDescription)) { if (codeDescription !== undefined) {
return codeDescription; return codeDescription;
} }
if (options.userFriendlyDescriptionsOnly) {
return '';
}
if (error.stack) { if (error.stack) {
return error.stack; return error.stack;
} }
return JSON.stringify(error, null, 2);
if (_.isEmpty(error)) {
return '';
}
const INDENTATION_SPACES = 2;
return JSON.stringify(error, null, INDENTATION_SPACES);
} }
/** /**
@ -162,24 +128,24 @@ export function createError(options: {
title: string; title: string;
description?: string; description?: string;
report?: boolean; report?: boolean;
code?: string; code?: keyof typeof HUMAN_FRIENDLY;
}): Error & { description?: string; report?: boolean; code?: string } { }): ErrorWithPath & { description?: string; report?: boolean } {
if (isBlank(options.title)) { if (isBlank(options.title)) {
throw new Error(`Invalid error title: ${options.title}`); throw new Error(`Invalid error title: ${options.title}`);
} }
const error: Error & { const error: ErrorWithPath & {
description?: string; description?: string;
report?: boolean; report?: boolean;
code?: string; code?: string;
} = new Error(options.title); } = new Error(options.title);
error.description = options.description; error.description = options.description;
if (!_.isNil(options.report) && !options.report) { if (options.report === false) {
error.report = false; error.report = false;
} }
if (!_.isNil(options.code)) { if (options.code !== undefined) {
error.code = options.code; error.code = options.code;
} }
@ -198,7 +164,7 @@ export function createError(options: {
export function createUserError(options: { export function createUserError(options: {
title: string; title: string;
description: string; description: string;
code?: string; code?: keyof typeof HUMAN_FRIENDLY;
}): Error { }): Error {
return createError({ return createError({
title: options.title, 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 * @summary Convert an Error object to a JSON object
* @function * @function
@ -260,5 +219,5 @@ export function toJSON(
* @summary Convert a JSON object to an Error object * @summary Convert a JSON object to an Error object
*/ */
export function fromJSON(json: any): Error { 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 "dev": true
}, },
"etcher-sdk": { "etcher-sdk": {
"version": "4.1.20", "version": "4.1.21",
"resolved": "https://registry.npmjs.org/etcher-sdk/-/etcher-sdk-4.1.20.tgz", "resolved": "https://registry.npmjs.org/etcher-sdk/-/etcher-sdk-4.1.21.tgz",
"integrity": "sha512-cVjAifAKKYDc9SNMICnK4YWXHz3uGlwKgYOkRykPATxkwhVYfaW6wlhV2DFqbfkps+l4si7iC82Tz1RaV0VjHA==", "integrity": "sha512-jQzKZxYkUA9+rsmelr6U1d2pBwiRw6AGM//a8EnqL1Aorq44xq2BZ59BomDwofzMJ5RCGEJjgOmi7/w+eUVVvg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@balena/udif": "^1.0.3", "@balena/udif": "^1.0.3",
@ -7035,7 +7035,6 @@
"drivelist": "^9.0.0", "drivelist": "^9.0.0",
"file-disk": "^8.0.0", "file-disk": "^8.0.0",
"file-type": "^8.0.0", "file-type": "^8.0.0",
"lodash": "^4.17.19",
"lzma-native": "^6.0.0", "lzma-native": "^6.0.0",
"mountutils": "^1.3.18", "mountutils": "^1.3.18",
"node-raspberrypi-usbboot": "^0.2.9", "node-raspberrypi-usbboot": "^0.2.9",

View File

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

View File

@ -37,54 +37,12 @@ describe('Shared: Errors', function () {
}); });
describe('.getTitle()', 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 () { it('should return a generic error message if the error is an empty object', function () {
const error = {}; const error = {};
// @ts-ignore // @ts-ignore
expect(errors.getTitle(error)).to.equal('An error ocurred'); 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 () { it('should return the error message', function () {
const error = new Error('This is an error'); const error = new Error('This is an error');
expect(errors.getTitle(error)).to.equal('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 () { 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 () { it('should understand an error-like object with a description', function () {
const error = { const error = {
description: 'My description', description: 'My description',
@ -384,122 +306,26 @@ describe('Shared: Errors', function () {
describe('given userFriendlyDescriptionsOnly is false', function () { describe('given userFriendlyDescriptionsOnly is false', function () {
it('should return the stack for a basic error', function () { it('should return the stack for a basic error', function () {
const error = new Error('Foo'); const error = new Error('Foo');
expect( expect(errors.getDescription(error)).to.equal(error.stack);
errors.getDescription(error, {
userFriendlyDescriptionsOnly: false,
}),
).to.equal(error.stack);
}); });
it('should return the stack if the description is an empty string', function () { it('should return the stack if the description is an empty string', function () {
const error = new Error('Foo'); const error = new Error('Foo');
// @ts-ignore // @ts-ignore
error.description = ''; error.description = '';
expect( expect(errors.getDescription(error)).to.equal(error.stack);
errors.getDescription(error, {
userFriendlyDescriptionsOnly: false,
}),
).to.equal(error.stack);
}); });
it('should return the stack if the description is a blank string', function () { it('should return the stack if the description is a blank string', function () {
const error = new Error('Foo'); const error = new Error('Foo');
// @ts-ignore // @ts-ignore
error.description = ' '; error.description = ' ';
expect( expect(errors.getDescription(error)).to.equal(error.stack);
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('');
}); });
}); });
}); });
describe('.createError()', function () { 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 () { it('should be an instance of Error', function () {
const error = errors.createError({ const error = errors.createError({
title: 'Foo', title: 'Foo',
@ -523,10 +349,10 @@ describe('Shared: Errors', function () {
const error = errors.createError({ const error = errors.createError({
title: 'Foo', title: 'Foo',
description: 'Something happened', 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 () { it('should correctly add only a title', function () {
@ -590,15 +416,6 @@ describe('Shared: Errors', function () {
}); });
describe('.createUserError()', 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 () { it('should be an instance of Error', function () {
const error = errors.createUserError({ const error = errors.createUserError({
title: 'Foo', title: 'Foo',
@ -632,11 +449,11 @@ describe('Shared: Errors', function () {
// @ts-ignore // @ts-ignore
const error = errors.createUserError({ const error = errors.createUserError({
title: 'Foo', title: 'Foo',
code: 'HELLO', code: 'ENOENT',
}); });
// @ts-ignore // @ts-ignore
expect(error.code).to.equal('HELLO'); expect(error.code).to.equal('ENOENT');
}); });
it('should ignore an empty description', function () { 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 () { describe('.toJSON()', function () {
it('should convert a simple error', function () { it('should convert a simple error', function () {
const error = new Error('My error'); const error = new Error('My error');