Compare commits

...

16 Commits

Author SHA1 Message Date
Alexis Svinartchouk
868a35337c wip 2019-07-16 13:17:46 +02:00
Alexis Svinartchouk
1398ca2931 wip 2019-07-12 18:59:06 +02:00
Alexis Svinartchouk
96c865f14a wip 2019-07-10 18:44:30 +02:00
Alexis Svinartchouk
6dbd425e89 wip 2019-07-04 19:35:52 +02:00
Alexis Svinartchouk
5b2769d0e9 Sleep button on main screen for etcher-pro
Change-type: patch
2019-07-01 13:10:46 +02:00
Alexis Svinartchouk
5f38cca60c Led module prototype
Change-type: patch
2019-07-01 13:10:46 +02:00
Alexis Svinartchouk
78cebdb7a4 Change setting enableScreensaver -> screensaverDelay
Change-type: patch
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
a6aedab0a0 Add / remove listeners and timeouts when the sceensaver setting changes
Change-type: patch
Changelog-entry: Add / remove listeners and timeouts when the sceensaver setting changes
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
4aeccbe963 Don't use settings.getDefaults() in etcher.js
Change-type: patch
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
126b3fbb40 Indent typescript files with tabs
Change-type: patch
Changelog-entry: Indent typescript files with tabs
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
9ea8a6134e Remove unused settings.assign function
Change-type: patch
Changelog-entry: Remove unused settings.assign function
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
3706770322 Convert utils, settings and errors to typescript
Change-type: patch
Changelog-entry: Convert utils, settings and errors to typescript
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
7be07bfe8c Screensaver for etcher-pro
Change-type: patch
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
791c047fa1 Simplify imports
Change-type: patch
2019-06-28 15:05:30 +02:00
Lorenzo Alberto Maria Ambrosi
35ad0340b9 Move shared folder to gui/app/modules
Change-type: patch
Changelog-entry: Move shared folder to gui/app/modules
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-28 15:05:30 +02:00
Alexis Svinartchouk
0a0be3a13d Allow typescript files
Change-type: patch
Changelog-entry: Allow typescript files
2019-06-28 15:05:30 +02:00
82 changed files with 3018 additions and 1402 deletions

View File

@@ -14,3 +14,6 @@ trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[*.ts]
indent_style = tab

1
.gitattributes vendored
View File

@@ -1,4 +1,5 @@
# Javascript files must retain LF line-endings (to keep eslint happy)
*.ts text eol=lf
*.js text eol=lf
*.jsx text eol=lf
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)

View File

@@ -150,6 +150,9 @@ sass:
npm rebuild node-sass
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
lint-ts:
resin-lint --typescript lib
lint-js:
eslint --ignore-pattern scripts/resin/**/*.js lib tests scripts bin webpack.config.js
@@ -169,9 +172,9 @@ lint-spell:
--skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
lib tests docs scripts Makefile *.md LICENSE
lint: lint-js lint-sass lint-cpp lint-html 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"))')

View File

@@ -62,7 +62,7 @@ since fresh eyes could help unveil things that we take for granted, but should
be documented instead!
[lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/shared/exit-codes.js
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/gui/app/modules/exit-codes.js
[gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui
[electron]: http://electron.atom.io
[nodejs]: https://nodejs.org

View File

@@ -31,11 +31,12 @@ const sdk = require('etcher-sdk')
const _ = require('lodash')
const uuidV4 = require('uuid/v4')
const EXIT_CODES = require('../../shared/exit-codes')
const messages = require('../../shared/messages')
const EXIT_CODES = require('../../gui/app/modules/exit-codes')
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,8 @@ 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 */
@@ -453,6 +456,32 @@ app.controller('HeaderController', function (OSOpenExternalService) {
this.shouldShowHelp = () => {
return !settings.get('disableExternalLinks')
}
/**
* @summary Whether to show the sleep button
* @function
* @public
*
* @returns {Boolean}
*
* @example
* HeaderController.shouldShowSleep()
*/
this.shouldShowSleep = () => {
return settings.get('showScreensaverDelay')
}
/**
* @summary Enables the screensaver
* @function
* @public
*
* @example
* HeaderController.sleep()
*/
this.sleep = () => {
screensaver.off()
}
})
app.controller('StateController', function ($rootScope, $scope) {
@@ -506,3 +535,5 @@ angular.element(document).ready(() => {
angular.bootstrap(document, [ 'Etcher' ])
}).catch(exceptionReporter.report)
})
screensaver.init()

View File

@@ -19,12 +19,13 @@
const angular = require('angular')
const _ = require('lodash')
const Bluebird = require('bluebird')
const constraints = require('../../../../../shared/drive-constraints')
const constraints = require('../../../../../gui/app/modules/drive-constraints')
const store = require('../../../models/store')
const analytics = require('../../../modules/analytics')
const availableDrives = require('../../../models/available-drives')
const selectionState = require('../../../models/selection-state')
const utils = require('../../../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../../../gui/app/modules/utils')
module.exports = function (
$q,

View File

@@ -14,21 +14,19 @@
* limitations under the License.
*/
'use strict'
/**
* @module Etcher.Components.TargetSelector
*/
const angular = require('angular')
const { react2angular } = require('react2angular')
import * as angular from 'angular';
import { react2angular } from 'react2angular';
const MODULE_NAME = 'Etcher.Components.TargetSelector'
const SelectTargetButton = angular.module(MODULE_NAME, [])
const MODULE_NAME = 'Etcher.Components.TargetSelector';
const SelectTargetButton = angular.module(MODULE_NAME, []);
SelectTargetButton.component(
'targetSelector',
react2angular(require('./target-selector.jsx'))
)
'targetSelector',
react2angular(require('./target-selector.jsx')),
);
module.exports = MODULE_NAME
export = MODULE_NAME;

View File

@@ -31,7 +31,7 @@ const {
} = require('./../../styled-components')
const { Txt } = require('rendition')
const middleEllipsis = require('./../../utils/middle-ellipsis')
const { bytesToClosestUnit } = require('./../../../../shared/units')
const { bytesToClosestUnit } = require('./../../../../gui/app/modules/units')
const TargetDetail = styled((props) => (
<Txt.span {...props}>

View File

@@ -0,0 +1,264 @@
/*
* Copyright 2019 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 { Meter } from 'grommet';
import { sortBy } from 'lodash';
import * as React from 'react';
import { Badge, Modal, Table } from 'rendition';
import { getDrives } from '../../models/available-drives';
import { COMPATIBILITY_STATUS_TYPES } from '../../modules/drive-constraints';
import { subscribe } from '../../models/store';
import { ThemedProvider } from '../../styled-components';
import { bytesToClosestUnit } from '../../modules/units';
interface Drive {
description: string;
device: string;
isSystem: boolean;
isReadOnly: boolean;
progress?: number;
size?: number;
link?: string;
linkCTA?: string;
displayName: string;
}
interface CompatibilityStatus {
type: number;
message: string;
}
interface DriveSelectorProps {
title: string;
close: () => void;
setSelectedDrives: (drives: Drive[]) => void;
isDriveSelected: (drive: Drive) => boolean;
isDriveValid: (drive: Drive) => boolean;
getDriveBadges: (drive: Drive) => CompatibilityStatus[];
}
interface DriveSelectorState {
drives: Drive[];
selected: Drive[];
disabledDrives: string[];
}
const modalStyle = {
width: '800px',
height: '600px',
paddingTop: '20px',
paddingLeft: '30px',
paddingRight: '30px',
paddingBottom: '11px',
};
const titleStyle = {
color: '#2a506f',
};
const subtitleStyle = {
marginLeft: '10px',
fontSize: '11px',
color: '#5b82a7',
};
const wrapperStyle = {
height: '250px',
overflowX: 'hidden' as 'hidden',
overflowY: 'auto' as 'auto',
};
export class DriveSelector2 extends React.Component<
DriveSelectorProps,
DriveSelectorState
> {
private table: Table<Drive> | null = null;
private columns: {
field: keyof Drive;
label: string;
render?: (value: any, row: Drive) => string | number | JSX.Element | null;
}[];
private unsubscribe?: () => void;
constructor(props: DriveSelectorProps) {
super(props);
this.columns = [
{
field: 'description',
label: 'Name',
} as const,
{
field: 'size',
label: 'Size',
render: this.renderSize.bind(this),
} as const,
{
field: 'displayName',
label: 'Location',
render: this.renderLocation.bind(this),
} as const,
{
field: 'isReadOnly', // We don't use this, but a valid field that is not used in another column is required
label: ' ',
render: this.renderBadges.bind(this),
} as const,
];
this.state = this.getNewState();
}
public componentDidMount() {
this.update();
if (this.unsubscribe === undefined) {
this.unsubscribe = subscribe(this.update.bind(this));
}
}
public componentWillUnmount() {
if (this.unsubscribe !== undefined) {
this.unsubscribe();
this.unsubscribe = undefined;
}
}
private getNewState() {
let drives: Drive[] = getDrives();
for (let i = 0; i < drives.length; i++) {
drives[i] = { ...drives[i] };
}
drives = sortBy(drives, 'device');
const selected = drives.filter(d => this.props.isDriveSelected(d));
const disabledDrives = drives
.filter(d => !this.props.isDriveValid(d))
.map(d => d.device);
return { drives, disabledDrives, selected };
}
private update() {
this.setState(this.getNewState());
this.updateTableSelection();
}
private updateTableSelection() {
if (this.table !== null) {
this.table.setRowSelection(this.state.selected);
}
}
private renderSize(size: number) {
if (size) {
return bytesToClosestUnit(size);
} else {
return null;
}
}
private renderLocation(displayName: string, drive: Drive) {
const result: Array<string | JSX.Element> = [displayName];
if (drive.link && drive.linkCTA) {
result.push(<a href={drive.link}>{drive.linkCTA}</a>);
}
return <React.Fragment>{result}</React.Fragment>;
}
private renderBadges(_value: any, row: Drive) {
const result = [];
if (row.progress !== undefined) {
result.push(
<Meter
size="small"
thickness="xxsmall"
values={[
{
value: row.progress,
label: row.progress + '%',
color: '#2297de',
},
]}
/>,
);
}
result.push(
...this.props.getDriveBadges(row).map((status: CompatibilityStatus) => {
const props: {
key: string;
xsmall: true;
danger?: boolean;
warning?: boolean;
} = { xsmall: true, key: status.message };
if (status.type === COMPATIBILITY_STATUS_TYPES.ERROR) {
props.danger = true;
} else if (status.type === COMPATIBILITY_STATUS_TYPES.WARNING) {
props.warning = true;
}
return <Badge {...props}>{status.message}</Badge>;
}),
);
return <React.Fragment>{result}</React.Fragment>;
}
private renderTbodyPrefix() {
if (this.state.drives.length === 0) {
return (
<tr>
<td colSpan={this.columns.length} style={{ textAlign: 'center' }}>
<b>Connect a drive</b>
<div>No removable drive detected.</div>
</td>
</tr>
);
}
}
public render() {
return (
<ThemedProvider>
<Modal
titleElement={
<div style={titleStyle}>
{this.props.title}
<span style={subtitleStyle}>
{this.state.drives.length} found
</span>
</div>
}
action={`Select (${this.state.selected.length})`}
style={modalStyle}
done={this.props.close}
>
<div style={wrapperStyle}>
<Table<Drive>
ref={t => {
this.table = t;
this.updateTableSelection();
}}
rowKey="device"
onCheck={this.onCheck.bind(this)}
columns={this.columns}
data={this.state.drives}
disabledRows={this.state.disabledDrives}
tbodyPrefix={this.renderTbodyPrefix()}
/>
</div>
</Modal>
</ThemedProvider>
);
}
private onCheck(checkedDrives: Drive[]): void {
this.props.setSelectedDrives(checkedDrives);
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2019 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 angular from 'angular';
import { react2angular } from 'react2angular';
import { DriveSelector2 } from './drive-selector.tsx';
const MODULE_NAME = 'Etcher.Components.DriveSelector2';
angular
.module(MODULE_NAME, [])
.component(
'driveSelector2',
react2angular(DriveSelector2, [
'close',
'getDriveBadges',
'isDriveSelected',
'isDriveValid',
'setSelectedDrives',
'title',
]),
);
export = MODULE_NAME;

View File

@@ -18,8 +18,10 @@
const _ = require('lodash')
const os = require('os')
// eslint-disable-next-line node/no-missing-require
const settings = require('../../../models/settings')
const utils = require('../../../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../../../gui/app/modules/utils')
const angular = require('angular')
/* eslint-disable lodash/prefer-lodash-method */

View File

@@ -25,7 +25,7 @@ const colors = require('./colors')
const prettyBytes = require('pretty-bytes')
const files = require('../../../models/files')
const middleEllipsis = require('../../../utils/middle-ellipsis')
const supportedFormats = require('../../../../../shared/supported-formats')
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
const debug = require('debug')('etcher:gui:file-selector')

View File

@@ -35,9 +35,9 @@ const selectionState = require('../../../models/selection-state')
const store = require('../../../models/store')
const osDialog = require('../../../os/dialog')
const exceptionReporter = require('../../../modules/exception-reporter')
const messages = require('../../../../../shared/messages')
const errors = require('../../../../../shared/errors')
const supportedFormats = require('../../../../../shared/supported-formats')
const messages = require('../../../../../gui/app/modules/messages')
const errors = require('../../../../../gui/app/modules/errors')
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
const analytics = require('../../../modules/analytics')
const debug = require('debug')('etcher:gui:file-selector')

View File

@@ -19,10 +19,12 @@
/* eslint-disable no-unused-vars */
const React = require('react')
const propTypes = require('prop-types')
const { Badge, DropDownButton, Select } = require('rendition')
const { default: styled } = require('styled-components')
const middleEllipsis = require('./../../utils/middle-ellipsis')
const shared = require('./../../../../shared/units')
const shared = require('./../../../../gui/app/modules/units')
const {
StepButton,
StepNameButton,
@@ -34,6 +36,17 @@ const {
ThemedProvider
} = require('./../../styled-components')
const DropdownItem = styled.p`
padding-top: 10px;
text-align: left;
width: 150px;
cursor: pointer;
`
const DropdownItemIcon = styled.i`
padding-right: 10px;
`
const SelectImageButton = (props) => {
if (props.hasImage) {
return (
@@ -50,9 +63,9 @@ const SelectImageButton = (props) => {
<ChangeButton
plain
mb={14}
onClick={props.reselectImage}
onClick={props.deselectImage}
>
Change
Remove
</ChangeButton>
}
<DetailsText>
@@ -64,11 +77,26 @@ const SelectImageButton = (props) => {
return (
<ThemedProvider>
<StepSelection>
<StepButton
onClick={props.openImageSelector}
<DropDownButton
primary
label={
<div onClick={props.openImageSelector}>Select image</div>
}
style={{height: '48px'}}
>
Select image
</StepButton>
<DropdownItem
onClick={props.openImageSelector}
>
<DropdownItemIcon className="far fa-file"/>
Select image file
</DropdownItem>
<DropdownItem
onClick={props.openDriveSelector}
>
<DropdownItemIcon className="far fa-copy"/>
Duplicate drive
</DropdownItem>
</DropDownButton>
<Footer>
{ props.mainSupportedExtensions.join(', ') }, and{' '}
<Underline
@@ -84,15 +112,17 @@ const SelectImageButton = (props) => {
SelectImageButton.propTypes = {
openImageSelector: propTypes.func,
openDriveSelector: propTypes.func,
mainSupportedExtensions: propTypes.array,
extraSupportedExtensions: propTypes.array,
hasImage: propTypes.bool,
showSelectedImageDetails: propTypes.func,
imageName: propTypes.string,
imageBasename: propTypes.string,
reselectImage: propTypes.func,
deselectImage: propTypes.func,
flashing: propTypes.bool,
imageSize: propTypes.number
imageSize: propTypes.number,
sourceType: propTypes.string
}
module.exports = SelectImageButton

View File

@@ -11,22 +11,36 @@
</head>
<body>
<header class="section-header" ng-controller="HeaderController as header">
<button
class="button button-link sleep-button"
tabindex="4"
ng-if="header.shouldShowSleep()"
ng-click="header.sleep()"
>
<svg-icon paths="[ '../../assets/moon.svg' ]"
width="'14px'"
height="'14px'"></svg-icon>
<span >
Sleep
</span>
</button>
<button class="button button-link"
ng-if="header.shouldShowHelp()"
ng-click="header.openHelpPage()"
tabindex="4">
tabindex="5">
<span class="glyphicon glyphicon-question-sign"></span>
</button>
<button class="button button-link"
ui-sref="settings"
hide-if-state="settings"
tabindex="5">
tabindex="6">
<span class="glyphicon glyphicon-cog"></span>
</button>
<button class="button button-link"
tabindex="5"
tabindex="7"
ui-sref="main"
show-if-state="settings">
<span class="glyphicon glyphicon-chevron-left"></span> Back

View File

@@ -18,7 +18,7 @@
const _ = require('lodash')
const store = require('./store')
const units = require('../../../shared/units')
const units = require('../../../gui/app/modules/units')
/**
* @summary Reset flash state

View File

@@ -0,0 +1,70 @@
import { delay } from 'bluebird';
import { Gpio } from 'pigpio';
class Led {
private gpio: Gpio;
constructor(gpioNumber: number) {
this.gpio = new Gpio(gpioNumber, { mode: Gpio.OUTPUT });
}
public set intensity(value: number) {
// TODO: check that 0 <= value <= 1
this.gpio.pwmWrite(Math.round(value * 255));
}
}
export type Color = [number, number, number];
export type AnimationFunction = (t: number) => Color;
export class RGBLed {
private leds: [Led, Led, Led];
private currentAnimation?: AnimationFunction;
private static animations: Map<string, AnimationFunction> = new Map();
constructor(gpioNumbers: [number, number, number], public frequency = 60) {
this.leds = gpioNumbers.map(n => new Led(n)) as [Led, Led, Led];
}
private async loop() {
while (this.currentAnimation !== undefined) {
this.$setColor(...this.currentAnimation(new Date().getTime()));
await delay(1000 / this.frequency);
}
}
private $setColor(red: number, green: number, blue: number) {
this.leds[0].intensity = red;
this.leds[1].intensity = green;
this.leds[2].intensity = blue;
}
public setColor(red: number, green: number, blue: number) {
// stop any running animation
this.setAnimation();
this.$setColor(red, green, blue);
}
public static registerAnimation(name: string, animation: AnimationFunction) {
RGBLed.animations.set(name, animation);
}
public setAnimation(name?: string) {
const hadAnimation = this.currentAnimation !== undefined;
this.currentAnimation = name ? RGBLed.animations.get(name) : undefined;
// Don't launch the loop a second time
if (!hadAnimation) {
this.loop();
}
}
}
RGBLed.registerAnimation('breathe-white', (t: number) => {
const intensity = Math.sin(t / 1000);
return [intensity, intensity, intensity];
});
RGBLed.registerAnimation('blink-white', (t: number) => {
const intensity = Math.floor(t / 1000) % 2;
return [intensity, intensity, intensity];
});

View File

@@ -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 { inspect, 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,15 @@ 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<any> {
try {
return JSON.parse(await readFileAsync(filename, { encoding: 'utf8' }));
} catch (error) {
console.error(
`Failed to load settings from ${filename}: ${inspect(error)}`,
);
return {};
}
}
/**
@@ -106,17 +93,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 +112,8 @@ const writeConfigFile = (filename, data) => {
* console.log(settings);
* });
*/
exports.readAll = () => {
return readConfigFile(CONFIG_PATH)
export async function readAll(): Promise<any> {
return await readConfigFile(CONFIG_PATH);
}
/**
@@ -152,8 +132,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 +151,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<void> {
await unlinkAsync(CONFIG_PATH);
}

View File

@@ -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('../../../shared/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)
}

View File

@@ -0,0 +1,103 @@
/*
* 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 { EventEmitter } from 'events';
import { cloneDeep } 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<any> = cloneDeep(DEFAULT_SETTINGS);
export const events = new EventEmitter();
// Exported for tests only, don't use that
export async function reset(): Promise<void> {
debug('reset');
settings = cloneDeep(DEFAULT_SETTINGS);
await writeAll(settings);
}
export async function load(): Promise<any> {
debug('load');
const loadedSettings = await readAll();
const oldSettings = cloneDeep(settings);
settings = { ...settings, ...loadedSettings };
for (const key of Object.keys(settings)) {
const value = settings[key];
if (!oldSettings.hasOwnProperty(key) || value !== oldSettings[key]) {
events.emit(key, value);
}
}
return settings;
}
export async function set(key: string, value: any): Promise<void> {
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;
}
if (value !== previousValue) {
events.emit(key, value);
}
}
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);
}

View File

@@ -20,11 +20,14 @@ const Immutable = require('immutable')
const _ = require('lodash')
const redux = require('redux')
const uuidV4 = require('uuid/v4')
const constraints = require('../../../shared/drive-constraints')
const supportedFormats = require('../../../shared/supported-formats')
const errors = require('../../../shared/errors')
const fileExtensions = require('../../../shared/file-extensions')
const utils = require('../../../shared/utils')
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')
/**
@@ -65,10 +68,7 @@ const flashStateNoNilFields = [
* @constant
* @private
*/
const selectImageNoNilFields = [
'path',
'extension'
]
const selectImageNoNilFields = [ 'path' ]
/**
* @summary Application default state
@@ -382,42 +382,44 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
})
}
if (!_.isString(action.data.extension)) {
throw errors.createError({
title: `Invalid image extension: ${action.data.extension}`
})
}
const extension = _.toLower(action.data.extension)
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
throw errors.createError({
title: `Invalid image extension: ${action.data.extension}`
})
}
let lastImageExtension = fileExtensions.getLastFileExtension(action.data.path)
lastImageExtension = _.isString(lastImageExtension) ? _.toLower(lastImageExtension) : lastImageExtension
if (lastImageExtension !== extension) {
if (!_.isString(action.data.archiveExtension)) {
if (!action.data.isDrive) { // We don't care about extensions if the source is a drive
if (!_.isString(action.data.extension)) {
throw errors.createError({
title: 'Missing image archive extension'
title: `Invalid image extension: ${action.data.extension}`
})
}
const archiveExtension = _.toLower(action.data.archiveExtension)
const extension = _.toLower(action.data.extension)
if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) {
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
throw errors.createError({
title: `Invalid image archive extension: ${action.data.archiveExtension}`
title: `Invalid image extension: ${action.data.extension}`
})
}
if (lastImageExtension !== archiveExtension) {
throw errors.createError({
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`
})
let lastImageExtension = fileExtensions.getLastFileExtension(action.data.path)
lastImageExtension = _.isString(lastImageExtension) ? _.toLower(lastImageExtension) : lastImageExtension
if (lastImageExtension !== extension) {
if (!_.isString(action.data.archiveExtension)) {
throw errors.createError({
title: 'Missing image archive extension'
})
}
const archiveExtension = _.toLower(action.data.archiveExtension)
if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) {
throw errors.createError({
title: `Invalid image archive extension: ${action.data.archiveExtension}`
})
}
if (lastImageExtension !== archiveExtension) {
throw errors.createError({
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`
})
}
}
}

View File

@@ -19,8 +19,10 @@
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')
const { getConfig, hasProps } = require('../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const { getConfig, hasProps } = require('../../../gui/app/modules/utils')
const sentryToken = settings.get('analyticsSentryToken') ||
_.get(packageJSON, [ 'analytics', 'sentry', 'token' ])

View File

@@ -20,8 +20,9 @@ const Bluebird = require('bluebird')
const _ = require('lodash')
const ipc = require('node-ipc')
const sdk = require('etcher-sdk')
const EXIT_CODES = require('../../shared/exit-codes')
const errors = require('../../shared/errors')
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
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT

View File

@@ -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')
/**

View File

@@ -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<any>): 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<any>): Error {
return assign(new Error(json.message), json);
}

View File

@@ -24,10 +24,12 @@ 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')
const errors = require('../../../shared/errors')
const permissions = require('../../../shared/permissions')
// 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')
const analytics = require('../modules/analytics')
const updateLock = require('./update-lock')

View File

@@ -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)

View File

@@ -16,9 +16,11 @@
'use strict'
// eslint-disable-next-line node/no-missing-require
const settings = require('../models/settings')
const utils = require('../../../shared/utils')
const units = require('../../../shared/units')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../gui/app/modules/utils')
const units = require('../../../gui/app/modules/units')
/**
* @summary Make the progress status subtitle string

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2019 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 { execFile } from 'child_process';
import { promisify } from 'util';
import * as settings from '../models/settings';
const execFileAsync = promisify(execFile);
const EVENT_TYPES = [
'focus',
'keydown',
'keyup',
'pointerdown',
'pointermove',
'pointerup',
] as const;
function exec(
command: string,
...args: string[]
): Promise<{ stdout: string; stderr: string }> {
return execFileAsync(command, args);
}
async function screenOff(): Promise<void> {
await exec('xset', 'dpms', 'force', 'suspend');
}
async function ledsOn(): Promise<void> {
// TODO
}
async function ledsOff(): Promise<void> {
// TODO
}
export async function off() {
await Promise.all([ledsOff(), screenOff()]);
}
let timeout: NodeJS.Timeout;
let delay: number | null = null;
async function listener() {
if (timeout !== undefined) {
clearTimeout(timeout);
}
if (delay !== null) {
timeout = setTimeout(off, delay);
}
await ledsOn();
}
async function setDelay($delay: number | null) {
const listenersSetUp = delay === null;
delay = $delay;
if (timeout !== undefined) {
clearTimeout(timeout);
}
if (delay === null) {
for (const eventType of EVENT_TYPES) {
removeEventListener(eventType, listener);
}
} else {
timeout = setTimeout(screenOff, delay);
if (!listenersSetUp) {
for (const eventType of EVENT_TYPES) {
addEventListener(eventType, listener);
}
}
}
}
function delayValue(d?: string): number | null {
if (d === undefined || d === 'never') {
return null;
}
return parseInt(d, 10) * 60 * 1000;
}
export async function init(): Promise<void> {
setDelay(delayValue(await settings.get('screensaverDelay')));
settings.events.on('screensaverDelay', d => {
setDelay(delayValue(d));
});
}

View File

@@ -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 */

View File

@@ -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<T> {
[key: string]: T;
}

View File

@@ -19,8 +19,9 @@
const _ = require('lodash')
const electron = require('electron')
const Bluebird = require('bluebird')
const errors = require('../../../shared/errors')
const supportedFormats = require('../../../shared/supported-formats')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../gui/app/modules/errors')
const supportedFormats = require('../../../gui/app/modules/supported-formats')
/**
* @summary Current renderer BrowserWindow instance

View File

@@ -17,6 +17,7 @@
'use strict'
const electron = require('electron')
// eslint-disable-next-line node/no-missing-require
const settings = require('../models/settings')
/**

View File

@@ -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 () {
@@ -34,7 +35,7 @@ module.exports = function () {
*/
this.open = (url) => {
// Don't open links if they're disabled by the env var
if (settings.get('disableExternalLinks')) {
if (settings.get('disableExternalLinks') || !url) {
return
}

View File

@@ -17,7 +17,8 @@
'use strict'
const electron = require('electron')
const utils = require('../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../gui/app/modules/utils')
const progressStatus = require('../modules/progress-status')
/**

View File

@@ -25,7 +25,8 @@ const Path = require('path')
const process = require('process')
const { promisify } = require('util')
const { tmpFileDisposer } = require('../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const { tmpFileDisposer } = require('../../../gui/app/modules/utils')
const readFileAsync = promisify(fs.readFile)

View File

@@ -19,12 +19,13 @@
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')
const analytics = require('../../../modules/analytics')
const updateLock = require('../../../modules/update-lock')
const messages = require('../../../../../shared/messages')
const messages = require('../../../../../gui/app/modules/messages')
module.exports = function ($state) {
/**

View File

@@ -20,13 +20,19 @@ 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 availableDrives = require('../../../models/available-drives')
const selectionState = require('../../../models/selection-state')
const driveConstraints = require('../../../modules/drive-constraints')
const analytics = require('../../../modules/analytics')
const exceptionReporter = require('../../../modules/exception-reporter')
const utils = require('../../../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../../../gui/app/modules/utils')
module.exports = function ($timeout, DriveSelectorService) {
this.driveSelectorModalOpen = false;
module.exports = function (DriveSelectorService) {
/**
* @summary Get drive title based on device quantity
* @function
@@ -101,20 +107,23 @@ module.exports = function (DriveSelectorService) {
* DriveSelectionController.openDriveSelector();
*/
this.openDriveSelector = () => {
DriveSelectorService.open().then((drive) => {
if (!drive) {
return
}
this.driveSelectorModalOpen = true;
// Trigger re-render
$timeout()
//DriveSelectorService.open().then((drive) => {
// if (!drive) {
// return
// }
selectionState.selectDrive(drive.device)
// selectionState.selectDrive(drive.device)
analytics.logEvent('Select drive', {
device: drive.device,
unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
})
}).catch(exceptionReporter.report)
// analytics.logEvent('Select drive', {
// device: drive.device,
// unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
// applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
// flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
// })
//}).catch(exceptionReporter.report)
}
/**
@@ -156,4 +165,34 @@ module.exports = function (DriveSelectorService) {
this.shouldShowDrivesButton = () => {
return !settings.get('disableExplicitDriveSelection')
}
this.closeDriveSelectorModal = () => {
this.driveSelectorModalOpen = false
// Trigger re-render
$timeout()
}
this.setSelectedDrives = (drives) => {
const devices = drives.map(d => d.device);
for (const drive of availableDrives.getDrives()) {
if (devices.indexOf(drive.device) !== -1) {
selectionState.selectDrive(drive.device)
} else {
selectionState.deselectDrive(drive.device)
}
}
}
this.isDriveSelected = (drive) => {
return selectionState.isDriveSelected(drive.device)
}
this.isDriveValid = (drive) => {
return driveConstraints.isDriveValid(drive, selectionState.getImage());
}
this.getDriveBadges = (drive) => {
return driveConstraints.getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
}
}

View File

@@ -17,7 +17,7 @@
'use strict'
const _ = require('lodash')
const messages = require('../../../../../shared/messages')
const messages = require('../../../../../gui/app/modules/messages')
const flashState = require('../../../models/flash-state')
const driveScanner = require('../../../modules/drive-scanner')
const progressStatus = require('../../../modules/progress-status')
@@ -26,7 +26,7 @@ const analytics = require('../../../modules/analytics')
const imageWriter = require('../../../modules/image-writer')
const path = require('path')
const store = require('../../../models/store')
const constraints = require('../../../../../shared/drive-constraints')
const constraints = require('../../../../../gui/app/modules/drive-constraints')
const availableDrives = require('../../../models/available-drives')
const selection = require('../../../models/selection-state')

View File

@@ -22,10 +22,12 @@ const path = require('path')
const sdk = require('etcher-sdk')
const store = require('../../../models/store')
const messages = require('../../../../../shared/messages')
const errors = require('../../../../../shared/errors')
const supportedFormats = require('../../../../../shared/supported-formats')
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')
@@ -245,6 +247,11 @@ module.exports = function (
this.openImageSelector()
}
this.deselectImage = () => {
selectionState.deselectImage()
$timeout()
}
/**
* @summary Get the basename of the selected image
* @function
@@ -262,4 +269,54 @@ module.exports = function (
return path.basename(selectionState.getImagePath())
}
this.driveSelectorModalOpen = false
this.openDriveSelector = () => {
this.driveSelectorModalOpen = true;
$timeout()
}
this.closeDriveSelectorModal = () => {
this.driveSelectorModalOpen = false;
$timeout()
}
this.setSelectedDrives = (drives) => {
const currentlySelected = this.getSelectedDrive()
if (currentlySelected) {
drives = drives.filter(d => d.device !== currentlySelected.path)
}
if (drives.length === 0) {
this.deselectImage()
} else {
selectionState.selectImage({
path: drives[0].device,
size: drives[0].size,
isDrive: true
})
}
}
this.getSelectedDrive = () => {
const image = selectionState.getImage()
if (image && image.isDrive) {
return image
}
}
this.isDriveSelected = (drive) => {
const selectedDrive = this.getSelectedDrive()
return selectedDrive && selectedDrive.path === drive.device
}
this.isDriveValid = (drive) => {
return true // TODO: not valid if already a destination drive
}
this.getDriveBadges = (drive) => {
return [] // TODO: selected as destination (same as above)
}
}

View File

@@ -18,14 +18,15 @@
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')
const exceptionReporter = require('../../../modules/exception-reporter')
const availableDrives = require('../../../models/available-drives')
const selectionState = require('../../../models/selection-state')
const driveConstraints = require('../../../../../shared/drive-constraints')
const messages = require('../../../../../shared/messages')
const driveConstraints = require('../../../../../gui/app/modules/drive-constraints')
const messages = require('../../../../../gui/app/modules/messages')
const prettyBytes = require('pretty-bytes')
module.exports = function (

View File

@@ -34,6 +34,7 @@ const MainPage = angular.module(MODULE_NAME, [
require('angular-seconds-to-date'),
require('../../components/drive-selector/drive-selector'),
require('../../components/drive-selector2'),
require('../../components/tooltip-modal/tooltip-modal'),
require('../../components/flash-error-modal/flash-error-modal'),
require('../../components/progress-button'),

View File

@@ -1,5 +1,15 @@
<div class="page-main row around-xs">
<div class="col-xs" ng-controller="ImageSelectionController as image">
<drive-selector-2
ng-if="image.driveSelectorModalOpen"
title="'Select a source drive'"
close="image.closeDriveSelectorModal"
set-selected-drives="image.setSelectedDrives"
is-drive-selected="image.isDriveSelected"
is-drive-valid="image.isDriveValid"
get-drive-badges="image.getDriveBadges"
>
</drive-selector-2>
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
<div class="center-block">
@@ -10,12 +20,13 @@
<image-selector
has-image="main.selection.hasImage()"
open-image-selector="image.openImageSelector"
open-drive-selector="image.openDriveSelector"
main-supported-extensions="image.mainSupportedExtensions"
extra-supported-extensions="image.extraSupportedExtensions"
show-selected-image-details="main.showSelectedImageDetails"
image-name="main.selection.getImageName()"
image-basename="image.getImageBasename()"
reselect-image="image.reselectImage"
deselect-image="image.deselectImage"
flashing="main.state.isFlashing()"
image-size="main.selection.getImageSize()"
>
@@ -26,6 +37,16 @@
</div>
<div class="col-xs" ng-controller="DriveSelectionController as drive">
<drive-selector-2
ng-if="drive.driveSelectorModalOpen"
title="'Available targets'"
close="drive.closeDriveSelectorModal"
set-selected-drives="drive.setSelectedDrives"
is-drive-selected="drive.isDriveSelected"
is-drive-valid="drive.isDriveValid"
get-drive-badges="drive.getDriveBadges"
>
</drive-selector-2>
<div class="box text-center relative">
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>

View File

@@ -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')
@@ -107,6 +108,16 @@ module.exports = function (WarningModalService) {
}).catch(exceptionReporter.report)
}
this.set = (setting, value) => {
analytics.logEvent('Set setting', {
setting,
value,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid
})
this.model.set(setting, value)
this.refreshSettings()
}
/**
* @summary Show unsafe mode based on an env var
* @function
@@ -120,4 +131,19 @@ module.exports = function (WarningModalService) {
this.shouldShowUnsafeMode = () => {
return !settings.get('disableUnsafeMode')
}
/**
* @summary Show the screensaverDelay setting
* @function
* @public
*
* @returns {Boolean}
*
*
* @example
* SettingsController.shouldShowScreensaverDelay()
*/
this.shouldShowScreensaverDelay = () => {
return settings.get('showScreensaverDelay')
}
}

View File

@@ -77,4 +77,41 @@
<span>Unsafe mode <span class="label label-danger">Dangerous</span></span>
</label>
</div>
<div ng-if="settings.shouldShowScreensaverDelay()">
<h4>
Screensaver
</h4>
<label>
<input
ng-model="settings.currentData.screensaverDelay"
ng-change="settings.set('screensaverDelay', '5')"
type="radio"
value="5"
tabindex="11"
>
5 min
</label>
<label>
<input
ng-model="settings.currentData.screensaverDelay"
ng-change="settings.set('screensaverDelay', '10')"
type="radio"
value="10"
tabindex="12"
>
10 min
</label>
<label>
<input
ng-model="settings.currentData.screensaverDelay"
ng-change="settings.set('screensaverDelay', 'never')"
type="radio"
value="never"
tabindex="13"
>
never
</label>
</div>
</div>

View File

@@ -45,6 +45,7 @@ $fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free-webfonts/web
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fontawesome";
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-solid";
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-regular";
@font-face {
font-family: 'Nunito';
@@ -238,3 +239,19 @@ featured-project {
overflow: hidden;
}
}
.sleep-button {
float: left;
background-color: #3c3e42;
width: 76px;
height: 24px;
border-radius: 24px;
padding: 0px;
margin-top: 10px;
margin-left: 6px;
font-size: 12px;
&:hover {
background-color: #414347;
}
}

View File

@@ -16,7 +16,7 @@
'use strict'
const units = require('../../../../shared/units')
const units = require('../../../../gui/app/modules/units')
module.exports = () => {
/**

View File

@@ -16,7 +16,8 @@
'use strict'
const errors = require('../../../../../shared/errors')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../../../gui/app/modules/errors')
/**
* @summary ManifestBind directive

3
lib/gui/assets/moon.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#D3D6DB" fill-rule="evenodd" stroke="#D3D6DB" stroke-linecap="round" d="M5.313 1.002a.173.173 0 0 0-.038.01C2.559 2.02 1 4.642 1 7.686a7.12 7.12 0 0 0 7.116 7.116c3.044 0 5.667-1.565 6.673-4.28a.173.173 0 0 0-.226-.221c-.77.309-1.607.468-2.49.468-3.686 0-7.046-3.354-7.046-7.04 0-.883.16-1.72.469-2.49a.173.173 0 0 0-.183-.237zm-.248.49a7.033 7.033 0 0 0-.383 2.237c0 3.894 3.497 7.386 7.39 7.386.786 0 1.53-.147 2.237-.383-1.04 2.362-3.399 3.725-6.193 3.725-3.741 0-6.771-3.03-6.771-6.77 0-2.794 1.36-5.154 3.72-6.195z"/>
</svg>

After

Width:  |  Height:  |  Size: 630 B

View File

@@ -9860,6 +9860,17 @@ readers do not read off random characters that represent icons */
font-family: 'Font Awesome 5 Free';
font-weight: 900; }
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 400;
src: url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.eot");
src: url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.woff2") format("woff2"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.woff") format("woff"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.ttf") format("truetype"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
.far {
font-family: 'Font Awesome 5 Free';
font-weight: 400; }
@font-face {
font-family: 'Nunito';
src: url("Nunito-Regular.eot");
@@ -9992,3 +10003,16 @@ featured-project.fp-visible webview {
top: 45px;
border-radius: 7px;
overflow: hidden; }
.sleep-button {
float: left;
background-color: #3c3e42;
width: 76px;
height: 24px;
border-radius: 24px;
padding: 0px;
margin-top: 10px;
margin-left: 6px;
font-size: 12px; }
.sleep-button:hover {
background-color: #414347; }

View File

@@ -21,14 +21,14 @@ const path = require('path')
const _ = require('lodash')
const { autoUpdater } = require('electron-updater')
const Bluebird = require('bluebird')
const EXIT_CODES = require('../shared/exit-codes')
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')
const { getConfig } = require('../shared/utils')
/* eslint-disable lodash/prefer-lodash-method */
const config = settings.getDefaults()
// eslint-disable-next-line node/no-missing-require
const { getConfig } = require('./app/modules/utils')
/* eslint-disable lodash/prefer-lodash-method,func-style,space-before-function-paren,require-jsdoc */
const configUrl = settings.get('configUrl') || 'https://balena.io/etcher/static/config.json'
@@ -55,10 +55,11 @@ const checkForUpdates = async (interval) => {
/**
* @summary Create Etcher's main window
* @param {Object} config - config
* @example
* electron.app.on('ready', createMainWindow)
*/
const createMainWindow = () => {
const createMainWindow = (config) => {
const mainWindow = new electron.BrowserWindow({
// eslint-disable-next-line no-magic-numbers
width: parseInt(config.width, 10) || 800,
@@ -139,18 +140,17 @@ electron.app.on('before-quit', () => {
process.exit(EXIT_CODES.SUCCESS)
})
settings.load().then((localSettings) => {
Object.assign(config, localSettings)
}).catch((error) => {
// TODO: What do if loading the config fails?
console.error('Error loading settings:')
console.error(error)
}).finally(() => {
async function main() {
const config = settings.load()
if (electron.app.isReady()) {
createMainWindow()
createMainWindow(config)
} else {
electron.app.on('ready', createMainWindow)
electron.app.on('ready', () => {
createMainWindow(config)
})
}
})
}
main()
console.time('ready-to-show')

View File

@@ -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)
}

View File

@@ -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/modules/child-writer')
require('../generated/child-writer')
} else {
require('../generated/etcher')
}

1660
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
},
"scripts": {
"test": "make lint test sanity-checks",
"prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\"",
"start": "./node_modules/.bin/electron .",
"postshrinkwrap": "node ./scripts/clean-shrinkwrap.js",
"configure": "node-gyp configure",
@@ -35,6 +36,7 @@
"license": "Apache-2.0",
"platformSpecificDependencies": [
"fsevents",
"pigpio",
"winusb-driver-generator"
],
"dependencies": {
@@ -62,13 +64,14 @@
"nan": "^2.9.2",
"node-ipc": "^9.1.1",
"path-is-inside": "^1.0.2",
"pigpio": "^1.2.3",
"pretty-bytes": "^1.0.4",
"prop-types": "^15.5.9",
"react": "^16.8.5",
"react-dom": "^16.8.5",
"react2angular": "^4.0.2",
"redux": "^3.5.2",
"rendition": "^8.7.2",
"rendition": "^10.1.0",
"request": "^2.81.0",
"resin-corvus": "^2.0.3",
"roboto-fontface": "^0.9.0",
@@ -85,7 +88,14 @@
"@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/pigpio": "^1.2.1",
"@types/prop-types": "^15.7.1",
"@types/react": "^16.8.22",
"@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",
@@ -111,10 +121,15 @@
"node-sass": "^4.7.2",
"omit-deep-lodash": "1.1.4",
"pkg": "^4.3.0",
"resin-lint": "^3.0.4",
"sass-lint": "^1.12.1",
"simple-progress-webpack-plugin": "^1.1.2",
"spectron": "^5.0.0",
"webpack": "^4.27.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",
"webpack-node-externals": "^1.7.2"
}

View File

@@ -14,7 +14,7 @@ const chalk = require('chalk')
const path = require('path')
const _ = require('lodash')
const angularValidate = require('html-angular-validate')
const EXIT_CODES = require('../lib/shared/exit-codes')
const EXIT_CODES = require('../lib/gui/app/modules/exit-codes')
const PROJECT_ROOT = path.join(__dirname, '..')
const FILENAME = path.relative(PROJECT_ROOT, __filename)

View File

@@ -20,7 +20,8 @@ const _ = require('lodash')
const m = require('mochainon')
const angular = require('angular')
require('angular-mocks')
const utils = require('../../../lib/shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../lib/gui/app/modules/utils')
describe('Browser: DriveSelector', function () {
beforeEach(angular.mock.module(

View File

@@ -20,7 +20,7 @@ const m = require('mochainon')
const path = require('path')
const availableDrives = require('../../../lib/gui/app/models/available-drives')
const selectionState = require('../../../lib/gui/app/models/selection-state')
const constraints = require('../../../lib/shared/drive-constraints')
const constraints = require('../../../lib/gui/app/modules/drive-constraints')
describe('Model: availableDrives', function () {
describe('availableDrives', function () {

View File

@@ -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 () {
@@ -72,62 +73,6 @@ describe('Browser: settings', function () {
})
})
describe('.assign()', function () {
it('should throw if no settings', function (done) {
settings.assign().asCallback((error) => {
m.chai.expect(error).to.be.an.instanceof(Error)
m.chai.expect(error.message).to.equal('Missing settings')
done()
})
})
it('should not override all settings', function () {
return settings.assign({
foo: 'bar',
bar: 'baz'
}).then(() => {
m.chai.expect(settings.getAll()).to.deep.equal(_.assign({}, DEFAULT_SETTINGS, {
foo: 'bar',
bar: 'baz'
}))
})
})
it('should store the settings to the local machine', function () {
return localSettings.readAll().then((data) => {
m.chai.expect(data.foo).to.be.undefined
m.chai.expect(data.bar).to.be.undefined
return settings.assign({
foo: 'bar',
bar: 'baz'
})
}).then(localSettings.readAll).then((data) => {
m.chai.expect(data.foo).to.equal('bar')
m.chai.expect(data.bar).to.equal('baz')
})
})
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')
const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll')
localSettingsWriteAllStub.returns(Bluebird.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)
})
})
describe('.load()', function () {
it('should extend the application state with the local settings content', function () {
const object = {
@@ -160,28 +105,24 @@ 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) => {
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 +143,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)
}
})
})

View File

@@ -18,7 +18,7 @@
const m = require('mochainon')
const ipc = require('node-ipc')
require('../../../lib/gui/modules/child-writer')
require('../../../lib/gui/app/modules/child-writer')
describe('Browser: childWriter', function () {
it('should have the ipc config set to silent', function () {

View File

@@ -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')

View File

@@ -20,7 +20,7 @@ const m = require('mochainon')
const _ = require('lodash')
const fs = require('fs')
const path = require('path')
const supportedFormats = require('../../../lib/shared/supported-formats')
const supportedFormats = require('../../../lib/gui/app/modules/supported-formats')
const angular = require('angular')
const flashState = require('../../../lib/gui/app/models/flash-state')
const availableDrives = require('../../../lib/gui/app/models/available-drives')

View File

@@ -19,7 +19,7 @@
const m = require('mochainon')
const angular = require('angular')
require('angular-mocks')
const units = require('../../../lib/shared/units')
const units = require('../../../lib/gui/app/modules/units')
describe('Browser: ByteSize', function () {
beforeEach(angular.mock.module(
@@ -33,7 +33,7 @@ describe('Browser: ByteSize', function () {
closestUnitFilter = _closestUnitFilter_
}))
it('should expose lib/shared/units.js bytesToGigabytes()', function () {
it('should expose lib/gui/app/modules/units.js bytesToGigabytes()', function () {
m.chai.expect(closestUnitFilter).to.equal(units.bytesToClosestUnit)
})
})

View File

@@ -19,8 +19,8 @@
const m = require('mochainon')
const _ = require('lodash')
const path = require('path')
const constraints = require('../../lib/shared/drive-constraints')
const messages = require('../../lib/shared/messages')
const constraints = require('../../lib/gui/app/modules/drive-constraints')
const messages = require('../../lib/gui/app/modules/messages')
describe('Shared: DriveConstraints', function () {
describe('.isDriveLocked()', function () {

View File

@@ -18,7 +18,8 @@
const m = require('mochainon')
const _ = require('lodash')
const errors = require('../../lib/shared/errors')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../lib/gui/app/modules/errors')
describe('Shared: Errors', function () {
describe('.HUMAN_FRIENDLY', 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)
})
})

View File

@@ -18,7 +18,7 @@
const m = require('mochainon')
const _ = require('lodash')
const fileExtensions = require('../../lib/shared/file-extensions')
const fileExtensions = require('../../lib/gui/app/modules/file-extensions')
describe('Shared: fileExtensions', function () {
describe('.getFileExtensions()', function () {

View File

@@ -18,7 +18,7 @@
const m = require('mochainon')
const _ = require('lodash')
const messages = require('../../lib/shared/messages')
const messages = require('../../lib/gui/app/modules/messages')
describe('Shared: Messages', function () {
beforeEach(function () {

View File

@@ -20,7 +20,7 @@
const m = require('mochainon')
const os = require('os')
const permissions = require('../../lib/shared/permissions')
const permissions = require('../../lib/gui/app/modules/permissions')
describe('Shared: permissions', function () {
describe('.createLaunchScript()', function () {

View File

@@ -18,7 +18,7 @@
const m = require('mochainon')
const _ = require('lodash')
const supportedFormats = require('../../lib/shared/supported-formats')
const supportedFormats = require('../../lib/gui/app/modules/supported-formats')
describe('Shared: SupportedFormats', function () {
describe('.getCompressedExtensions()', function () {

View File

@@ -17,7 +17,7 @@
'use strict'
const m = require('mochainon')
const units = require('../../lib/shared/units')
const units = require('../../lib/gui/app/modules/units')
describe('Shared: Units', function () {
describe('.bytesToClosestUnit()', function () {

View File

@@ -18,7 +18,8 @@
const _ = require('lodash')
const m = require('mochainon')
const utils = require('../../lib/shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../lib/gui/app/modules/utils')
describe('Shared: Utils', function () {
describe('.isValidPercentage()', function () {

View File

@@ -19,7 +19,7 @@
const Bluebird = require('bluebird')
const spectron = require('spectron')
const m = require('mochainon')
const EXIT_CODES = require('../../lib/shared/exit-codes')
const EXIT_CODES = require('../../lib/gui/app/modules/exit-codes')
const entrypoint = process.env.ETCHER_SPECTRON_ENTRYPOINT
if (!entrypoint) {

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"allowJs": true,
"moduleResolution": "node",
"module": "commonjs",
"target": "es2017",
"jsx": "react"
},
"include": [
"lib/**/*.ts",
"node_modules/electron/**/*.d.ts",
"typings/**/*.d.ts"
]
}

View File

@@ -27,7 +27,6 @@ const commonConfig = {
// Minification breaks angular.
minimize: false
},
target: 'electron-main',
module: {
rules: [
{
@@ -51,11 +50,16 @@ const commonConfig = {
use: {
loader: 'html-loader'
}
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.js', '.jsx', '.json' ]
extensions: [ '.js', '.jsx', '.json', '.ts', '.tsx' ]
},
plugins: [
new SimpleProgressWebpackPlugin({
@@ -64,91 +68,92 @@ const commonConfig = {
]
}
const guiConfig = _.assign({
node: {
__dirname: true,
__filename: true
},
externals: [
nodeExternals(),
(context, request, callback) => {
// eslint-disable-next-line lodash/prefer-lodash-method
const absoluteContext = path.resolve(context)
const absoluteNodeModules = path.resolve('node_modules')
const guiConfig = _.assign(
{},
commonConfig,
{
node: {
__dirname: true,
__filename: true
},
target: 'electron-renderer',
externals: [
nodeExternals(),
(context, request, callback) => {
// eslint-disable-next-line lodash/prefer-lodash-method
const absoluteContext = path.resolve(context)
const absoluteNodeModules = path.resolve('node_modules')
// We shouldn't rewrite any node_modules import paths
// eslint-disable-next-line lodash/prefer-lodash-method
if (!path.relative(absoluteNodeModules, absoluteContext).startsWith('..')) {
return callback()
}
// We shouldn't rewrite any node_modules import paths
// eslint-disable-next-line lodash/prefer-lodash-method
if (!path.relative(absoluteNodeModules, absoluteContext).startsWith('..')) {
return callback()
}
// We want to keep the SDK code outside the GUI bundle.
// This piece of code allows us to run the GUI directly
// on the tree (for testing purposes) or inside a generated
// bundle (for production purposes), by translating
// relative require paths within the bundle.
if (/\/(sdk|shared)/i.test(request) || /package\.json$/.test(request)) {
const output = path.join(__dirname, 'generated')
const dirname = path.join(context, request)
const relative = path.relative(output, dirname)
return callback(null, `commonjs ${path.join('..', '..', relative)}`)
}
return callback()
],
entry: {
gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.js')
},
output: {
path: path.join(__dirname, 'generated'),
filename: '[name].js'
}
],
entry: {
gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.js')
},
output: {
path: path.join(__dirname, 'generated'),
filename: '[name].js'
}
}, commonConfig)
)
const etcherConfig = _.assign({
node: {
__dirname: false,
__filename: true
},
externals: [
nodeExternals(),
(context, request, callback) => {
// eslint-disable-next-line lodash/prefer-lodash-method
const absoluteContext = path.resolve(context)
const absoluteNodeModules = path.resolve('node_modules')
const etcherConfig = _.assign(
{},
commonConfig,
{
node: {
__dirname: false,
__filename: true
},
target: 'electron-main',
externals: [
nodeExternals(),
(context, request, callback) => {
// eslint-disable-next-line lodash/prefer-lodash-method
const absoluteContext = path.resolve(context)
const absoluteNodeModules = path.resolve('node_modules')
// We shouldn't rewrite any node_modules import paths
// eslint-disable-next-line lodash/prefer-lodash-method
if (!path.relative(absoluteNodeModules, absoluteContext).startsWith('..')) {
return callback()
}
// We shouldn't rewrite any node_modules import paths
// eslint-disable-next-line lodash/prefer-lodash-method
if (!path.relative(absoluteNodeModules, absoluteContext).startsWith('..')) {
return callback()
}
// We want to keep the SDK code outside the GUI bundle.
// This piece of code allows us to run the GUI directly
// on the tree (for testing purposes) or inside a generated
// bundle (for production purposes), by translating
// relative require paths within the bundle.
if (/\/shared/i.test(request) || /package\.json$/.test(request)) {
const output = path.join(__dirname, 'generated')
const dirname = path.join(context, request)
const relative = path.relative(output, dirname)
return callback(null, `commonjs ${path.join('..', 'lib', relative)}`)
}
return callback()
],
entry: {
etcher: path.join(__dirname, 'lib', 'gui', 'etcher.js')
},
output: {
path: path.join(__dirname, 'generated'),
filename: '[name].js'
}
],
entry: {
etcher: path.join(__dirname, 'lib', 'gui', 'etcher.js')
},
output: {
path: path.join(__dirname, 'generated'),
filename: '[name].js'
}
}, commonConfig)
)
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
]