From 370677032228a1f507f3a97ea749cfb37db7ab46 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 20 Jun 2019 17:40:53 +0200 Subject: [PATCH] Convert utils, settings and errors to typescript Change-type: patch Changelog-entry: Convert utils, settings and errors to typescript --- Makefile | 2 +- lib/gui/app/app.js | 2 + .../controllers/drive-selector.js | 1 + .../controllers/file-selector.js | 2 + .../{local-settings.js => local-settings.ts} | 97 ++--- lib/gui/app/models/settings.js | 232 ----------- lib/gui/app/models/settings.ts | 101 +++++ lib/gui/app/models/store.js | 3 + lib/gui/app/modules/analytics.js | 2 + lib/gui/app/modules/child-writer.js | 1 + lib/gui/app/modules/drive-scanner.js | 1 + lib/gui/app/modules/errors.js | 369 ------------------ lib/gui/app/modules/errors.ts | 347 ++++++++++++++++ lib/gui/app/modules/image-writer.js | 2 + lib/gui/app/modules/permissions.js | 2 + lib/gui/app/modules/progress-status.js | 2 + lib/gui/app/modules/update-lock.js | 1 + lib/gui/app/modules/{utils.js => utils.ts} | 148 +++---- lib/gui/app/os/dialog.js | 1 + lib/gui/app/os/notification.js | 1 + .../open-external/services/open-external.js | 1 + lib/gui/app/os/window-progress.js | 1 + lib/gui/app/os/windows-network-drives.js | 1 + .../app/pages/finish/controllers/finish.js | 1 + .../pages/main/controllers/drive-selection.js | 2 + .../pages/main/controllers/image-selection.js | 2 + lib/gui/app/pages/main/controllers/main.js | 1 + .../pages/settings/controllers/settings.js | 1 + .../manifest-bind/directives/manifest-bind.js | 1 + lib/gui/etcher.js | 2 + lib/start.js | 2 +- npm-shrinkwrap.json | 90 ++++- package.json | 4 + tests/gui/components/drive-selector.spec.js | 1 + tests/gui/models/settings.spec.js | 105 ++--- tests/gui/modules/progress-status.spec.js | 1 + tests/shared/errors.spec.js | 68 +--- tests/shared/utils.spec.js | 1 + tsconfig.json | 4 +- webpack.config.js | 17 +- 40 files changed, 782 insertions(+), 841 deletions(-) rename lib/gui/app/models/{local-settings.js => local-settings.ts} (60%) delete mode 100644 lib/gui/app/models/settings.js create mode 100644 lib/gui/app/models/settings.ts delete mode 100644 lib/gui/app/modules/errors.js create mode 100644 lib/gui/app/modules/errors.ts rename lib/gui/app/modules/{utils.js => utils.ts} (56%) diff --git a/Makefile b/Makefile index bb64d0b9..25708218 100644 --- a/Makefile +++ b/Makefile @@ -174,7 +174,7 @@ lint-spell: lint: lint-ts lint-js lint-sass lint-cpp lint-html lint-spell -MOCHA_OPTIONS=--recursive --reporter spec +MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register # See https://github.com/electron/spectron/issues/127 ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))') diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 24b6df34..235e7d9c 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -36,6 +36,7 @@ const messages = require('../../gui/app/modules/messages') const store = require('./models/store') const packageJSON = require('../../../package.json') const flashState = require('./models/flash-state') +// eslint-disable-next-line node/no-missing-require const settings = require('./models/settings') const windowProgress = require('./os/window-progress') const analytics = require('./modules/analytics') @@ -45,6 +46,7 @@ const driveScanner = require('./modules/drive-scanner') const osDialog = require('./os/dialog') const exceptionReporter = require('./modules/exception-reporter') const updateLock = require('./modules/update-lock') +// eslint-disable-next-line node/no-missing-require const screensaver = require('./modules/screensaver') /* eslint-disable lodash/prefer-lodash-method,lodash/prefer-get */ diff --git a/lib/gui/app/components/drive-selector/controllers/drive-selector.js b/lib/gui/app/components/drive-selector/controllers/drive-selector.js index 119ba5a8..c67db7f8 100644 --- a/lib/gui/app/components/drive-selector/controllers/drive-selector.js +++ b/lib/gui/app/components/drive-selector/controllers/drive-selector.js @@ -24,6 +24,7 @@ const store = require('../../../models/store') const analytics = require('../../../modules/analytics') const availableDrives = require('../../../models/available-drives') const selectionState = require('../../../models/selection-state') +// eslint-disable-next-line node/no-missing-require const utils = require('../../../../../gui/app/modules/utils') module.exports = function ( diff --git a/lib/gui/app/components/file-selector/controllers/file-selector.js b/lib/gui/app/components/file-selector/controllers/file-selector.js index e2fab386..6e46468a 100644 --- a/lib/gui/app/components/file-selector/controllers/file-selector.js +++ b/lib/gui/app/components/file-selector/controllers/file-selector.js @@ -18,7 +18,9 @@ const _ = require('lodash') const os = require('os') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../models/settings') +// eslint-disable-next-line node/no-missing-require const utils = require('../../../../../gui/app/modules/utils') const angular = require('angular') diff --git a/lib/gui/app/models/local-settings.js b/lib/gui/app/models/local-settings.ts similarity index 60% rename from lib/gui/app/models/local-settings.js rename to lib/gui/app/models/local-settings.ts index 94c0387b..81ff7398 100644 --- a/lib/gui/app/models/local-settings.js +++ b/lib/gui/app/models/local-settings.ts @@ -14,18 +14,21 @@ * limitations under the License. */ -'use strict' +import { app, remote } from 'electron'; +import { readFile, unlink, writeFile } from 'fs'; +import { join } from 'path'; +import { promisify } from 'util'; -const Bluebird = require('bluebird') -const fs = require('fs') -const path = require('path') +const readFileAsync = promisify(readFile); +const writeFileAsync = promisify(writeFile); +const unlinkAsync = promisify(unlink); /** * @summary Number of spaces to indent JSON output with * @type {Number} * @constant */ -const JSON_INDENT = 2 +const JSON_INDENT = 2; /** * @summary Userdata directory path @@ -38,21 +41,16 @@ const JSON_INDENT = 2 * @constant * @type {String} */ -const USER_DATA_DIR = (() => { - // NOTE: The ternary is due to this module being loaded both, - // Electron's main process and renderer process - const electron = require('electron') - return electron.app - ? electron.app.getPath('userData') - : electron.remote.app.getPath('userData') -})() +// NOTE: The ternary is due to this module being loaded both, +// Electron's main process and renderer process +const USER_DATA_DIR = (app || remote.app).getPath('userData'); /** * @summary Configuration file path * @type {String} * @constant */ -const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json') +const CONFIG_PATH = join(USER_DATA_DIR, 'config.json'); /** * @summary Read a local config.json file @@ -68,26 +66,22 @@ const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json') * console.log(settings) * }) */ -const readConfigFile = (filename) => { - return new Bluebird((resolve, reject) => { - fs.readFile(filename, { encoding: 'utf8' }, (error, contents) => { - let data = {} - if (error) { - if (error.code === 'ENOENT') { - resolve(data) - } else { - reject(error) - } - } else { - try { - data = JSON.parse(contents) - } catch (parseError) { - console.error(parseError) - } - resolve(data) - } - }) - }) +async function readConfigFile(filename: string): Promise { + let contents = '{}'; + try { + contents = await readFileAsync(filename, { encoding: 'utf8' }); + } catch (error) { + if (error.code === 'ENOENT') { + return {}; + } + throw error; + } + try { + return JSON.parse(contents); + } catch (error) { + console.error(error); + return {}; + } } /** @@ -106,17 +100,10 @@ const readConfigFile = (filename) => { * console.log('data written') * }) */ -const writeConfigFile = (filename, data) => { - return new Bluebird((resolve, reject) => { - const contents = JSON.stringify(data, null, JSON_INDENT) - fs.writeFile(filename, contents, (error) => { - if (error) { - reject(error) - } else { - resolve(data) - } - }) - }) +async function writeConfigFile(filename: string, data: any) { + const contents = JSON.stringify(data, null, JSON_INDENT); + await writeFileAsync(filename, contents); + return data; } /** @@ -132,8 +119,8 @@ const writeConfigFile = (filename, data) => { * console.log(settings); * }); */ -exports.readAll = () => { - return readConfigFile(CONFIG_PATH) +export async function readAll(): Promise { + return await readConfigFile(CONFIG_PATH); } /** @@ -152,8 +139,8 @@ exports.readAll = () => { * console.log('Done!'); * }); */ -exports.writeAll = (settings) => { - return writeConfigFile(CONFIG_PATH, settings) +export async function writeAll(settings: any) { + return await writeConfigFile(CONFIG_PATH, settings); } /** @@ -171,14 +158,6 @@ exports.writeAll = (settings) => { * console.log('Done!'); * }); */ -exports.clear = () => { - return new Bluebird((resolve, reject) => { - fs.unlink(CONFIG_PATH, (error) => { - if (error) { - reject(error) - } else { - resolve() - } - }) - }) +export async function clear(): Promise { + await unlinkAsync(CONFIG_PATH); } diff --git a/lib/gui/app/models/settings.js b/lib/gui/app/models/settings.js deleted file mode 100644 index 9e3b7ee1..00000000 --- a/lib/gui/app/models/settings.js +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2016 resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -/** - * @module Etcher.Models.Settings - */ - -const _ = require('lodash') -const Bluebird = require('bluebird') -const localSettings = require('./local-settings') -const errors = require('../modules/errors') -const packageJSON = require('../../../../package.json') -const debug = require('debug')('etcher:models:settings') - -/** - * @summary Default settings - * @constant - * @type {Object} - */ -const DEFAULT_SETTINGS = { - unsafeMode: false, - errorReporting: true, - unmountOnSuccess: true, - validateWriteOnSuccess: true, - trim: false, - updatesEnabled: packageJSON.updates.enabled && !_.includes([ 'rpm', 'deb' ], packageJSON.packageType), - lastSleptUpdateNotifier: null, - lastSleptUpdateNotifierVersion: null, - desktopNotifications: true -} - -/** - * @summary Settings state - * @type {Object} - * @private - */ -let settings = _.cloneDeep(DEFAULT_SETTINGS) - -/** - * @summary Reset settings to their default values - * @function - * @public - * - * @returns {Promise} - * - * @example - * settings.reset().then(() => { - * console.log('Done!'); - * }); - */ -exports.reset = () => { - debug('reset') - - // TODO: Remove default settings from config file (?) - settings = _.cloneDeep(DEFAULT_SETTINGS) - return localSettings.writeAll(settings) -} - -/** - * @summary Extend the current settings - * @function - * @public - * - * @param {Object} value - value - * @returns {Promise} - * - * @example - * settings.assign({ - * foo: 'bar' - * }).then(() => { - * console.log('Done!'); - * }); - */ -exports.assign = (value) => { - debug('assign', value) - if (_.isNil(value)) { - return Bluebird.reject(errors.createError({ - title: 'Missing settings' - })) - } - - if (!_.isPlainObject(value)) { - return Bluebird.reject(errors.createError({ - title: 'Settings must be an object' - })) - } - - const newSettings = _.assign({}, settings, value) - - return localSettings.writeAll(newSettings) - .then((updatedSettings) => { - // NOTE: Only update in memory settings when successfully written - settings = updatedSettings - }) -} - -/** - * @summary Extend the application state with the local settings - * @function - * @public - * - * @returns {Promise} - * - * @example - * settings.load().then(() => { - * console.log('Done!'); - * }); - */ -exports.load = () => { - debug('load') - return localSettings.readAll().then((loadedSettings) => { - return _.assign(settings, loadedSettings) - }) -} - -/** - * @summary Set a setting value - * @function - * @public - * - * @param {String} key - setting key - * @param {*} value - setting value - * @returns {Promise} - * - * @example - * settings.set('unmountOnSuccess', true).then(() => { - * console.log('Done!'); - * }); - */ -exports.set = (key, value) => { - debug('set', key, value) - if (_.isNil(key)) { - return Bluebird.reject(errors.createError({ - title: 'Missing setting key' - })) - } - - if (!_.isString(key)) { - return Bluebird.reject(errors.createError({ - title: `Invalid setting key: ${key}` - })) - } - - const previousValue = settings[key] - - settings[key] = value - - return localSettings.writeAll(settings) - .catch((error) => { - // Revert to previous value if persisting settings failed - settings[key] = previousValue - throw error - }) -} - -/** - * @summary Get a setting value - * @function - * @public - * - * @param {String} key - setting key - * @returns {*} setting value - * - * @example - * const value = settings.get('unmountOnSuccess'); - */ -exports.get = (key) => { - return _.cloneDeep(_.get(settings, [ key ])) -} - -/** - * @summary Check if setting value exists - * @function - * @public - * - * @param {String} key - setting key - * @returns {Boolean} exists - * - * @example - * const hasValue = settings.has('unmountOnSuccess'); - */ -exports.has = (key) => { - /* eslint-disable no-eq-null */ - return settings[key] != null -} - -/** - * @summary Get all setting values - * @function - * @public - * - * @returns {Object} all setting values - * - * @example - * const allSettings = settings.getAll(); - * console.log(allSettings.unmountOnSuccess); - */ -exports.getAll = () => { - debug('getAll') - return _.cloneDeep(settings) -} - -/** - * @summary Get the default setting values - * @function - * @public - * - * @returns {Object} all setting values - * - * @example - * const defaults = settings.getDefaults(); - * console.log(defaults.unmountOnSuccess); - */ -exports.getDefaults = () => { - debug('getDefaults') - return _.cloneDeep(DEFAULT_SETTINGS) -} diff --git a/lib/gui/app/models/settings.ts b/lib/gui/app/models/settings.ts new file mode 100644 index 00000000..03ad5090 --- /dev/null +++ b/lib/gui/app/models/settings.ts @@ -0,0 +1,101 @@ +/* + * Copyright 2016 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as debug_ from 'debug'; +import { cloneDeep, isPlainObject } from 'lodash'; + +import { createError } from '../modules/errors'; +import { Dict } from '../modules/utils'; +import { readAll, writeAll } from './local-settings'; + +import * as packageJSON from '../../../../package.json'; + +const debug = debug_('etcher:models:settings'); + +const DEFAULT_SETTINGS = { + unsafeMode: false, + errorReporting: true, + unmountOnSuccess: true, + validateWriteOnSuccess: true, + trim: false, + updatesEnabled: + packageJSON.updates.enabled && + !['rpm', 'deb'].includes(packageJSON.packageType), + lastSleptUpdateNotifier: null, + lastSleptUpdateNotifierVersion: null, + desktopNotifications: true, +}; + +let settings: Dict = cloneDeep(DEFAULT_SETTINGS); + +export async function reset(): Promise { + debug('reset'); + // TODO: Remove default settings from config file (?) + settings = cloneDeep(DEFAULT_SETTINGS); + await writeAll(settings); +} + +export async function assign(value: any): Promise { + debug('assign', value); + if (!isPlainObject(value)) { + throw createError({ title: 'Settings must be an object' }); + } + const newSettings = { ...settings, ...value }; + const updatedSettings = await writeAll(newSettings); + // NOTE: Only update in memory settings when successfully written + settings = updatedSettings; +} + +export async function load(): Promise { + debug('load'); + const loadedSettings = await readAll(); + settings = { ...settings, ...loadedSettings }; + return settings; +} + +export async function set(key: string, value: any): Promise { + debug('set', key, value); + if (typeof key !== 'string') { + throw createError({ title: `Invalid setting key: ${key}` }); + } + const previousValue = settings[key]; + settings[key] = value; + try { + await writeAll(settings); + } catch (error) { + // Revert to previous value if persisting settings failed + settings[key] = previousValue; + throw error; + } +} + +export function get(key: string): any { + return cloneDeep(settings[key]); +} + +export function has(key: string): boolean { + return settings[key] !== undefined; +} + +export function getAll(): any { + debug('getAll'); + return cloneDeep(settings); +} + +export function getDefaults(): any { + debug('getDefaults'); + return cloneDeep(DEFAULT_SETTINGS); +} diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index 94d7e378..ef3e471c 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -22,9 +22,12 @@ const redux = require('redux') const uuidV4 = require('uuid/v4') const constraints = require('../modules/drive-constraints') const supportedFormats = require('../modules/supported-formats') +// eslint-disable-next-line node/no-missing-require const errors = require('../modules/errors') const fileExtensions = require('../modules/file-extensions') +// eslint-disable-next-line node/no-missing-require const utils = require('../modules/utils') +// eslint-disable-next-line node/no-missing-require const settings = require('./settings') /** diff --git a/lib/gui/app/modules/analytics.js b/lib/gui/app/modules/analytics.js index f6ba83e2..43c45e10 100644 --- a/lib/gui/app/modules/analytics.js +++ b/lib/gui/app/modules/analytics.js @@ -19,7 +19,9 @@ const _ = require('lodash') const resinCorvus = require('resin-corvus/browser') const packageJSON = require('../../../../package.json') +// eslint-disable-next-line node/no-missing-require const settings = require('../models/settings') +// eslint-disable-next-line node/no-missing-require const { getConfig, hasProps } = require('../../../gui/app/modules/utils') const sentryToken = settings.get('analyticsSentryToken') || diff --git a/lib/gui/app/modules/child-writer.js b/lib/gui/app/modules/child-writer.js index 1110d05d..7beb46ca 100644 --- a/lib/gui/app/modules/child-writer.js +++ b/lib/gui/app/modules/child-writer.js @@ -21,6 +21,7 @@ const _ = require('lodash') const ipc = require('node-ipc') const sdk = require('etcher-sdk') const EXIT_CODES = require('./exit-codes') +// eslint-disable-next-line node/no-missing-require const errors = require('./errors') ipc.config.id = process.env.IPC_CLIENT_ID diff --git a/lib/gui/app/modules/drive-scanner.js b/lib/gui/app/modules/drive-scanner.js index 614349e6..f9912840 100644 --- a/lib/gui/app/modules/drive-scanner.js +++ b/lib/gui/app/modules/drive-scanner.js @@ -19,6 +19,7 @@ const sdk = require('etcher-sdk') const process = require('process') +// eslint-disable-next-line node/no-missing-require const settings = require('../models/settings') /** diff --git a/lib/gui/app/modules/errors.js b/lib/gui/app/modules/errors.js deleted file mode 100644 index 7756cb6c..00000000 --- a/lib/gui/app/modules/errors.js +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright 2016 resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') - -/** - * @summary Create an error details object - * @function - * @private - * - * @param {Object} options - options - * @param {(String|Function)} options.title - error title - * @param {(String|Function)} options.description - error description - * @returns {Object} error details object - * - * @example - * const details = createErrorDetails({ - * title: (error) => { - * return `An error happened, the code is ${error.code}`; - * }, - * description: 'This is the error description' - * }); - */ -const createErrorDetails = (options) => { - return _.pick(_.mapValues(options, (value) => { - return _.isFunction(value) ? value : _.constant(value) - }), [ 'title', 'description' ]) -} - -/** - * @summary Human-friendly error messages - * @namespace HUMAN_FRIENDLY - * @public - */ -exports.HUMAN_FRIENDLY = { - - /* eslint-disable new-cap */ - - /** - * @namespace ENOENT - * @memberof HUMAN_FRIENDLY - */ - ENOENT: createErrorDetails({ - title: (error) => { - return `No such file or directory: ${error.path}` - }, - description: 'The file you\'re trying to access doesn\'t exist' - }), - - /** - * @namespace EPERM - * @memberof HUMAN_FRIENDLY - */ - EPERM: createErrorDetails({ - title: 'You\'re not authorized to perform this operation', - description: 'Please ensure you have necessary permissions for this task' - }), - - /** - * @namespace EACCES - * @memberof HUMAN_FRIENDLY - */ - EACCES: createErrorDetails({ - title: 'You don\'t have access to this resource', - description: 'Please ensure you have necessary permissions to access this resource' - }), - - /** - * @namespace ENOMEM - * @memberof HUMAN_FRIENDLY - */ - ENOMEM: createErrorDetails({ - title: 'Your system ran out of memory', - description: 'Please make sure your system has enough available memory for this task' - }) - - /* eslint-enable new-cap */ - -} - -/** - * @summary Get user friendly property from an error - * @function - * @private - * - * @param {Error} error - error - * @param {String} property - HUMAN_FRIENDLY property - * @returns {(String|Undefined)} user friendly message - * - * @example - * const error = new Error('My error'); - * error.code = 'ENOMEM'; - * - * const friendlyDescription = getUserFriendlyMessageProperty(error, 'description'); - * - * if (friendlyDescription) { - * console.log(friendlyDescription); - * } - */ -const getUserFriendlyMessageProperty = (error, property) => { - const code = _.get(error, [ 'code' ]) - - if (_.isNil(code) || !_.isString(code)) { - return null - } - - return _.invoke(exports.HUMAN_FRIENDLY, [ code, property ], error) -} - -/** - * @summary Check if a string is blank - * @function - * @private - * - * @param {String} string - string - * @returns {Boolean} whether the string is blank - * - * @example - * if (isBlank(' ')) { - * console.log('The string is blank'); - * } - */ -const isBlank = _.flow([ _.trim, _.isEmpty ]) - -/** - * @summary Get the title of an error - * @function - * @public - * - * @description - * Try to get as much information as possible about the error - * rather than falling back to generic messages right away. - * - * @param {Error} error - error - * @returns {String} error title - * - * @example - * const error = new Error('Foo bar'); - * const title = errors.getTitle(error); - * console.log(title); - */ -exports.getTitle = (error) => { - if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) { - return _.toString(error) - } - - const codeTitle = getUserFriendlyMessageProperty(error, 'title') - if (!_.isNil(codeTitle)) { - return codeTitle - } - - const message = _.get(error, [ 'message' ]) - if (!isBlank(message)) { - return message - } - - const code = _.get(error, [ 'code' ]) - if (!_.isNil(code) && !isBlank(code)) { - return `Error code: ${code}` - } - - return 'An error ocurred' -} - -/** - * @summary Get the description of an error - * @function - * @public - * - * @param {Error} error - error - * @param {Object} options - options - * @param {Boolean} [options.userFriendlyDescriptionsOnly=false] - only return user friendly descriptions - * @returns {String} error description - * - * @example - * const error = new Error('Foo bar'); - * const description = errors.getDescription(error); - * console.log(description); - */ -exports.getDescription = (error, options = {}) => { - _.defaults(options, { - userFriendlyDescriptionsOnly: false - }) - - if (!_.isError(error) && !_.isPlainObject(error)) { - return '' - } - - if (!isBlank(error.description)) { - return error.description - } - - const codeDescription = getUserFriendlyMessageProperty(error, 'description') - if (!_.isNil(codeDescription)) { - 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) -} - -/** - * @summary Create an error - * @function - * @public - * - * @param {Object} options - options - * @param {String} options.title - error title - * @param {String} [options.description] - error description - * @param {Boolean} [options.report] - report error - * @returns {Error} error - * - * @example - * const error = errors.createError({ - * title: 'Foo' - * description: 'Bar' - * }); - * - * throw error; - */ -exports.createError = (options) => { - if (isBlank(options.title)) { - throw new Error(`Invalid error title: ${options.title}`) - } - - const error = new Error(options.title) - error.description = options.description - - if (!_.isNil(options.report) && !options.report) { - error.report = false - } - - if (!_.isNil(options.code)) { - error.code = options.code - } - - return error -} - -/** - * @summary Create a user error - * @function - * @public - * - * @description - * User errors represent invalid states that the user - * caused, that are not errors on the application itself. - * Therefore, user errors don't get reported to analytics - * and error reporting services. - * - * @param {Object} options - options - * @param {String} options.title - error title - * @param {String} [options.description] - error description - * @returns {Error} user error - * - * @example - * const error = errors.createUserError({ - * title: 'Foo', - * description: 'Bar' - * }); - * - * throw error; - */ -exports.createUserError = (options) => { - return exports.createError({ - title: options.title, - description: options.description, - report: false, - code: options.code - }) -} - -/** - * @summary Check if an error is an user error - * @function - * @public - * - * @param {Error} error - error - * @returns {Boolean} whether the error is a user error - * - * @example - * const error = errors.createUserError('Foo', 'Bar'); - * - * if (errors.isUserError(error)) { - * console.log('This error is a user error'); - * } - */ -exports.isUserError = (error) => { - return _.isNil(error.report) ? false : !error.report -} - -/** - * @summary Convert an Error object to a JSON object - * @function - * @public - * - * @param {Error} error - error object - * @returns {Object} json error - * - * @example - * const error = errors.toJSON(new Error('foo')) - * - * console.log(error.message); - * > 'foo' - */ -exports.toJSON = (error) => { - // Handle string error objects to be on the safe side - const isErrorLike = _.isError(error) || _.isPlainObject(error) - const errorObject = isErrorLike ? error : new Error(error) - - return { - name: errorObject.name, - message: errorObject.message, - description: errorObject.description, - stack: errorObject.stack, - report: errorObject.report, - code: errorObject.code, - syscall: errorObject.syscall, - errno: errorObject.errno, - stdout: errorObject.stdout, - stderr: errorObject.stderr, - device: errorObject.device - } -} - -/** - * @summary Convert a JSON object to an Error object - * @function - * @public - * - * @param {Error} json - json object - * @returns {Object} error object - * - * @example - * const error = errors.fromJSON(errors.toJSON(new Error('foo'))); - * - * console.log(error.message); - * > 'foo' - */ -exports.fromJSON = (json) => { - return _.assign(new Error(json.message), json) -} diff --git a/lib/gui/app/modules/errors.ts b/lib/gui/app/modules/errors.ts new file mode 100644 index 00000000..60684a47 --- /dev/null +++ b/lib/gui/app/modules/errors.ts @@ -0,0 +1,347 @@ +/* + * Copyright 2016 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + assign, + flow, + invoke, + isEmpty, + isError, + isNil, + isPlainObject, + isString, + toString, + trim, +} from 'lodash'; + +import { Dict } from './utils'; + +const INDENTATION_SPACES = 2; + +/** + * @summary Human-friendly error messages + */ +export const HUMAN_FRIENDLY: Dict<{ + title: (error?: { path?: string }) => string; + description: (error?: any) => string; +}> = { + ENOENT: { + title: (error: { path: string }) => + `No such file or directory: ${error.path}`, + 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: { + title: () => 'Your system ran out of memory', + description: () => + 'Please make sure your system has enough available memory for this task', + }, +}; + +/** + * @summary Get user friendly property from an error + * @function + * @private + * + * @param {Error} error - error + * @param {String} property - HUMAN_FRIENDLY property + * @returns {(String|Undefined)} user friendly message + * + * @example + * const error = new Error('My error'); + * error.code = 'ENOMEM'; + * + * const friendlyDescription = getUserFriendlyMessageProperty(error, 'description'); + * + * if (friendlyDescription) { + * console.log(friendlyDescription); + * } + */ +function getUserFriendlyMessageProperty( + error: { code?: string; path?: string }, + property: 'title' | 'description', +): string | null { + const code = error.code; + if (!isString(code)) { + return null; + } + return invoke(HUMAN_FRIENDLY, [code, property], error); +} + +/** + * @summary Check if a string is blank + * @function + * @private + * + * @param {String} string - string + * @returns {Boolean} whether the string is blank + * + * @example + * if (isBlank(' ')) { + * console.log('The string is blank'); + * } + */ +const isBlank = flow([trim, isEmpty]); + +/** + * @summary Get the title of an error + * @function + * @public + * + * @description + * Try to get as much information as possible about the error + * rather than falling back to generic messages right away. + * + * @param {Error} error - error + * @returns {String} error title + * + * @example + * const error = new Error('Foo bar'); + * const title = errors.getTitle(error); + * console.log(title); + */ +export function getTitle(error: Error | Dict): string { + if (!isError(error) && !isPlainObject(error) && !isNil(error)) { + return toString(error); + } + + const codeTitle = getUserFriendlyMessageProperty(error, 'title'); + if (!isNil(codeTitle)) { + return codeTitle; + } + + const message = error.message; + if (!isBlank(message)) { + return message; + } + + const code = error.code; + if (!isNil(code) && !isBlank(code)) { + return `Error code: ${code}`; + } + + return 'An error ocurred'; +} + +/** + * @summary Get the description of an error + * @function + * @public + * + * @param {Error} error - error + * @returns {String} error description + * + * @example + * const error = new Error('Foo bar'); + * const description = errors.getDescription(error); + * console.log(description); + */ +export function getDescription(error: { + code?: string; + description?: string; + stack?: string; +}): string { + if (!isError(error) && !isPlainObject(error)) { + return ''; + } + + if (!isBlank(error.description)) { + return error.description as string; + } + + const codeDescription = getUserFriendlyMessageProperty(error, 'description'); + if (!isNil(codeDescription)) { + return codeDescription; + } + + if (error.stack) { + return error.stack; + } + + if (isEmpty(error)) { + return ''; + } + + return JSON.stringify(error, null, INDENTATION_SPACES); +} + +/** + * @summary Create an error + * @function + * @public + * + * @param {Object} options - options + * @param {String} options.title - error title + * @param {String} [options.description] - error description + * @param {Boolean} [options.report] - report error + * @returns {Error} error + * + * @example + * const error = errors.createError({ + * title: 'Foo' + * description: 'Bar' + * }); + * + * throw error; + */ +export function createError(options: { + title: string; + description?: string; + report?: boolean; + code?: string; +}): Error & { description?: string; report?: boolean; code?: string } { + if (isBlank(options.title)) { + throw new Error(`Invalid error title: ${options.title}`); + } + + const error: Error & { + description?: string; + report?: boolean; + code?: string; + } = new Error(options.title); + error.description = options.description; + + if (!isNil(options.report) && !options.report) { + error.report = false; + } + + if (!isNil(options.code)) { + error.code = options.code; + } + + return error; +} + +/** + * @summary Create a user error + * @function + * @public + * + * @description + * User errors represent invalid states that the user + * caused, that are not errors on the application itself. + * Therefore, user errors don't get reported to analytics + * and error reporting services. + * + * @returns {Error} user error + * + * @example + * const error = errors.createUserError({ + * title: 'Foo', + * description: 'Bar' + * }); + * + * throw error; + */ +export function createUserError(options: { + title: string; + description: string; + code?: string; +}): Error { + return createError({ + title: options.title, + description: options.description, + report: false, + code: options.code, + }); +} + +/** + * @summary Check if an error is an user error + * @function + * @public + * + * @param {Error} error - error + * @returns {Boolean} whether the error is a user error + * + * @example + * const error = errors.createUserError('Foo', 'Bar'); + * + * if (errors.isUserError(error)) { + * console.log('This error is a user error'); + * } + */ +export function isUserError(error: { report?: boolean }): boolean { + return isNil(error.report) ? false : !error.report; +} + +/** + * @summary Convert an Error object to a JSON object + * @function + * @public + * + * @param {Error} error - error object + * @returns {Object} json error + * + * @example + * const error = errors.toJSON(new Error('foo')) + * + * console.log(error.message); + * > 'foo' + */ +export function toJSON( + error: Error & { + description?: string; + report?: boolean; + code?: string; + syscall?: string; + errno?: string | number; + stdout?: string; + stderr?: string; + device?: any; + }, +) { + return { + name: error.name, + message: error.message, + description: error.description, + stack: error.stack, + report: error.report, + code: error.code, + syscall: error.syscall, + errno: error.errno, + stdout: error.stdout, + stderr: error.stderr, + device: error.device, + }; +} + +/** + * @summary Convert a JSON object to an Error object + * @function + * @public + * + * @param {Error} json - json object + * @returns {Object} error object + * + * @example + * const error = errors.fromJSON(errors.toJSON(new Error('foo'))); + * + * console.log(error.message); + * > 'foo' + */ +export function fromJSON(json: Dict): Error { + return assign(new Error(json.message), json); +} diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index 95e9fc02..30e6e9f1 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -24,8 +24,10 @@ const ipc = require('node-ipc') const isRunningInAsar = require('electron-is-running-in-asar') const electron = require('electron') const store = require('../models/store') +// eslint-disable-next-line node/no-missing-require const settings = require('../models/settings') const flashState = require('../models/flash-state') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../gui/app/modules/errors') const permissions = require('../../../gui/app/modules/permissions') const windowProgress = require('../os/window-progress') diff --git a/lib/gui/app/modules/permissions.js b/lib/gui/app/modules/permissions.js index b051cc7f..ad6ed833 100755 --- a/lib/gui/app/modules/permissions.js +++ b/lib/gui/app/modules/permissions.js @@ -26,8 +26,10 @@ const os = require('os') const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt')) const { promisify } = require('util') +// eslint-disable-next-line node/no-missing-require const errors = require('./errors') +// eslint-disable-next-line node/no-missing-require const { tmpFileDisposer } = require('./utils') const writeFileAsync = promisify(fs.writeFile) diff --git a/lib/gui/app/modules/progress-status.js b/lib/gui/app/modules/progress-status.js index e11cf1b2..679af195 100644 --- a/lib/gui/app/modules/progress-status.js +++ b/lib/gui/app/modules/progress-status.js @@ -16,7 +16,9 @@ 'use strict' +// eslint-disable-next-line node/no-missing-require const settings = require('../models/settings') +// eslint-disable-next-line node/no-missing-require const utils = require('../../../gui/app/modules/utils') const units = require('../../../gui/app/modules/units') diff --git a/lib/gui/app/modules/update-lock.js b/lib/gui/app/modules/update-lock.js index 43fb1fb6..89385de3 100644 --- a/lib/gui/app/modules/update-lock.js +++ b/lib/gui/app/modules/update-lock.js @@ -21,6 +21,7 @@ const EventEmitter = require('events') const createInactivityTimer = require('inactivity-timer') const debug = require('debug')('etcher:update-lock') const analytics = require('./analytics') +// eslint-disable-next-line node/no-missing-require const settings = require('../models/settings') /* eslint-disable no-magic-numbers, callback-return */ diff --git a/lib/gui/app/modules/utils.js b/lib/gui/app/modules/utils.ts similarity index 56% rename from lib/gui/app/modules/utils.js rename to lib/gui/app/modules/utils.ts index 88189a79..3fc022e8 100755 --- a/lib/gui/app/modules/utils.js +++ b/lib/gui/app/modules/utils.ts @@ -14,14 +14,15 @@ * limitations under the License. */ -'use strict' +import * as Bluebird from 'bluebird'; +import * as _ from 'lodash'; +import * as request from 'request'; +import * as tmp from 'tmp'; +import { promisify } from 'util'; -const _ = require('lodash') -const Bluebird = require('bluebird') -const request = Bluebird.promisifyAll(require('request')) -const tmp = require('tmp') +import * as errors from './errors'; -const errors = require('./errors') +const getAsync = promisify(request.get); /** * @summary Minimum percentage value @@ -29,7 +30,7 @@ const errors = require('./errors') * @public * @type {Number} */ -exports.PERCENTAGE_MINIMUM = 0 +export const PERCENTAGE_MINIMUM = 0; /** * @summary Maximum percentage value @@ -37,7 +38,7 @@ exports.PERCENTAGE_MINIMUM = 0 * @public * @type {Number} */ -exports.PERCENTAGE_MAXIMUM = 100 +export const PERCENTAGE_MAXIMUM = 100; /** * @summary Check if a percentage is valid @@ -52,12 +53,12 @@ exports.PERCENTAGE_MAXIMUM = 100 * console.log('The percentage is valid'); * } */ -exports.isValidPercentage = (percentage) => { - return _.every([ - _.isNumber(percentage), - percentage >= exports.PERCENTAGE_MINIMUM, - percentage <= exports.PERCENTAGE_MAXIMUM - ]) +export function isValidPercentage(percentage: number) { + return _.every([ + _.isNumber(percentage), + percentage >= exports.PERCENTAGE_MINIMUM, + percentage <= exports.PERCENTAGE_MAXIMUM, + ]); } /** @@ -73,14 +74,14 @@ exports.isValidPercentage = (percentage) => { * console.log(value); * > 0.5 */ -exports.percentageToFloat = (percentage) => { - if (!exports.isValidPercentage(percentage)) { - throw errors.createError({ - title: `Invalid percentage: ${percentage}` - }) - } +export function percentageToFloat(percentage: number) { + if (!isValidPercentage(percentage)) { + throw errors.createError({ + title: `Invalid percentage: ${percentage}`, + }); + } - return percentage / exports.PERCENTAGE_MAXIMUM + return percentage / PERCENTAGE_MAXIMUM; } /** @@ -109,37 +110,40 @@ exports.percentageToFloat = (percentage) => { * * const memoizedFunction = memoize(getList, angular.equals); */ -exports.memoize = (func, comparer) => { - let previousTuples = [] +export function memoize( + func: (...args: any[]) => any, + comparer: (a: any, b: any) => boolean, +) { + let previousTuples: any[] = []; - return (...restArgs) => { - let areArgsInTuple = false - let state = Reflect.apply(func, this, restArgs) + return (...restArgs: any[]) => { + let areArgsInTuple = false; + let state = Reflect.apply(func, this, restArgs); - previousTuples = _.map(previousTuples, ([ oldArgs, oldState ]) => { - if (comparer(oldArgs, restArgs)) { - areArgsInTuple = true + previousTuples = _.map(previousTuples, ([oldArgs, oldState]) => { + if (comparer(oldArgs, restArgs)) { + areArgsInTuple = true; - if (comparer(state, oldState)) { - // Use the previously memoized state for this argument - state = oldState - } + if (comparer(state, oldState)) { + // Use the previously memoized state for this argument + state = oldState; + } - // Update the tuple state - return [ oldArgs, state ] - } + // Update the tuple state + return [oldArgs, state]; + } - // Return the tuple unchanged - return [ oldArgs, oldState ] - }) + // Return the tuple unchanged + return [oldArgs, oldState]; + }); - // Add the state associated with these args to be memoized - if (!areArgsInTuple) { - previousTuples.push([ restArgs, state ]) - } + // Add the state associated with these args to be memoized + if (!areArgsInTuple) { + previousTuples.push([restArgs, state]); + } - return state - } + return state; + }; } /** @@ -155,20 +159,19 @@ exports.memoize = (func, comparer) => { * @example * const doesIt = hasProps({ foo: 'bar' }, [ 'foo' ]); */ -exports.hasProps = (obj, props) => { - return _.every(props, (prop) => { - return _.has(obj, prop) - }) +export function hasProps(obj: any, props: string[]) { + return _.every(props, prop => { + return _.has(obj, prop); + }); } /** -* @summary Get etcher configs stored online -* @param {String} - url where config.json is stored -*/ -// eslint-disable-next-line -exports.getConfig = (configUrl) => { - return request.getAsync(configUrl, { json: true }) - .get('body') + * @summary Get etcher configs stored online + * @param {String} - url where config.json is stored + */ +export async function getConfig(configUrl: string) { + // @ts-ignore + return (await getAsync(configUrl, { json: true })).body; } /** @@ -186,16 +189,16 @@ exports.getConfig = (configUrl) => { * cleanup() * }); */ -const tmpFileAsync = (options) => { - return new Promise((resolve, reject) => { - tmp.file(options, (error, path, _fd, cleanup) => { - if (error) { - reject(error) - } else { - resolve({ path, cleanup }) - } - }) - }) +function tmpFileAsync(options: tmp.FileOptions) { + return new Promise((resolve, reject) => { + tmp.file(options, (error, path, _fd, cleanup) => { + if (error) { + reject(error); + } else { + resolve({ path, cleanup }); + } + }); + }); } /** @@ -211,9 +214,12 @@ const tmpFileAsync = (options) => { * console.log(path); * }) */ -exports.tmpFileDisposer = (options) => { - return Bluebird.resolve(tmpFileAsync(options)) - .disposer(({ cleanup }) => { - cleanup() - }) +export function tmpFileDisposer(options: tmp.FileOptions) { + return Bluebird.resolve(tmpFileAsync(options)).disposer(({ cleanup }) => { + cleanup(); + }); +} + +export interface Dict { + [key: string]: T; } diff --git a/lib/gui/app/os/dialog.js b/lib/gui/app/os/dialog.js index 1e3ac027..9d7487da 100644 --- a/lib/gui/app/os/dialog.js +++ b/lib/gui/app/os/dialog.js @@ -19,6 +19,7 @@ const _ = require('lodash') const electron = require('electron') const Bluebird = require('bluebird') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../gui/app/modules/errors') const supportedFormats = require('../../../gui/app/modules/supported-formats') diff --git a/lib/gui/app/os/notification.js b/lib/gui/app/os/notification.js index c85c589d..0ed14610 100644 --- a/lib/gui/app/os/notification.js +++ b/lib/gui/app/os/notification.js @@ -17,6 +17,7 @@ 'use strict' const electron = require('electron') +// eslint-disable-next-line node/no-missing-require const settings = require('../models/settings') /** diff --git a/lib/gui/app/os/open-external/services/open-external.js b/lib/gui/app/os/open-external/services/open-external.js index f2527ee2..7328ef75 100644 --- a/lib/gui/app/os/open-external/services/open-external.js +++ b/lib/gui/app/os/open-external/services/open-external.js @@ -19,6 +19,7 @@ const electron = require('electron') const store = require('../../../models/store') const analytics = require('../../../modules/analytics') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../models/settings') module.exports = function () { diff --git a/lib/gui/app/os/window-progress.js b/lib/gui/app/os/window-progress.js index 3de94fdd..cd5da221 100644 --- a/lib/gui/app/os/window-progress.js +++ b/lib/gui/app/os/window-progress.js @@ -17,6 +17,7 @@ 'use strict' const electron = require('electron') +// eslint-disable-next-line node/no-missing-require const utils = require('../../../gui/app/modules/utils') const progressStatus = require('../modules/progress-status') diff --git a/lib/gui/app/os/windows-network-drives.js b/lib/gui/app/os/windows-network-drives.js index 2c2637bb..317e332a 100755 --- a/lib/gui/app/os/windows-network-drives.js +++ b/lib/gui/app/os/windows-network-drives.js @@ -25,6 +25,7 @@ const Path = require('path') const process = require('process') const { promisify } = require('util') +// eslint-disable-next-line node/no-missing-require const { tmpFileDisposer } = require('../../../gui/app/modules/utils') const readFileAsync = promisify(fs.readFile) diff --git a/lib/gui/app/pages/finish/controllers/finish.js b/lib/gui/app/pages/finish/controllers/finish.js index aaed3466..6dd5be42 100644 --- a/lib/gui/app/pages/finish/controllers/finish.js +++ b/lib/gui/app/pages/finish/controllers/finish.js @@ -19,6 +19,7 @@ const _ = require('lodash') const uuidV4 = require('uuid/v4') const store = require('../../../models/store') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../models/settings') const flashState = require('../../../models/flash-state') const selectionState = require('../../../models/selection-state') diff --git a/lib/gui/app/pages/main/controllers/drive-selection.js b/lib/gui/app/pages/main/controllers/drive-selection.js index fd56280d..b73b6838 100644 --- a/lib/gui/app/pages/main/controllers/drive-selection.js +++ b/lib/gui/app/pages/main/controllers/drive-selection.js @@ -20,10 +20,12 @@ const _ = require('lodash') const angular = require('angular') const prettyBytes = require('pretty-bytes') const store = require('../../../models/store') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../models/settings') const selectionState = require('../../../models/selection-state') const analytics = require('../../../modules/analytics') const exceptionReporter = require('../../../modules/exception-reporter') +// eslint-disable-next-line node/no-missing-require const utils = require('../../../../../gui/app/modules/utils') module.exports = function (DriveSelectorService) { diff --git a/lib/gui/app/pages/main/controllers/image-selection.js b/lib/gui/app/pages/main/controllers/image-selection.js index 3a9cfda9..e74c3960 100644 --- a/lib/gui/app/pages/main/controllers/image-selection.js +++ b/lib/gui/app/pages/main/controllers/image-selection.js @@ -23,9 +23,11 @@ const sdk = require('etcher-sdk') const store = require('../../../models/store') const messages = require('../../../../../gui/app/modules/messages') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../../../gui/app/modules/errors') const supportedFormats = require('../../../../../gui/app/modules/supported-formats') const analytics = require('../../../modules/analytics') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../models/settings') const selectionState = require('../../../models/selection-state') const osDialog = require('../../../os/dialog') diff --git a/lib/gui/app/pages/main/controllers/main.js b/lib/gui/app/pages/main/controllers/main.js index c499e3e4..cef642d7 100644 --- a/lib/gui/app/pages/main/controllers/main.js +++ b/lib/gui/app/pages/main/controllers/main.js @@ -18,6 +18,7 @@ const path = require('path') const store = require('../../../models/store') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../models/settings') const flashState = require('../../../models/flash-state') const analytics = require('../../../modules/analytics') diff --git a/lib/gui/app/pages/settings/controllers/settings.js b/lib/gui/app/pages/settings/controllers/settings.js index 62f79685..a6e0376a 100644 --- a/lib/gui/app/pages/settings/controllers/settings.js +++ b/lib/gui/app/pages/settings/controllers/settings.js @@ -19,6 +19,7 @@ const os = require('os') const _ = require('lodash') const store = require('../../../models/store') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../models/settings') const analytics = require('../../../modules/analytics') const exceptionReporter = require('../../../modules/exception-reporter') diff --git a/lib/gui/app/utils/manifest-bind/directives/manifest-bind.js b/lib/gui/app/utils/manifest-bind/directives/manifest-bind.js index 7f6bafaf..4b312ccc 100644 --- a/lib/gui/app/utils/manifest-bind/directives/manifest-bind.js +++ b/lib/gui/app/utils/manifest-bind/directives/manifest-bind.js @@ -16,6 +16,7 @@ 'use strict' +// eslint-disable-next-line node/no-missing-require const errors = require('../../../../../gui/app/modules/errors') /** diff --git a/lib/gui/etcher.js b/lib/gui/etcher.js index 9bf1a830..db834227 100644 --- a/lib/gui/etcher.js +++ b/lib/gui/etcher.js @@ -23,8 +23,10 @@ const { autoUpdater } = require('electron-updater') const Bluebird = require('bluebird') const EXIT_CODES = require('./app/modules/exit-codes') const buildWindowMenu = require('./menu') +// eslint-disable-next-line node/no-missing-require const settings = require('./app/models/settings') const analytics = require('./app/modules/analytics') +// eslint-disable-next-line node/no-missing-require const { getConfig } = require('./app/modules/utils') /* eslint-disable lodash/prefer-lodash-method */ diff --git a/lib/start.js b/lib/start.js index ae25ea21..20fea9e4 100644 --- a/lib/start.js +++ b/lib/start.js @@ -27,7 +27,7 @@ // an older equivalent of `ELECTRON_RUN_AS_NODE` that still gets set when // using `child_process.fork()`. if (process.env.ELECTRON_RUN_AS_NODE || process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE) { - require('./gui/app/modules/child-writer') + require('../generated/child-writer') } else { require('../generated/etcher') } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index bc7881dd..5e3c1a0a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -980,6 +980,12 @@ "integrity": "sha512-6BmYWSBea18+tSjjSC3QIyV93ZKAeNWGM7R6aYt1ryTZXrlHF+QLV0G2yV0viEGVyRkyQsWfMoJ0k/YghBX5sQ==", "dev": true }, + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", + "dev": true + }, "@types/color": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/color/-/color-2.0.1.tgz", @@ -1001,6 +1007,12 @@ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, + "@types/debug": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.4.tgz", + "integrity": "sha512-D9MyoQFI7iP5VdpEyPZyjjqIJ8Y8EDNQFIFVLOmeg1rI1xiHOChyUPMPRUVfqFCerxfE+yS3vMyj37F6IdtOoQ==", + "dev": true + }, "@types/depcheck": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/depcheck/-/depcheck-0.6.0.tgz", @@ -1013,6 +1025,15 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, + "@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/glob": { "version": "5.0.36", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.36.tgz", @@ -1127,6 +1148,18 @@ "@types/react": "*" } }, + "@types/request": { + "version": "2.48.1", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", + "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", + "dev": true, + "requires": { + "@types/caseless": "*", + "@types/form-data": "*", + "@types/node": "*", + "@types/tough-cookie": "*" + } + }, "@types/styled-components": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-4.1.8.tgz", @@ -1145,6 +1178,18 @@ "csstype": "^2.6.4" } }, + "@types/tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA==", + "dev": true + }, + "@types/tough-cookie": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", + "dev": true + }, "@types/usb": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@types/usb/-/usb-1.5.1.tgz", @@ -1674,6 +1719,12 @@ "readable-stream": "^2.0.6" } }, + "arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -4504,9 +4555,9 @@ }, "dependencies": { "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", "dev": true }, "fs-extra": { @@ -7823,6 +7874,12 @@ } } }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, "mamacro": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", @@ -12678,6 +12735,27 @@ } } }, + "ts-node": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz", + "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + } + } + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -14300,6 +14378,12 @@ "fd-slicer": "~1.1.0" } }, + "yn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz", + "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==", + "dev": true + }, "zip-stream": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", diff --git a/package.json b/package.json index 58ebb495..5631b114 100644 --- a/package.json +++ b/package.json @@ -86,8 +86,11 @@ "@babel/plugin-proposal-function-bind": "^7.2.0", "@babel/preset-env": "^7.2.0", "@babel/preset-react": "^7.0.0", + "@types/debug": "^4.1.4", "@types/node": "^10.14.9", "@types/react-dom": "^16.8.4", + "@types/request": "^2.48.1", + "@types/tmp": "^0.1.0", "acorn": "^6.0.5", "angular-mocks": "1.7.6", "babel-loader": "^8.0.4", @@ -119,6 +122,7 @@ "spectron": "^5.0.0", "style-loader": "^0.23.1", "ts-loader": "^6.0.2", + "ts-node": "^8.3.0", "typescript": "^3.5.1", "webpack": "^4.31.0", "webpack-cli": "^3.1.2", diff --git a/tests/gui/components/drive-selector.spec.js b/tests/gui/components/drive-selector.spec.js index 11144a43..4055cb0b 100644 --- a/tests/gui/components/drive-selector.spec.js +++ b/tests/gui/components/drive-selector.spec.js @@ -20,6 +20,7 @@ const _ = require('lodash') const m = require('mochainon') const angular = require('angular') require('angular-mocks') +// eslint-disable-next-line node/no-missing-require const utils = require('../../../lib/gui/app/modules/utils') describe('Browser: DriveSelector', function () { diff --git a/tests/gui/models/settings.spec.js b/tests/gui/models/settings.spec.js index bbc672bd..93a61ee9 100644 --- a/tests/gui/models/settings.spec.js +++ b/tests/gui/models/settings.spec.js @@ -18,8 +18,9 @@ const m = require('mochainon') const _ = require('lodash') -const Bluebird = require('bluebird') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../lib/gui/app/models/settings') +// eslint-disable-next-line node/no-missing-require const localSettings = require('../../../lib/gui/app/models/local-settings') describe('Browser: settings', function () { @@ -73,12 +74,14 @@ describe('Browser: settings', function () { }) describe('.assign()', function () { - it('should throw if no settings', function (done) { - settings.assign().asCallback((error) => { + it('should throw if no settings', async () => { + try { + await settings.assign() + m.chai.expect(true).to.be.false + } catch (error) { m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('Missing settings') - done() - }) + m.chai.expect(error.message).to.equal('Settings must be an object') + } }) it('should not override all settings', function () { @@ -108,23 +111,22 @@ describe('Browser: settings', function () { }) }) - it('should not change the application state if storing to the local machine results in an error', function (done) { - settings.set('foo', 'bar').then(() => { - m.chai.expect(settings.get('foo')).to.equal('bar') + it('should not change the application state if storing to the local machine results in an error', async () => { + await settings.set('foo', 'bar') + m.chai.expect(settings.get('foo')).to.equal('bar') - const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') - localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error'))) + const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') + localSettingsWriteAllStub.returns(Promise.reject(new Error('localSettings error'))) - settings.assign({ - foo: 'baz' - }).asCallback((error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('localSettings error') - localSettingsWriteAllStub.restore() - m.chai.expect(settings.get('foo')).to.equal('bar') - done() - }) - }).catch(done) + try { + await settings.assign({ foo: 'baz' }) + m.chai.expect(true).to.be.false + } catch (error) { + m.chai.expect(error).to.be.an.instanceof(Error) + m.chai.expect(error.message).to.equal('localSettings error') + } + localSettingsWriteAllStub.restore() + m.chai.expect(settings.get('foo')).to.equal('bar') }) }) @@ -160,28 +162,34 @@ describe('Browser: settings', function () { }) }) - it('should reject if no key', function (done) { - settings.set(null, true).asCallback((error) => { + it('should reject if no key', async () => { + try { + await settings.set(null, true) + m.chai.expect(true).to.be.false + } catch (error) { m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('Missing setting key') - done() - }) + m.chai.expect(error.message).to.equal('Invalid setting key: null') + } }) - it('should throw if key is not a string', function (done) { - settings.set(1234, true).asCallback((error) => { + it('should throw if key is not a string', async () => { + try { + await settings.set(1234, true) + m.chai.expect(true).to.be.false + } catch (error) { m.chai.expect(error).to.be.an.instanceof(Error) m.chai.expect(error.message).to.equal('Invalid setting key: 1234') - done() - }) + } }) - it('should throw if setting an array', function (done) { - settings.assign([ 1, 2, 3 ]).asCallback((error) => { + it('should throw if setting an array', async () => { + try { + await settings.assign([ 1, 2, 3 ]) + m.chai.expect(true).to.be.false + } catch (error) { m.chai.expect(error).to.be.an.instanceof(Error) m.chai.expect(error.message).to.equal('Settings must be an object') - done() - }) + } }) it('should set the key to undefined if no value', function () { @@ -202,21 +210,22 @@ describe('Browser: settings', function () { }) }) - it('should not change the application state if storing to the local machine results in an error', function (done) { - settings.set('foo', 'bar').then(() => { + it('should not change the application state if storing to the local machine results in an error', async () => { + await settings.set('foo', 'bar') + m.chai.expect(settings.get('foo')).to.equal('bar') + + const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') + localSettingsWriteAllStub.returns(Promise.reject(new Error('localSettings error'))) + + try { + await settings.set('foo', 'baz') + m.chai.expect(true).to.be.false + } catch (error) { + m.chai.expect(error).to.be.an.instanceof(Error) + m.chai.expect(error.message).to.equal('localSettings error') + localSettingsWriteAllStub.restore() m.chai.expect(settings.get('foo')).to.equal('bar') - - const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') - localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error'))) - - settings.set('foo', 'baz').asCallback((error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('localSettings error') - localSettingsWriteAllStub.restore() - m.chai.expect(settings.get('foo')).to.equal('bar') - done() - }) - }).catch(done) + } }) }) diff --git a/tests/gui/modules/progress-status.spec.js b/tests/gui/modules/progress-status.spec.js index 3eb38e69..1671e017 100644 --- a/tests/gui/modules/progress-status.spec.js +++ b/tests/gui/modules/progress-status.spec.js @@ -1,6 +1,7 @@ 'use strict' const m = require('mochainon') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../lib/gui/app/models/settings') const progressStatus = require('../../../lib/gui/app/modules/progress-status') diff --git a/tests/shared/errors.spec.js b/tests/shared/errors.spec.js index 0d39ab0a..b64676f2 100644 --- a/tests/shared/errors.spec.js +++ b/tests/shared/errors.spec.js @@ -18,6 +18,7 @@ const m = require('mochainon') const _ = require('lodash') +// eslint-disable-next-line node/no-missing-require const errors = require('../../lib/gui/app/modules/errors') describe('Shared: Errors', function () { @@ -64,16 +65,6 @@ describe('Shared: Errors', function () { m.chai.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 - m.chai.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 - m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred') - }) - it('should return the error message', function () { const error = new Error('This is an error') m.chai.expect(errors.getTitle(error)).to.equal('This is an error') @@ -325,54 +316,21 @@ describe('Shared: Errors', function () { m.chai.expect(errors.getDescription(error)).to.equal('Memory error') }) - describe('given userFriendlyDescriptionsOnly is false', function () { - it('should return the stack for a basic error', function () { - const error = new Error('Foo') - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: false - })).to.equal(error.stack) - }) - - it('should return the stack if the description is an empty string', function () { - const error = new Error('Foo') - error.description = '' - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: false - })).to.equal(error.stack) - }) - - it('should return the stack if the description is a blank string', function () { - const error = new Error('Foo') - error.description = ' ' - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: false - })).to.equal(error.stack) - }) + it('should return the stack for a basic error', function () { + const error = new Error('Foo') + m.chai.expect(errors.getDescription(error)).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') - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: true - })).to.equal('') - }) + it('should return the stack if the description is an empty string', function () { + const error = new Error('Foo') + error.description = '' + m.chai.expect(errors.getDescription(error)).to.equal(error.stack) + }) - it('should return an empty string if the description is an empty string', function () { - const error = new Error('Foo') - error.description = '' - m.chai.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') - error.description = ' ' - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: true - })).to.equal('') - }) + it('should return the stack if the description is a blank string', function () { + const error = new Error('Foo') + error.description = ' ' + m.chai.expect(errors.getDescription(error)).to.equal(error.stack) }) }) diff --git a/tests/shared/utils.spec.js b/tests/shared/utils.spec.js index e298ecba..47a40888 100644 --- a/tests/shared/utils.spec.js +++ b/tests/shared/utils.spec.js @@ -18,6 +18,7 @@ const _ = require('lodash') const m = require('mochainon') +// eslint-disable-next-line node/no-missing-require const utils = require('../../lib/gui/app/modules/utils') describe('Shared: Utils', function () { diff --git a/tsconfig.json b/tsconfig.json index d99d78fd..ec9878a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,8 +4,10 @@ "noUnusedLocals": true, "noUnusedParameters": true, "strictNullChecks": true, + "resolveJsonModule": true, + "allowJs": true, "moduleResolution": "node", - "module": "esNext", + "module": "commonjs", "target": "es2017", "jsx": "react" }, diff --git a/webpack.config.js b/webpack.config.js index a9d00e6d..0f7f1619 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -138,7 +138,22 @@ const etcherConfig = _.assign( } ) +const childWriterConfig = _.assign( + {}, + etcherConfig, + { + entry: { + etcher: path.join(__dirname, 'lib', 'gui', 'app', 'modules', 'child-writer.js') + }, + output: { + path: path.join(__dirname, 'generated'), + filename: 'child-writer.js' + } + } +) + module.exports = [ guiConfig, - etcherConfig + etcherConfig, + childWriterConfig ]