diff --git a/.editorconfig b/.editorconfig index 1a62086..7267325 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,4 +6,4 @@ indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = false -insert_final_newline = false \ No newline at end of file +insert_final_newline = true \ No newline at end of file diff --git a/config/webpack.config.dev.js b/config/webpack.config.dev.js index 0929679..fe7979a 100644 --- a/config/webpack.config.dev.js +++ b/config/webpack.config.dev.js @@ -73,10 +73,7 @@ module.exports = { // We placed these paths second because we want `node_modules` to "win" // if there are any conflicts. This matches Node resolution mechanism. // https://github.com/facebookincubator/create-react-app/issues/253 - modules: [ - // path.resolve(__dirname, '..', 'src'), - // path.resolve(__dirname, '..', 'src', 'components'), - 'node_modules', paths.appNodeModules, paths.appSrc].concat( + modules: [paths.appSrc, paths.appNodeModules, 'node_modules'].concat( // It is guaranteed to exist because we tweak it in `env.js` process.env.NODE_PATH.split(path.delimiter).filter(Boolean) ), @@ -111,7 +108,7 @@ module.exports = { // To fix this, we prevent you from importing files out of src/ -- if you'd like to, // please link the files into your node_modules/ and let module-resolution kick in. // Make sure your source files are compiled, as they will not be processed in any way. - new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), + // new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), ], }, module: { diff --git a/package.json b/package.json index d257b7d..7298972 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,20 @@ "homepage": "https://redar.com/", "dependencies": { "@types/chrome": "^0.0.56", + "@types/flux": "^3.1.4", "autoprefixer": "7.1.2", + "axios": "^0.17.1", "case-sensitive-paths-webpack-plugin": "2.1.1", "chalk": "1.1.3", - "chrome": "^0.1.0", "css-loader": "0.28.4", "dotenv": "4.0.0", "extract-text-webpack-plugin": "3.0.0", "file-loader": "0.11.2", + "flux": "^3.1.3", "fs-extra": "3.0.1", "html-webpack-plugin": "2.29.0", "ifdef-loader": "^2.0.3", + "immutable": "^3.8.2", "jest": "20.0.4", "object-assign": "4.1.1", "postcss-flexbugs-fixes": "3.2.0", @@ -24,7 +27,6 @@ "react": "^16.0.0", "react-dev-utils": "^4.0.1", "react-dom": "^16.0.0", - "react-flux": "^1.0.1", "source-map-loader": "^0.2.1", "style-loader": "0.18.2", "sw-precache-webpack-plugin": "0.11.4", diff --git a/src/background.tsx b/src/background.tsx index 2c24f4a..7d63c99 100644 --- a/src/background.tsx +++ b/src/background.tsx @@ -1,4 +1,17 @@ -chrome .browserAction.onClicked.addListener((activeTab) => { +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' + +chrome.browserAction.onClicked.addListener((activeTab) => { const newURL = chrome.extension.getURL('index.html') chrome.tabs.create({ url: newURL }) -}) \ No newline at end of file +}) + +if (typeof chrome.runtime.onMessage.addListener === 'function') { + chrome.runtime.onMessage.addListener( + (message: {action: string, payload?: any}, sender: any, sendResponse: (response: any) => void) => { + if (message.action === 'request') { + console.debug('message received:', message.payload) + axios.request(message.payload).then((response: AxiosResponse) => sendResponse(response)) + } + return true + }) +} diff --git a/src/common/Dispatcher.tsx b/src/common/Dispatcher.tsx new file mode 100644 index 0000000..2432d1d --- /dev/null +++ b/src/common/Dispatcher.tsx @@ -0,0 +1,43 @@ +import { Dispatcher } from 'flux' +import { ReduceStore } from 'flux/utils' +import * as Immutable from 'immutable' + +export interface Action { + name: string, + payload?: T +} + +export const AppDispatcher = new Dispatcher() + +type TState = {} + +class AppStore extends ReduceStore { + private state: TState + + constructor() { + super(AppDispatcher) + this.state = {} + } + + getInitialState() { + return Immutable.OrderedMap([]) + } + + getState() { + return this.state + } + + reduce(state: TState, action: Action) { + switch (action.name) { + case 'get-data': + return state + default: + return state + } + } +} + +const Store = new AppStore() + +export default AppDispatcher +export { Store } diff --git a/src/components/AddressBar/AddressBar.module.d.ts b/src/components/AddressBar/AddressBar.module.d.ts index d94c7dd..dc8aaed 100644 --- a/src/components/AddressBar/AddressBar.module.d.ts +++ b/src/components/AddressBar/AddressBar.module.d.ts @@ -1,7 +1,8 @@ export interface IProps { + url?: string handleChange?(value: string, e: React.ChangeEvent): void } export interface IState { - -} \ No newline at end of file + url: string +} diff --git a/src/components/AddressBar/AddressBar.tsx b/src/components/AddressBar/AddressBar.tsx index ae2590d..10ec970 100644 --- a/src/components/AddressBar/AddressBar.tsx +++ b/src/components/AddressBar/AddressBar.tsx @@ -1,8 +1,16 @@ import * as React from 'react' import * as css from './AddressBar.css' -import * as types from './AddressBar.module' +import * as I from './AddressBar.module' +import Dispatcher from 'src/common/Dispatcher' -class AddressBar extends React.Component { +class AddressBar extends React.Component { + constructor(props: I.IProps) { + super(props) + + this.state = { + url: props.url || '' + } + } private handleChange(e: React.ChangeEvent) { const { value } = e.target as HTMLInputElement if (typeof this.props.handleChange === 'function') { @@ -10,10 +18,17 @@ class AddressBar extends React.Component { } } + public componentWillReceiveProps(nextProps: I.IProps) { + if (nextProps.url && nextProps.url !== this.props.url) { + this.setState({ url: nextProps.url }) + } + } + render() { return (
) => this.handleChange(e)} />
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 78e0341..8772808 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,10 +1,15 @@ import * as React from 'react' import * as css from './Button.css' -class Button extends React.Component { +interface Props { + className?: string + [k: string]: any | null | undefined +} + +class Button extends React.Component { render() { return ( - ) diff --git a/src/components/Header/Header.module.d.ts b/src/components/Header/Header.module.d.ts index 01ccf61..8b7e0fc 100644 --- a/src/components/Header/Header.module.d.ts +++ b/src/components/Header/Header.module.d.ts @@ -2,6 +2,6 @@ export interface IProps { } export interface IState { - uri: string + url: string method: string -} \ No newline at end of file +} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index c6fccc3..aa05b8c 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,34 +1,43 @@ import * as React from 'react' import * as css from './Header.css' import * as I from './Header.module' -import * as selectBoxStyle from 'components/SelectBox/SelectBox.css' import AddressBar from 'components/AddressBar/AddressBar' -import SelectBox from 'components/SelectBox/SelectBox' +import SelectBox, { Option, styles as selectBoxStyle } from 'components/SelectBox/SelectBox' import Button from 'components/Button/Button' -import { Option } from 'components/SelectBox/SelectBox.module' +import axios from 'axios' class Header extends React.Component { private httpMethods = [ - { label: 'GET', value: 'GET', className: selectBoxStyle.option }, - { label: 'POST', value: 'POST', className: selectBoxStyle.option }, - { label: 'PUT', value: 'PUT', className: selectBoxStyle.option }, - { label: 'DELETE', value: 'DELETE', className: selectBoxStyle.option }, - ] as Option[] + { label: 'GET', value: 'GET' }, + { label: 'POST', value: 'POST' }, + { label: 'PUT', value: 'PUT' }, + { label: 'DELETE', value: 'DELETE' }, + ] as Array> constructor(props: I.IProps) { super(props) this.state = { - uri: '', - method: 'GET', + url: localStorage.lastUrl || '', + method: localStorage.lastMethod || 'GET', } } - private changeURI(location: string) { - this.setState({ uri: location }) + private changeURL(url: string) { + this.setState({ url }) + localStorage.lastUrl = url } - + private changeMethod(method: string) { this.setState({ method }) + localStorage.lastMethod = method + } + + private go() { + const { method, url: url } = this.state + axios.request({ method, url, data: [ + { a: 1, b: 2, c: 3 } + ] + }) } render() { @@ -44,10 +53,11 @@ class Header extends React.Component { />
- this.changeURI(value)}/> + this.changeURL(value)}/>
- +
) diff --git a/src/components/SelectBox/SelectBox.module.d.ts b/src/components/SelectBox/SelectBox.module.d.ts index 8adba20..641f41d 100644 --- a/src/components/SelectBox/SelectBox.module.d.ts +++ b/src/components/SelectBox/SelectBox.module.d.ts @@ -5,7 +5,7 @@ export interface Props { value?: T options: Option[] placeholder?: string - onChange?(value: T, e: React.ChangeEvent): void + onChange?(option: Option, e?: React.MouseEvent): void className?: string } @@ -23,4 +23,4 @@ export interface Option { className?: string } -export as namespace ISelectBox \ No newline at end of file +export as namespace ISelectBox diff --git a/src/components/SelectBox/SelectBox.tsx b/src/components/SelectBox/SelectBox.tsx index a37bb9a..df0a8e1 100644 --- a/src/components/SelectBox/SelectBox.tsx +++ b/src/components/SelectBox/SelectBox.tsx @@ -12,7 +12,7 @@ class SelectBox extends React.Component { this.state = { selected, - options: this.props.options, + options: props.options, open: false, input: '' } @@ -74,7 +74,12 @@ class SelectBox extends React.Component { this.setState({ selected: option, input: '' - }, () => this.toggleDropdown(false)) + }, () => { + this.toggleDropdown(false) + if (typeof this.props.onChange === 'function') { + this.props.onChange(this.state.selected!, e) + } + }) } public render() { @@ -118,4 +123,6 @@ class SelectBox extends React.Component { } } -export default SelectBox \ No newline at end of file +export default SelectBox +export * from './SelectBox.module' +export { css as styles } diff --git a/src/index.tsx b/src/index.tsx index 5c1ca7b..41d992e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import * as ReactDOM from 'react-dom' -import App from './layouts/App/App' +import App from './layouts/AppContainer' import registerServiceWorker from './registerServiceWorker' import './index.css' diff --git a/src/layouts/App/App.tsx b/src/layouts/App/App.tsx index 556309c..98c68b1 100644 --- a/src/layouts/App/App.tsx +++ b/src/layouts/App/App.tsx @@ -2,22 +2,41 @@ import * as React from 'react' import * as css from './App.css' import Header from 'components/Header/Header' import DataTable from 'components/DataTable/DataTable' +import axios, { AxiosResponse } from 'axios' const data = [ { first: 'John', last: 'Doe', age: 22 }, { first: 'Jane', last: 'Doe', age: 24 }, ] -class App extends React.Component { +class App extends React.Component<{}> { constructor(props: {}) { super(props) console.debug('App Init!') + + /// #if EXTENSION + axios.defaults.adapter = (config) => { + return new Promise( + (resolve: (response: AxiosResponse) => void, reject: (reason: any) => void) => { + chrome.runtime.sendMessage({ action: 'request', payload: config }, (response) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError) + } + + resolve(response) + }) + }) + } + /// #endif } render() { return (
+
+ {JSON.stringify(this.props)} +
) diff --git a/src/layouts/AppContainer.tsx b/src/layouts/AppContainer.tsx new file mode 100644 index 0000000..2e25f59 --- /dev/null +++ b/src/layouts/AppContainer.tsx @@ -0,0 +1,20 @@ +import App from './App/App' +import { Container } from 'flux/utils' +import { Store } from 'common/Dispatcher' +import * as React from 'react' + +function getStores() { + return [ Store ] +} + +function getState() { + return { + store: Store.getState() + } +} + +function AppView(props: any): React.ReactElement { + return +} + +export default Container.createFunctional(AppView, getStores, getState) diff --git a/tsconfig.json b/tsconfig.json index 90fb9ff..de4d53e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,8 +19,9 @@ "baseUrl": "./", "allowSyntheticDefaultImports": true, "paths": { - "src/*": ["src/*"], - "components/*": ["src/components/*"] + "src/*": ["./src/*"], + "components/*": ["./src/components/*"], + "common/*": ["./src/common/*"] } }, "exclude": [ diff --git a/yarn.lock b/yarn.lock index 4e0b39e..8566b8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8,6 +8,10 @@ dependencies: "@types/filesystem" "*" +"@types/fbemitter@*": + version "2.0.32" + resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c" + "@types/filesystem@*": version "0.0.28" resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.28.tgz#3fd7735830f2c7413cb5ac45780bc45904697b0e" @@ -18,6 +22,13 @@ version "0.0.28" resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3" +"@types/flux@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@types/flux/-/flux-3.1.4.tgz#2573d724ad85edb53727af118003efd755af9a6b" + dependencies: + "@types/fbemitter" "*" + "@types/react" "*" + "@types/jest@^21.1.8": version "21.1.8" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.8.tgz#d497213725684f1e5a37900b17a47c9c018f1a97" @@ -789,6 +800,13 @@ aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +axios@^0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d" + dependencies: + follow-redirects "^1.2.5" + is-buffer "^1.1.5" + babel-code-frame@6.26.0, babel-code-frame@^6.11.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -1405,10 +1423,6 @@ base-task@^0.7.0: composer "^0.14.0" is-valid-app "^0.3.0" -base64-js@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" - base64-js@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" @@ -1453,7 +1467,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@^3.0.3, bluebird@^3.4.7, bluebird@^3.5.0: +bluebird@^3.4.7, bluebird@^3.5.0: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1840,13 +1854,6 @@ chownr@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" -chrome@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/chrome/-/chrome-0.1.0.tgz#f61d9b792fefe8c194c7056ddc102c726a864329" - dependencies: - exeq "^2.2.0" - plist "^1.1.0" - ci-info@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.1.tgz#47b44df118c48d2597b56d342e7e25791060171a" @@ -3174,13 +3181,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -exeq@^2.2.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/exeq/-/exeq-2.4.0.tgz#4ddf2a684648c427ad799349cf33bd75358f884a" - dependencies: - bluebird "^3.0.3" - native-or-bluebird "^1.2.0" - exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" @@ -3409,7 +3409,13 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.16, fbjs@^0.8.9: +fbemitter@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-2.1.1.tgz#523e14fdaf5248805bb02f62efc33be703f51865" + dependencies: + fbjs "^0.8.4" + +fbjs@^0.8.0, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" dependencies: @@ -3586,6 +3592,19 @@ flush-write-stream@^1.0.0: inherits "^2.0.1" readable-stream "^2.0.4" +flux@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/flux/-/flux-3.1.3.tgz#d23bed515a79a22d933ab53ab4ada19d05b2f08a" + dependencies: + fbemitter "^2.0.0" + fbjs "^0.8.0" + +follow-redirects@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.6.tgz#4dcdc7e4ab3dd6765a97ff89c3b4c258117c79bf" + dependencies: + debug "^3.1.0" + for-in@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" @@ -4432,6 +4451,10 @@ ignore@^3.3.5: version "3.3.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" +immutable@^3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -5732,10 +5755,6 @@ lodash.where@^3.1.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -lodash@^3.10.1, lodash@^3.5.0: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - log-ok@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334" @@ -6237,10 +6256,6 @@ nanoseconds@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/nanoseconds/-/nanoseconds-0.1.0.tgz#69ec39fcd00e77ab3a72de0a43342824cd79233a" -native-or-bluebird@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/native-or-bluebird/-/native-or-bluebird-1.2.0.tgz#39c47bfd7825d1fb9ffad32210ae25daadf101c9" - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6917,15 +6932,6 @@ pkg-store@^0.2.2: union-value "^0.2.3" write-json "^0.2.2" -plist@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/plist/-/plist-1.2.0.tgz#084b5093ddc92506e259f874b8d9b1afb8c79593" - dependencies: - base64-js "0.0.8" - util-deprecate "1.0.2" - xmlbuilder "4.0.0" - xmldom "0.1.x" - plugin-error@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace" @@ -7311,7 +7317,7 @@ promise@8.0.1: dependencies: asap "~2.0.3" -promise@^7.0.1, promise@^7.1.1: +promise@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" dependencies: @@ -7592,13 +7598,6 @@ react-error-overlay@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-3.0.0.tgz#c2bc8f4d91f1375b3dad6d75265d51cd5eeaf655" -react-flux@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/react-flux/-/react-flux-1.0.1.tgz#eb19f465a626a91edcf887b08c8df71a576fb300" - dependencies: - lodash "^3.10.1" - promise "^7.0.1" - react-input-autosize@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.0.1.tgz#e92190497b4026c2780ad0f2fd703c835ba03e33" @@ -9323,7 +9322,7 @@ use@^2.0.0: isobject "^3.0.0" lazy-cache "^2.0.2" -util-deprecate@1.0.2, util-deprecate@~1.0.1: +util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -9717,16 +9716,6 @@ xml-name-validator@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" -xmlbuilder@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.0.0.tgz#98b8f651ca30aa624036f127d11cc66dc7b907a3" - dependencies: - lodash "^3.5.0" - -xmldom@0.1.x: - version "0.1.27" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" - "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"