diff --git a/package.json b/package.json index 12fdc04e..34cf05ab 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "license": "LGPL-3.0+", "scripts": { "dev": "webpack-dev-server --config ./webpack/config.dev.babel.js --mode development", + "dev:local": "webpack-dev-server --config ./webpack/config.dev.local.babel.js --mode development", "build": "rm -rf build && webpack --config ./webpack/config.prod.babel.js --progress", "flow": "flow check src/js", "lint": "npx eslint ./src ./webpack", @@ -53,7 +54,7 @@ "redux-raven-middleware": "^1.2.0", "redux-thunk": "^2.2.0", "styled-components": "^3.3.3", - "trezor-connect": "5.0.13", + "trezor-connect": "5.0.28", "web3": "^0.19.0", "webpack": "^4.16.3", "whatwg-fetch": "^2.0.4", diff --git a/src/flowtype/index.js b/src/flowtype/index.js index a66b4135..7de58c39 100644 --- a/src/flowtype/index.js +++ b/src/flowtype/index.js @@ -53,11 +53,13 @@ export type TrezorDevice = { instanceLabel: string; instanceName: ?string; features?: Features; - unacquired?: boolean; - isUsedElsewhere?: boolean; + // unacquired?: boolean; // device.type === 'unacquired' && device.type !== 'unreadable' + // isUsedElsewhere?: boolean; // device.status === 'occupied' + type: 'acquired' | 'unacquired' | 'unreadable'; + status: 'available' | 'occupied' | 'used'; featuresNeedsReload?: boolean; ts: number; -} +}; export type RouterLocationState = LocationState; diff --git a/src/js/actions/SendFormActions.js b/src/js/actions/SendFormActions.js index e1e6a5e7..423d96f6 100644 --- a/src/js/actions/SendFormActions.js +++ b/src/js/actions/SendFormActions.js @@ -830,6 +830,25 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge if (!selected) return; const signedTransaction = await TrezorConnect.ethereumSignTransaction({ + device: { + path: selected.path, + instance: selected.instance, + state: selected.state + }, + useEmptyPassphrase: !selected.instance, + path: txData.address_n, + transaction: { + to: strip(txData.to), + value: strip(txData.value), + gasPrice: strip(txData.gasPrice), + gasLimit: strip(txData.gasLimit), + nonce: strip(txData.nonce), + data: strip(txData.data), + chainId: txData.chainId, + r: txData.r, + s: txData.s, + v: txData.v, + }, device: { path: selected.path, instance: selected.instance, @@ -860,8 +879,8 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge return; } - txData.r = `0x${signedTransaction.payload.r}`; - txData.s = `0x${signedTransaction.payload.s}`; + txData.r = signedTransaction.payload.r; + txData.s = signedTransaction.payload.s; txData.v = w3.toHex(signedTransaction.payload.v); diff --git a/src/js/actions/TrezorConnectActions.js b/src/js/actions/TrezorConnectActions.js index 72278921..cc87cbbc 100644 --- a/src/js/actions/TrezorConnectActions.js +++ b/src/js/actions/TrezorConnectActions.js @@ -16,7 +16,6 @@ import { resolveAfter } from '../utils/promiseUtils'; import type { Device, - ResponseMessage, DeviceMessage, UiMessage, TransportMessage, @@ -114,13 +113,17 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS try { await TrezorConnect.init({ + connectSrc: 'http://localhost:8081/', transportReconnect: true, - // connectSrc: 'https://localhost:8088/', - connectSrc: 'https://sisyfos.trezor.io/', debug: false, popup: false, webusb: true, pendingTransportEvent: (getState().devices.length < 1), + + // Use this if running 'yarn dev:local' + connectSrc: window.location.origin + '/', + // iframeSrc: window.location.origin + '/iframe.html', + // webusbSrc: window.location.origin + '/webusb.html', }); } catch (error) { // dispatch({ @@ -156,17 +159,17 @@ export const postInit = (): ThunkAction => (dispatch: Dispatch, getState: GetSta } if (devices.length > 0) { - const unacquired: ?TrezorDevice = devices.find(d => d.unacquired); + const unacquired: ?TrezorDevice = devices.find(d => d.type === 'unacquired'); if (unacquired) { - dispatch(onSelectDevice(unacquired)); + dispatch( onSelectDevice(unacquired) ); } else { const latest: Array = sortDevices(devices); const firstConnected: ?TrezorDevice = latest.find(d => d.connected); - dispatch(onSelectDevice(firstConnected || latest[0])); + dispatch( onSelectDevice(firstConnected || latest[0]) ); // TODO if (initialParams) { - if (!initialParams.hasOwnProperty('network') && initialPathname !== getState().router.location.pathname) { + if (!initialParams.hasOwnProperty("network") && initialPathname !== getState().router.location.pathname) { // dispatch( push(initialPathname) ); } else { @@ -246,7 +249,7 @@ export const switchToFirstAvailableDevice = (): AsyncAction => async (dispatch: // 1. First Unacquired // 2. First connected // 3. Saved with latest timestamp - const unacquired = devices.find(d => d.unacquired); + const unacquired = devices.find(d => d.type === 'unacquired'); if (unacquired) { dispatch(initConnectedDevice(unacquired)); } else { @@ -316,7 +319,7 @@ export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch dispatch(DiscoveryActions.stop(selected)); } - const instances = getState().devices.filter(d => d.features && d.state && !d.remember && d.features.device_id === device.features.device_id); + const instances = getState().devices.filter(d => d.features && d.state && !d.remember && device.features && d.features.device_id === device.features.device_id); if (instances.length > 0) { dispatch({ type: CONNECT.REMEMBER_REQUEST, diff --git a/src/js/actions/WalletActions.js b/src/js/actions/WalletActions.js index 763bb8db..09fcc423 100644 --- a/src/js/actions/WalletActions.js +++ b/src/js/actions/WalletActions.js @@ -75,7 +75,9 @@ export const toggleDeviceDropdown = (opened: boolean): WalletAction => ({ export const clearUnavailableDevicesData = (prevState: State, device: Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { if (!device.features) return; - const affectedDevices = prevState.devices.filter(d => d.features + const affectedDevices = prevState.devices.filter(d => + d.features + && device.features && d.features.device_id === device.features.device_id && d.features.passphrase_protection !== device.features.passphrase_protection); diff --git a/src/js/components/common/LoaderCircle.js b/src/js/components/common/LoaderCircle.js index fe4f4827..6020083a 100644 --- a/src/js/components/common/LoaderCircle.js +++ b/src/js/components/common/LoaderCircle.js @@ -3,14 +3,22 @@ import React from 'react'; -export default (props: { size: string, label?: string }): React$Element => { +type Props = { + size: string, + label?: string, + className?: string, +}; + +export default (props: Props): React$Element => { + const className = props.className ? `loader-circle ${props.className}` : 'loader-circle'; + const style = { width: `${props.size}px`, height: `${props.size}px`, - }; + } return ( -
+

{ props.label }

@@ -18,4 +26,4 @@ export default (props: { size: string, label?: string }): React$Element
); -}; \ No newline at end of file +}; diff --git a/src/js/components/common/Notification.js b/src/js/components/common/Notification.js index 59e450fa..ac80f222 100644 --- a/src/js/components/common/Notification.js +++ b/src/js/components/common/Notification.js @@ -5,6 +5,8 @@ import React from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; +import Loader from '../common/LoaderCircle'; + import * as NOTIFICATION from '~/js/actions/constants/notification'; import * as NotificationActions from '~/js/actions/NotificationActions'; import type { Action, State, Dispatch } from '~/flowtype'; @@ -21,7 +23,8 @@ type NProps = { title: string; message?: string; actions?: Array; - close?: typeof NotificationActions.close + close?: typeof NotificationActions.close, + loading?: boolean; } export const Notification = (props: NProps): React$Element => { @@ -48,6 +51,11 @@ export const Notification = (props: NProps): React$Element => { { actionButtons }
) : null } + { props.loading ? ( + + ) : null } ); diff --git a/src/js/components/wallet/aside/DeviceSelection.js b/src/js/components/wallet/aside/DeviceSelection.js index 443e37dc..eeae1b90 100644 --- a/src/js/components/wallet/aside/DeviceSelection.js +++ b/src/js/components/wallet/aside/DeviceSelection.js @@ -26,11 +26,11 @@ export const DeviceSelect = (props: Props) => { css += ' unavailable'; deviceStatus = 'Unavailable'; } else { - if (selected.unacquired) { + if (selected.type === 'unacquired') { css += ' unacquired'; deviceStatus = 'Used in other window'; } - if (selected.isUsedElsewhere) { + if (selected.status === 'occupied') { css += ' used-elsewhere'; deviceStatus = 'Used in other window'; } else if (selected.featuresNeedsReload) { @@ -147,7 +147,7 @@ export class DeviceDropdown extends Component { if (selected.features) { const deviceMenuItems: Array = []; - if (selected.isUsedElsewhere) { + if (selected.status === 'occupied') { deviceMenuItems.push({ type: 'reload', label: 'Renew session' }); } else if (selected.featuresNeedsReload) { deviceMenuItems.push({ type: 'reload', label: 'Renew session' }); @@ -177,7 +177,7 @@ export class DeviceDropdown extends Component { let deviceStatus: string = 'Connected'; let css: string = 'device item'; - if (dev.unacquired || dev.isUsedElsewhere) { + if (dev.type === 'unacquired' || dev.status === 'occupied') { deviceStatus = 'Used in other window'; css += ' unacquired'; } else if (!dev.connected) { diff --git a/src/js/components/wallet/pages/Acquire.js b/src/js/components/wallet/pages/Acquire.js index 1a3b768a..e60fa57e 100644 --- a/src/js/components/wallet/pages/Acquire.js +++ b/src/js/components/wallet/pages/Acquire.js @@ -23,16 +23,32 @@ const Acquire = (props: Props) => { }, }, ]; + const acquireDeviceAction = { + label: 'Acquire device', + callback: () => { + props.acquireDevice() + } + }; return (
- + {props.acquiring ? ( + + ) : ( + + )}
); }; diff --git a/src/js/reducers/DevicesReducer.js b/src/js/reducers/DevicesReducer.js index 7bc64155..d96ffc67 100644 --- a/src/js/reducers/DevicesReducer.js +++ b/src/js/reducers/DevicesReducer.js @@ -44,8 +44,8 @@ const mergeDevices = (current: TrezorDevice, upcoming: Device | TrezorDevice): T }; // corner-case: trying to merge unacquired device with acquired // make sure that sensitive fields will not be changed and device will remain acquired - if (upcoming.unacquired && current.state) { - dev.unacquired = false; + if (upcoming.type === 'unacquired' && current.state) { + dev.type = 'unacquired'; dev.features = current.features; dev.label = current.label; } @@ -65,7 +65,7 @@ const addDevice = (state: State, device: Device): State => { } otherDevices = state.filter(d => affectedDevices.indexOf(d) === -1); } else { - affectedDevices = state.filter(d => d.features && d.features.device_id === device.features.device_id); + affectedDevices = state.filter(d => d.features && device.features && d.features.device_id === device.features.device_id); const unacquiredDevices = state.filter(d => d.path.length > 0 && d.path === device.path); otherDevices = state.filter(d => affectedDevices.indexOf(d) < 0 && unacquiredDevices.indexOf(d) < 0); } @@ -112,7 +112,7 @@ const addDevice = (state: State, device: Device): State => { // changedDevices.push(newDevice); // } - const changedDevices: Array = affectedDevices.filter(d => d.features && d.features.passphrase_protection === device.features.passphrase_protection).map(d => mergeDevices(d, { ...device, connected: true, available: true })); + const changedDevices: Array = affectedDevices.filter(d => d.features && device.features && d.features.passphrase_protection === device.features.passphrase_protection).map(d => mergeDevices(d, { ...device, connected: true, available: true })); if (changedDevices.length !== affectedDevices.length) { changedDevices.push(newDevice); } @@ -150,7 +150,7 @@ const changeDevice = (state: State, device: Device): State => { // find devices with the same device_id and passphrase_protection settings // or devices with the same path (TODO: should be that way?) - const affectedDevices: Array = state.filter(d => (d.features && d.features.device_id === device.features.device_id && d.features.passphrase_protection === device.features.passphrase_protection) + const affectedDevices: Array = state.filter(d => (d.features && device.features && d.features.device_id === device.features.device_id && d.features.passphrase_protection === device.features.passphrase_protection) || (d.features && d.path.length > 0 && d.path === device.path)); const otherDevices: Array = state.filter(d => affectedDevices.indexOf(d) === -1); @@ -184,7 +184,8 @@ const devicesFromStorage = (devices: Array): State => devices.map( path: '', acquiring: false, featuresNeedsReload: false, - isUsedElsewhere: false, + //isUsedElsewhere: false, + status: 'available', })); // Remove all device reference from State @@ -204,11 +205,11 @@ const disconnectDevice = (state: State, device: Device): State => { const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1); if (affectedDevices.length > 0) { - const acquiredDevices = affectedDevices.filter(d => !d.unacquired && d.state); + const acquiredDevices = affectedDevices.filter(d => d.type !== 'unacquired' && d.type !== 'unreadable' && d.state); return otherDevices.concat(acquiredDevices.map((d) => { d.connected = false; d.available = false; - d.isUsedElsewhere = false; + d.status = 'used'; d.featuresNeedsReload = false; d.path = ''; return d; @@ -251,10 +252,9 @@ export default function devices(state: State = initialState, action: Action): St case DEVICE.CHANGED: return changeDevice(state, { ...action.device, connected: true, available: true }); - // TODO: check if available will propagate to unavailable + // TODO: check if available will propagate to unavailable case DEVICE.DISCONNECT: - case DEVICE.DISCONNECT_UNACQUIRED: return disconnectDevice(state, action.device); case WALLET.SET_SELECTED_DEVICE: diff --git a/src/js/reducers/ModalReducer.js b/src/js/reducers/ModalReducer.js index e545d561..996e6215 100644 --- a/src/js/reducers/ModalReducer.js +++ b/src/js/reducers/ModalReducer.js @@ -53,10 +53,11 @@ export default function modal(state: State = initialState, action: Action): Stat windowType: action.type, }; - case DEVICE.CHANGED: - if (state.opened && action.device.path === state.device.path && action.device.isUsedElsewhere) { - return initialState; + case DEVICE.CHANGED : + if (state.opened && action.device.path === state.device.path && action.device.status === 'occupied') { + return initialState } + return state; case DEVICE.DISCONNECT: diff --git a/src/js/reducers/utils/index.js b/src/js/reducers/utils/index.js index 26fb3761..11b5020d 100644 --- a/src/js/reducers/utils/index.js +++ b/src/js/reducers/utils/index.js @@ -30,7 +30,7 @@ export const getSelectedDevice = (state: State): ?TrezorDevice => { const instance: ?number = locationState.deviceInstance ? parseInt(locationState.deviceInstance) : undefined; return state.devices.find((d) => { - if (d.unacquired && d.path === locationState.device) { + if (d.type === 'unacquired' && d.path === locationState.device) { return true; } if (d.features && d.features.bootloader_mode && d.path === locationState.device) { return true; diff --git a/src/styles/loader.less b/src/styles/loader.less index 6b5338ae..671e50d0 100644 --- a/src/styles/loader.less +++ b/src/styles/loader.less @@ -15,18 +15,18 @@ } .circular { - + width: 100%; height: 100%; animation: rotate 2s linear infinite; transform-origin: center center; - + position: absolute; - + .route { stroke: @color_gray_light; } - + .path { stroke-dasharray: 1, 200; stroke-dashoffset: 0; @@ -34,15 +34,15 @@ stroke-linecap: round; } } -} -@keyframes rotate { - 100% { - transform: rotate(360deg); + &.info { + .path { + animation: dash 1.5s ease-in-out infinite, colorInfo 6s ease-in-out infinite; + } } } -@keyframes dash { +@keyframes dash { 0% { stroke-dasharray: 1, 200; stroke-dashoffset: 0; @@ -57,7 +57,7 @@ } } -@keyframes color { +@keyframes color { 100%, 0% { stroke: @color_green_primary; } @@ -70,4 +70,19 @@ 80%, 90% { stroke: @color_green_tertiary; } -} \ No newline at end of file +} + +@keyframes colorInfo { + 100%, 0% { + stroke: @color_info_primary; + } + 40% { + stroke: @color_info_primary; + } + 66% { + stroke: @color_info_primary; + } + 80%, 90% { + stroke: @color_info_primary; + } +} diff --git a/src/styles/notification.less b/src/styles/notification.less index dc8aab70..d091ceab 100644 --- a/src/styles/notification.less +++ b/src/styles/notification.less @@ -40,7 +40,7 @@ h2 { font-size: 14px; font-weight: bold; - + padding: 0px; &:before { .icomoon-info; @@ -68,7 +68,7 @@ } } - + &.success { color: @color_success_primary; diff --git a/webpack/config.dev.local.babel.js b/webpack/config.dev.local.babel.js new file mode 100644 index 00000000..b4160fc7 --- /dev/null +++ b/webpack/config.dev.local.babel.js @@ -0,0 +1,164 @@ +import { + TREZOR_CONNECT_ROOT, + TREZOR_CONNECT_HTML, + TREZOR_CONNECT_FILES, + TREZOR_CONNECT, TREZOR_IFRAME, TREZOR_POPUP, TREZOR_WEBUSB, + SRC, + BUILD, + PORT +} from './constants'; + + +import webpack from 'webpack'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; + +module.exports = { + watch: true, + mode: 'development', + devtool: 'inline-source-map', + entry: { + 'index': ['react-hot-loader/patch', `${SRC}js/index.js`], + 'trezor-connect-npm': `${TREZOR_CONNECT}.js`, + // 'extension-permissions': `${TREZOR_CONNECT_ROOT}src/js/extensionPermissions.js`, + 'iframe': TREZOR_IFRAME, + 'popup': TREZOR_POPUP, + 'webusb': TREZOR_WEBUSB, + }, + output: { + filename: '[name].[hash].js', + path: BUILD, + globalObject: 'this', // fix for HMR inside WebWorker from 'hd-wallet' + }, + devServer: { + contentBase: SRC, + hot: true, + https: false, + port: PORT, + stats: 'minimal', + inline: true, + }, + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: ['babel-loader'] + }, + { + test: /\.less$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + options: { publicPath: '../' } + }, + 'css-loader', + 'less-loader', + ] + }, + { + test: /\.(png|gif|jpg)$/, + loader: 'file-loader?name=./images/[name].[ext]', + query: { + outputPath: './images', + name: '[name].[ext]', + } + }, + { + test: /\.(ttf|eot|svg|woff|woff2)$/, + loader: 'file-loader', + query: { + outputPath: './fonts', + name: '[name].[ext]', + }, + }, + { + type: 'javascript/auto', + test: /\.json/, + exclude: /(node_modules)/, + loader: 'file-loader', + query: { + outputPath: './data', + name: '[name].[ext]', + }, + }, + { + type: 'javascript/auto', + test: /\.wasm$/, + loader: 'file-loader', + query: { + name: 'js/[name].[ext]', + }, + }, + ], + }, + resolve: { + modules: [SRC, 'node_modules', `${TREZOR_CONNECT_ROOT}/node_modules`], + alias: { + 'trezor-connect': `${TREZOR_CONNECT}` + } + }, + performance: { + hints: false + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css', + chunkFilename: '[id].css', + }), + + new HtmlWebpackPlugin({ + chunks: ['index'], + template: `${SRC}index.html`, + filename: 'index.html', + inject: true + }), + + new HtmlWebpackPlugin({ + chunks: ['iframe'], + filename: `iframe.html`, + template: `${TREZOR_CONNECT_HTML}iframe.html`, + inject: false + }), + new HtmlWebpackPlugin({ + chunks: ['popup'], + filename: 'popup.html', + template: `${TREZOR_CONNECT_HTML}popup.html`, + inject: false + }), + new HtmlWebpackPlugin({ + chunks: ['webusb'], + filename: `webusb.html`, + template: `${TREZOR_CONNECT_HTML}webusb.html`, + inject: true + }), + // new HtmlWebpackPlugin({ + // chunks: ['extension-permissions'], + // filename: `extension-permissions.html`, + // template: `${TREZOR_CONNECT_HTML}extension-permissions.html`, + // inject: true + // }), + + new CopyWebpackPlugin([ + { from: TREZOR_CONNECT_FILES, to: 'data' }, + ]), + + new webpack.optimize.OccurrenceOrderPlugin(), + new webpack.NoEmitOnErrorsPlugin(), + new webpack.HotModuleReplacementPlugin(), + new webpack.NamedModulesPlugin(), + + new webpack.DefinePlugin({ + LOCAL: JSON.stringify(`http://localhost:${PORT}/`), + }), + + // ignore node lib from trezor-link + new webpack.IgnorePlugin(/\/iconv-loader$/), + ], + // ignoring "fs" import in fastxpub + node: { + fs: "empty", + path: "empty", + }, +} diff --git a/webpack/constants.js b/webpack/constants.js index 8717349e..92858a2f 100644 --- a/webpack/constants.js +++ b/webpack/constants.js @@ -9,9 +9,17 @@ const constants: Object = Object.freeze({ SRC: path.join(ABSOLUTE_BASE, 'src/'), PORT: 8081, INDEX: path.join(ABSOLUTE_BASE, 'src/index.html'), - TREZOR_CONNECT_ROOT: path.join(ABSOLUTE_BASE, '../trezor.js2/'), + TREZOR_CONNECT_ROOT: path.join(ABSOLUTE_BASE, '../trezor-connect/') }); +export const TREZOR_CONNECT_ROOT: string = constants.TREZOR_CONNECT_ROOT; +export const TREZOR_CONNECT: string = path.join(constants.TREZOR_CONNECT_ROOT, 'src/js/index'); +export const TREZOR_IFRAME: string = path.join(constants.TREZOR_CONNECT_ROOT, 'src/js/iframe/iframe.js'); +export const TREZOR_POPUP: string = path.join(constants.TREZOR_CONNECT_ROOT, 'src/js/popup/popup.js'); +export const TREZOR_WEBUSB: string = path.join(constants.TREZOR_CONNECT_ROOT, 'src/js/webusb/index.js'); +export const TREZOR_CONNECT_HTML: string = path.join(constants.TREZOR_CONNECT_ROOT, 'src/html/'); +export const TREZOR_CONNECT_FILES: string = path.join(constants.TREZOR_CONNECT_ROOT, 'src/data/'); + export const BUILD: string = constants.BUILD; export const SRC: string = constants.SRC; export const PORT: string = constants.PORT; diff --git a/yarn.lock b/yarn.lock index a2694ede..6d8066e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3361,7 +3361,7 @@ eventemitter3@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" -events@^1.0.0: +events@^1.0.0, events@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -8787,9 +8787,14 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -trezor-connect@5.0.13: - version "5.0.13" - resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-5.0.13.tgz#2feee3d6644f8c3effd445b60ed80e7d5731469a" +trezor-connect@5.0.28: + version "5.0.28" + resolved "https://registry.yarnpkg.com/trezor-connect/-/trezor-connect-5.0.28.tgz#eb39bb0aa2a7555623251f0fb301233886fcdd09" + dependencies: + babel-polyfill "^6.26.0" + babel-runtime "^6.26.0" + events "^1.1.1" + whatwg-fetch "^2.0.4" trim-newlines@^1.0.0: version "1.0.0"