Browse Source

Merge branch 'master' into feature/meta-result-form

Artur Paikin 7 years ago
parent
commit
83d604c859
100 changed files with 11169 additions and 2554 deletions
  1. 1 1
      .babelrc
  2. 1 0
      .eslintignore
  3. 5 2
      .eslintrc
  4. 1 1
      .travis.yml
  5. 107 56
      CHANGELOG.md
  6. 5 5
      README.md
  7. 7 0
      bin/endtoend-build
  8. 0 2
      examples/aws-presigned-url/main.js
  9. 1 0
      examples/aws-uppy-server/.gitignore
  10. 7 0
      examples/aws-uppy-server/aliasify.js
  11. 12 0
      examples/aws-uppy-server/index.html
  12. 18 0
      examples/aws-uppy-server/main.js
  13. 4016 0
      examples/aws-uppy-server/package-lock.json
  14. 20 0
      examples/aws-uppy-server/package.json
  15. 15 0
      examples/aws-uppy-server/readme.md
  16. 23 0
      examples/aws-uppy-server/server.js
  17. 4 3
      examples/bundled-example/index.html
  18. 1 1
      examples/cdn-example/index.html
  19. 1 0
      examples/redux/.gitignore
  20. 7 0
      examples/redux/aliasify.js
  21. 27 0
      examples/redux/index.html
  22. 84 0
      examples/redux/main.js
  23. 3092 0
      examples/redux/package-lock.json
  24. 18 0
      examples/redux/package.json
  25. 16 0
      examples/redux/readme.md
  26. 978 219
      package-lock.json
  27. 24 15
      package.json
  28. 216 198
      src/core/Core.js
  29. 1 1
      src/core/Core.test.js
  30. 19 24
      src/core/Plugin.js
  31. 46 13
      src/core/Utils.js
  32. 61 4
      src/core/Utils.test.js
  33. 0 21
      src/generic-provider-views/AuthView.js
  34. 0 9
      src/generic-provider-views/Breadcrumb.js
  35. 0 9
      src/generic-provider-views/Loader.js
  36. 0 41
      src/generic-provider-views/Table.js
  37. 0 9
      src/generic-provider-views/TableColumn.js
  38. 0 14
      src/generic-provider-views/TableRow.js
  39. 9 41
      src/index.js
  40. 58 46
      src/plugins/AwsS3/index.js
  41. 35 20
      src/plugins/Dashboard/ActionBrowseTagline.js
  42. 68 145
      src/plugins/Dashboard/Dashboard.js
  43. 77 47
      src/plugins/Dashboard/FileCard.js
  44. 101 94
      src/plugins/Dashboard/FileItem.js
  45. 10 9
      src/plugins/Dashboard/FileItemProgress.js
  46. 40 40
      src/plugins/Dashboard/FileList.js
  47. 59 46
      src/plugins/Dashboard/Tabs.js
  48. 0 21
      src/plugins/Dashboard/UploadBtn.js
  49. 1 1
      src/plugins/Dashboard/getFileTypeIcon.js
  50. 53 52
      src/plugins/Dashboard/icons.js
  51. 190 111
      src/plugins/Dashboard/index.js
  52. 48 73
      src/plugins/DragDrop/index.js
  53. 11 9
      src/plugins/Dropbox/icons.js
  54. 23 30
      src/plugins/Dropbox/index.js
  55. 22 20
      src/plugins/Dummy.js
  56. 42 31
      src/plugins/FileInput.js
  57. 13 13
      src/plugins/Form.js
  58. 1 1
      src/plugins/GoldenRetriever/IndexedDBStore.js
  59. 63 43
      src/plugins/GoldenRetriever/index.js
  60. 21 31
      src/plugins/GoogleDrive/index.js
  61. 28 18
      src/plugins/Informer.js
  62. 25 36
      src/plugins/Instagram/index.js
  63. 5 6
      src/plugins/MagicLog.js
  64. 0 51
      src/plugins/MetaData.js
  65. 0 376
      src/plugins/Plugin.test.js
  66. 10 11
      src/plugins/ProgressBar.js
  67. 5 5
      src/plugins/Provider/index.js
  68. 31 0
      src/plugins/Provider/view/AuthView.js
  69. 5 0
      src/plugins/Provider/view/Breadcrumb.js
  70. 5 5
      src/plugins/Provider/view/Breadcrumbs.js
  71. 29 23
      src/plugins/Provider/view/Browser.js
  72. 44 0
      src/plugins/Provider/view/Filter.js
  73. 7 0
      src/plugins/Provider/view/Loader.js
  74. 54 0
      src/plugins/Provider/view/Table.js
  75. 46 0
      src/plugins/Provider/view/TableRow.js
  76. 230 55
      src/plugins/Provider/view/index.js
  77. 6 6
      src/plugins/Redux.js
  78. 10 16
      src/plugins/Redux.test.js
  79. 11 10
      src/plugins/ReduxDevTools.js
  80. 0 5
      src/plugins/RestoreFiles/index.js
  81. 95 85
      src/plugins/StatusBar/StatusBar.js
  82. 31 16
      src/plugins/StatusBar/index.js
  83. 306 66
      src/plugins/Transloadit/index.js
  84. 173 0
      src/plugins/Transloadit/index.test.js
  85. 65 48
      src/plugins/Tus.js
  86. 0 5
      src/plugins/Tus10.js
  87. 5 5
      src/plugins/Webcam/CameraIcon.js
  88. 30 32
      src/plugins/Webcam/CameraScreen.js
  89. 4 4
      src/plugins/Webcam/PermissionsScreen.js
  90. 11 11
      src/plugins/Webcam/RecordButton.js
  91. 3 3
      src/plugins/Webcam/RecordStartIcon.js
  92. 3 3
      src/plugins/Webcam/RecordStopIcon.js
  93. 7 7
      src/plugins/Webcam/SnapshotButton.js
  94. 5 5
      src/plugins/Webcam/WebcamIcon.js
  95. 46 30
      src/plugins/Webcam/index.js
  96. 51 35
      src/plugins/XHRUpload.js
  97. 1 1
      src/react/Dashboard.js
  98. 1 1
      src/react/DashboardModal.js
  99. 1 1
      src/react/DragDrop.js
  100. 1 1
      src/react/ProgressBar.js

+ 1 - 1
.babelrc

@@ -8,6 +8,6 @@
     "add-module-exports",
     "transform-object-assign",
     "es6-promise",
-    "yo-yoify"
+    ["transform-react-jsx", { "pragma":"h" }]
   ]
 }

+ 1 - 0
.eslintignore

@@ -10,3 +10,4 @@ website/public/**
 website/themes/uppy/source/js/smooth-scroll.min.js
 website/themes/uppy/source/js/uppy.js
 website/themes/uppy/source/uppy/**
+bundle.js

+ 5 - 2
.eslintrc

@@ -1,5 +1,5 @@
 {
-  "extends": "standard",
+  "extends": ["standard", "standard-preact"],
   "env": {
     "browser": true,
     "node": true,
@@ -9,5 +9,8 @@
     "window": true,
     "hexo": true
   },
-  "plugins": ["jest"]
+  "plugins": ["jest"],
+  "rules": {
+    "jsx-quotes": ["error", "prefer-double"]
+  }
 }

+ 1 - 1
.travis.yml

@@ -21,7 +21,7 @@ script:
 - npm run web:install
 - npm run build
 - npm run test
-- if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then npm run test:acceptance:handleservers; fi
+- if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then npm run test:acceptance; fi
 env:
   global:
   - CXX=g++-4.8

+ 107 - 56
CHANGELOG.md

@@ -18,107 +18,158 @@ last Friday of every new month.
 
 Ideas that will be planned and find their way into a release at one point
 
-- [ ] build: investigate Rollup someday, for tree-shaking and smaller dist https://github.com/substack/node-browserify/issues/1379#issuecomment-183383199, https://github.com/nolanlawson/rollupify, https://github.com/nolanlawson/rollup-comparison
-- [ ] core: Decouple rendering from Plugins and try to make Uppy work with React (add basic example) to remain aware of possible issues (@hedgerh), look at https://github.com/akiran/react-slick, https://github.com/nosir/cleave.js
-- [ ] core: Have base styles, be explicit about fonts, etc
+- [ ] core: Decouple rendering from Plugin ?
 - [ ] core: Make sure Uppy works well in VR
 - [ ] test: Human should check http://www.webpagetest.org and https://developers.google.com/web/tools/lighthouse/, use it sometimes to test website and Uppy. Will show response/loading times and other issues
 - [ ] test: Human should test with real screen reader to identify accessibility problems
-- [ ] test: Make Edge and Safari work via the tunnel so we can test localhost instead of uppy.io, and test the current build, vs the previous deploy that way
 - [ ] test: setup an HTML page with all sorts of crazy styles, resets & bootstrap to see what brakes Uppy (@arturi)
 - [ ] dependencies: es6-promise --> lie https://github.com/calvinmetcalf/lie ?
 - [ ] core: accessibility research: https://chrome.google.com/webstore/detail/accessibility-developer-t/fpkknkljclfencbdbgkenhalefipecmb, http://khan.github.io/tota11y/
-- [ ] core: see if it’s possible to add webworkers for thumbnail generation (@arturi, @goto-bus-stop)
-- [ ] website: Would one really connect a own google drive or dropbox for testing purpose? => maybe one can give something like a testing account of google drive and dropbox to try uppy
-- [ ] ui: do we want https://github.com/kazzkiq/balloon.css ?
 - [ ] core: consider adding presets, see https://github.com/cssinjs/jss-preset-default/blob/master/src/index.js (@arturi)
 - [ ] uppy/uppy-server: Transfer files between providers (from instagram to Google drive for example).
-- [ ] uppy/uppy-server: review websocket connection and throttling progress events (@arturi, @ifedapoolarewaju)
 - [ ] uploaders: consider not showing progress updates from the server after an upload’s been paused (@arturi, @ifedapoolarewaju)
-- [ ] image cropping on the client (#151)
 - [ ] maybe restrict system file picking dialog too https://github.com/transloadit/uppy/issues/253
 - [ ] uppy-server: what happens if access token expires amid an upload/download process.
 - [ ] good way to change plugin options at runtime—maybe `this.state.options`?
 - [ ] s3: multipart/"resumable" uploads for large files (@goto-bus-stop)
-- [ ] uppy-server/s3: make s3 endpoint more configurable (@goto-bus-stop)
 - [ ] DnD Bar: drag and drop + statusbar or progressbar ? (@arturi)
 - [ ] possibility to work on already uploaded / in progress files #112, #113
 - [ ] possibility to edit/delete more than one file at once #118, #97
 - [ ] optimize problematic filenames #72
 - [ ] an uploader plugin to receive files in a callback instead of uploading them
-- [ ] consider iframe / more security for Transloadit/Uppy integration widget
 - [ ] statusbar: add option to always show
 - [ ] have a `resetProgress` method for resetting a single file, and call it before starting an upload. see comment in #393
 - [ ] “Custom Provider” plugin for  Dashboard — shows already uploaded files or files from a custom service; accepts an array of files to show in options, no uppy-server required #362
 - [ ] WordPress plugin
+- [ ] Transformations, cropping, filters for images, see #53
+- [ ] Prepare for (piwik-) tracking of usage of uppy ? see #83
+- [ ] screenshot+screencast support similar to Webcam #148
+- [ ] Webcam modes #198
+- [ ] feature: improved UI for Provider, Google Drive and Instagram, grid/list views
+- [ ] feature: React Native support
+- [ ] consider iframe / more security for Transloadit/Uppy integration widget and Uppy itself. Page can’t get files from Google Drive if its an iframe; possibility for folder restriction for provider plugins
+- [ ] test: add https://github.com/pa11y/pa11y for automated accessibility testing?
+- [ ] test: add tests for `npm pack`
+- [ ] test: add deepFreeze to test that state in not mutated anywhere by accident #320
+- [ ] audio: audio recording similar to Webcam #143
 
 ## 1.0 Goals
 
 What we need to do to release Uppy 1.0
 
-- [x] feature: restrictions: by size, number of files, file type
-- [x] feature: beta file recovering after closed tab / browser crash
-- [ ] feature: improved UI for Provider, Google Drive and Instagram, grid/list views
-- [x] feature: finish the direct-to-s3 upload plugin and test it with the flow to then upload to :transloadit: afterwards. This is because this might influence the inner flow of the plugin architecture quite a bit
-- [x] feature: easy integration with React (UppyReact components)
-- [x] feature: Redux and ReduxDevTools support (currently mirrors Uppy state to Redux)
-- [ ] feature: React Native support
-- [ ] feature: preset for Transloadit that mimics jQuery SDK
-- [x] QA: tests for core and utils
-- [ ] QA: tests for plugins
 - [ ] QA: test how everything works together: user experience from `npm install` to production build with Webpack, using in React/Redux environment (npm pack)
-- [ ] QA: test uppy server. benchmarks / stress test. multiple connections, different setups, large files. add metrics to Librato
-- [ ] QA: test in multiple browsers and mobile devices, fix bugs
-- [ ] QA: test with real screen reader to identify accessibility problems
-- [x] uppy-server: add uppy-server to main API service to scale it horizontally. for the standalone server, we could write the script to support multiple clusters. Not sure how required or neccessary this may be for Transloadit's API service.
-- [ ] ui: refine UI, neat things up (if that’s even a word)
-- [ ] refactoring: reduce size where possible, like, socket.io --> websockets (saves 20KB)
+- [ ] QA: test in multiple browsers and mobile devices
+- [ ] QA: test uppy server. benchmarks / stress test. multiple connections, different setups, large files (10 GB)
+- [ ] QA: tests for some plugins
+- [ ] automatically host releases on edgly and use that as our main CDN
+- [ ] docs: on using plugins, all options, list of plugins, i18n
+- [ ] feature: preset for Transloadit that mimics jQuery SDK
 - [ ] refactoring: possibly add CSS-in-JS
-- [ ] refactoring: possibly switch from Yo-Yo to Preact, because it’s more stable, solves a few issues we are struggling with (onload being weird/hard/modern-browsers-only with bel; no way to pass refs to elements; extra network requests with base64 urls) and mature, “new standard”, larger community
-- [ ] refactoring: possibly differentiate UI plugins from logic plugins, so that, say uploading plugins don’t include rendering stuff
+- [x] refactoring: possibly switch from Yo-Yo to Preact, because it’s more stable, solves a few issues we are struggling with (onload being weird/hard/modern-browsers-only with bel; no way to pass refs to elements; extra network requests with base64 urls) and mature, “new standard”, larger community
+- [ ] refactoring: split uppy into small packages, lerna repo?
+- [x] QA: tests for core and utils
+- [x] feature: Redux and ReduxDevTools support (currently mirrors Uppy state to Redux)
+- [x] feature: beta file recovering after closed tab / browser crash
+- [x] feature: easy integration with React (UppyReact components)
+- [x] feature: finish the direct-to-s3 upload plugin and test it with the flow to then upload to :transloadit: afterwards. This is because this might influence the inner flow of the plugin architecture quite a bit
+- [x] feature: restrictions: by size, number of files, file type
 - [x] refactoring: webcam plugin
-- [ ] refactoring: clean up code everywhere
-- [ ] docs: on using plugins, all options, list of plugins, i18n
-- [ ] uppy-server: better error handling, general cleanup (remove unused code. etc)
-- [ ] uppy-server: security audit
+- [x] uppy-server: add uppy-server to main API service to scale it horizontally. for the standalone server, we could write the script to support multiple clusters. Not sure how required or neccessary this may be for Transloadit's API service.
+- [x] uppy-server: better error handling, general cleanup (remove unused code. etc)
+- [x] uppy-server: security audit
 - [x] uppy-server: storing tokens in user’s browser only (d040281cc9a63060e2f2685c16de0091aee5c7b4)
-- [ ] consider iframe / more security for Transloadit/Uppy integration widget and Uppy itself. Page can’t get files from Google Drive if its an iframe; possibility for folder restriction for provider plugins
-- [ ] automatically host releases on edgly and use that as our main CDN
 
-## 0.22.0
+## 0.23.0
 
-- [ ] core: improve on Redux PR #216 to allow using Redux (or any other solution) for all Uppy state management, instead of proxy-only (@goto-bus-stop, @arturi)
-- [ ] provider: improve UI, add icons for file types (@arturi)
-- [ ] dashboard: place upload button into StatusBar, use Alex’s suggestions for retry
+- [ ] dashboard: add image cropping, study https://github.com/MattKetmo/darkroomjs/, https://github.com/fengyuanchen/cropperjs #151
+- [ ] goldenretriever: confirmation before restore #443
+- [ ] core: i18n all strings + document them
+- [ ] dashboard: try adding optional whitelabel “powered by uppy.io” (@arturi, @nqst)
+- [ ] transloadit: add error reporting
+- [ ] importurl: new plugin that imports files from urls (@arturi, @ifedapoolarewaju)
+- [ ] core: queue preview generation #431
+- [ ] core: return `processing` results among with `upload` results in `success` event and `upload()` promise
+- [ ] core: css-in-js, while keeping non-random classnames (ideally prefixed) and useful preprocessor features. also see simple https://github.com/codemirror/CodeMirror/blob/master/lib/codemirror.css (@arturi, @goto-bus-stop)
+- [ ] look into text-based file type icons to save space, or more icons for file types? (@arturi)
+- [ ] core: all: reset or !important styles to be immune to any environment/page. Maybe `postcss-safe-important`, http://cleanslatecss.com/ Or increase specificity (with .uppy prefix) (@arturi)
 - [ ] dashboard: allow minimizing the Dashboard during upload (Uppy then becomes just a tiny progress indicator) (@arturi)
 - [ ] dashboard: cancel button for any kind of uploads? currently resume/pause only for tus, and cancel for XHR (@arturi, @goto-bus-stop)
+- [ ] docs: quick start guide: https://community.transloadit.com/t/quick-start-guide-would-be-really-helpful/14605 (@arturi)
+- [ ] docs: on writing plugins (@goto-bus-stop)
 - [ ] goldenretriever: add “ghost” files (@arturi)
+- [ ] webcam: URL.createObjectURL(MediaStream) is deprecated and will be removed soon: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject
+- [ ] xhrupload: add bundle option to send multiple files in one request (#442 / @goto-bus-stop)
+- [ ] uppy-server: look into typescripting for a type safe servers (@ifedapoolarewaju)
+- [ ] uppy-server: security audit, ask @acconut
+- [ ] uppy-server: benchmarks / stress test, large file, uppy-server / tus / S3 (10 GB)
 
 # next
 
+## 0.22.0
+
+To be released: 2017-12-21.
+Theme: 🎄 Christmas edition
+
+- [x] **⚠️ Breaking** core: rendering engine switched from `Yo-Yo` to `Preact`, and all views from `html` hyperx template strings to `JSX` (#451 / @arturi)
+- [x] **⚠️ Breaking** core: large refactor of Core and Plugins: `setFileState`, merge `MetaData` plugin into `Dashboard`, prefix "private" core methods with underscores (@arturi / #438) 
+- [x] **⚠️ Breaking** core: renamed `core` to `uppy` in plugins and what not. So instead of `this.core.state` we can now use `this.uppy.state` (#438 / @arturi) 
+- [x] **⚠️ Breaking** core: renamed events to remove `core:` prefix, as been suggested already. So: `success`, `error`, `upload-started` and so on, and prefixed event names for plugins sometimes, like `dashboard:file-card` (#438 / @arturi) 
+- [x] **⚠️ Breaking** core: CSS class names have been altered to use `uppy-` namespace, so `.UppyDashboard-files` --> `.uppy-Dashboard-files` and so on
+- [x] **⚠️ Breaking** dashboard: added `metaFields` option, pass an array of settings for UI field objects `{ id: 'caption', name: 'Caption', placeholder: 'describe what the image is about' }` (#438 / @arturi, @goto-bus-stop)
+- [ ] form: add `Form`, a new plugin that is used in conjunction with any other acquirer, responsible for: 1. acquiring the metadata from `<form>`; 2. intercepting submit event on the form, opening Uppy dialog instead; 3. injecting any result (like from Transloadit encoding plugin) back into the form (jquery-sdk includes the whole Assembly Status JSON in a hidden field) (@arturi)
+- [x] core: fix various bugs and issues (@arturi)
+- [x] dashboard: improve Dashboard UI (@arturi)
+- [x] uppy-server: remove pause/resume socket listeners when upload is done (@ifedapoolarewaju)
+- [x] uppy/uppy-server: remote server error handler (#446 / @ifedapoolarewaju)
+- [x] provider: dropbox thumbnail view seems not to be working (@ifedapoolarewaju)
+- [x] uppy-server: link uppy-server with https://snyk.io/ to aid vulnerability spotting (@ifedapoolarewaju)
+
+## 0.21.1
+
+Released: 2017-12-10.
+
+- **⚠️ Breaking** core: Set `this.el` in `Plugin` class (#425 / @arturi)
+- StatusBar, Dashboard and Provider UI improvements place upload button into StatusBar, use Alex’s suggestions for retry button; other UI tweaks (#434 / @arturi)
+- XHRUpload: fix fields in XHR remote uploader (#424 / @sadovnychyi)
+- XHRUpload: option to limit simultaneous uploads #360 (#427 / goto-bus-stop)
+- core: Add `isSupported()` API for providers (#421 / @goto-bus-stop, @arturi)
+- core: Add stores. Improve on Redux PR #216 to allow using Redux (or any other solution) for all Uppy state management, instead of proxy-only (#426 / @goto-bus-stop)
+- core: add ability to disable thumbnail generation (#432 / @richardwillars)
+- core: allow to select multiple files at once from remote providers (#419 / @sadovnychyi)
+- core: use `setPluginState` and `getPluginState` in Providers (#436 / @arturi)
+- docs: uppy-server docs for s3 `getKey` option (#444 / @goto-bus-stop)
+- goldenretriever: Fix IndexedDB store initialisation when not cleaning up (#430 / @goto-bus-stop)
+- provider: folder deselection did not remove all files (#439 / @ifedapoolarewaju)
+- s3: Use Translator for localised strings (420 / @goto-bus-stop )
+- transloadit: Port old tests from tape (#428 / @goto-bus-stop)
+- tus: Restore correctly from paused state (#443 / @goto-bus-stop)
+
 ## 0.21.0
 
-To be released: 2017-11-10
+Released: 2017-11-14.
 
-- [x] webcam: look into simplifying / improving webcam plugin (probably good to do modern browsers only) (#382 / @goto-bus-stop)
-- [ ] webcam: only show the webcam tab when browser support is available (media recorder API) (@arturi, @goto-bus-stop)
-- [ ] core: Redux PR (#216 / @arturi, @goto-bus-stop, @richardwillars)
-- [ ] core: css-in-js, while keeping non-random classnames (ideally prefixed) and useful preprocessor features. also see simple https://github.com/codemirror/CodeMirror/blob/master/lib/codemirror.css (@arturi, @goto-bus-stop)
-- [ ] core: research !important styles to be immune to any environment/page. Maybe use smth like `postcss-safe-important`. Or increase specificity (with .Uppy) (@arturi)
-- [ ] test: add tests for `npm pack`, modify acceptance/integration tests to not use website? (@arturi)
-- [ ] core: allow setting custom `id` for plugins: https://github.com/transloadit/uppy/pull/328#issuecomment-328242214 (@arturi)
-- [ ] add `Form`: a plugin that is used in conjunction with any other acquirer, responsible for 1\. acquiring the metadata from form; 2\. intercepting submit event on the form, opening Uppy dialog instead; 3\. injecting any result (like from Transloadit plugin) back into the form (jquery-sdk includes the whole Assembly Status JSON in a hidden field i think) (@arturi)
-- [ ] core: return `{ successful, failed }` from `uppy.upload()` (@goto-bus-stop)
+- [x] accessibility: add tabindex="0" to buttons and tabs, aria-labels, focus (#414 / @arturi)
+- [x] core: allow setting custom `id` for plugins to allow a plugin to be used multiple times (#418 / @arturi)
+- [x] core: do not check isPreviewSupported for unknown filetypes (#417 / @sadovnychyi)
 - [x] core: refactor `uppy-base` (#382 / @goto-bus-stop)
-- [x] uppy-server: look into storing tokens in user’s browser only (@ifedapoolarewaju)
-- [ ] accessibility: add tabindex="0" to buttons and tabs, aria-, focus; add https://github.com/pa11y/pa11y for automated accessibility testing  (@arturi)
-- [x] xhrupload: set a timeout in the onprogress event handler to detect stale network (#378 / @goto-bus-stop)
-- [ ] tus: Review b3cc48130e292f08c2a09f2f0adf6b6332bf7692
-- [x] tus: Rename Tus10 → Tus
-- [ ] docs: quick start guide: https://community.transloadit.com/t/quick-start-guide-would-be-really-helpful/14605 (@arturi)
+- [x] core: remove functions from state object (#408 / @goto-bus-stop)
+- [x] core: return `{ successful, failed }` from `uppy.upload()` (#404 / @goto-bus-stop)
+- [x] core: update state with error messages rather than error objects (#406 / @richardwillars)
+- [x] core: use `tinyify` for the unpkg bundle. (#371 / @goto-bus-stop)
+- [x] dashboard: Fix pasting files, default `image` file name, add type to meta, file type refactor (#395 / @arturi)
+- [x] dragdrop: Fix of the .uppy-DragDrop-inner spacing on small screens (#405 / @nqst)
+- [x] react: fix `uppy` PropType, closes (#416 / @goto-bus-stop)
+- [x] s3: automatically wrap XHRUpload. **Users should remove `.use(XHRUpload)` when using S3.** (#408 / @goto-bus-stop)
+- [x] test: refactored end-to-end tests to not use website, switched to Webdriver.io, added tests for Edge, Safari, Android and iOS (#410 / @arturi)
+- [x] tus: Rename Tus10 → Tus (#285 / @goto-bus-stop)
 - [x] uppy-serer: mask sensitive data from request logs (@ifedapoolarewaju)
 - [x] uppy-server: add request body validators (@ifedapoolarewaju)
-- [x] uppy-server: migrate dropbox to use v2 API (@ifedapoolarewaju)
+- [x] uppy-server: migrate dropbox to use v2 API (#386 / @ifedapoolarewaju)
+- [x] uppy-server: store tokens in user’s browser only (@ifedapoolarewaju)
+- [x] webcam: only show the webcam tab when browser support is available (media recorder API) (#421 / @arturi, @goto-bus-stop)
+- [x] webcam: simplify and refactor webcam plugin (modern browser APIs only) (#382 / @goto-bus-stop)
+- [x] xhrupload: set a timeout in the onprogress event handler to detect stale network (#378 / @goto-bus-stop)
+- [x] uppy-server: allow flexible whitelist endpoint protocols (@ifedapoolarewaju)
 
 ## 0.20.3
 

+ 5 - 5
README.md

@@ -33,7 +33,7 @@ const uppy = Uppy({ autoProceed: false })
   .use(GoogleDrive, { target: Dashboard, host: 'https://server.uppy.io' })
   .use(Tus, { endpoint: '://master.tus.io/files/' })
   .run()
-  .on('core:success', files => console.log(`Successfully uploaded these files: ${files}`))
+  .on('success', files => console.log(`Successfully uploaded these files: ${files}`))
 ```
 
 **[Try it online](http://uppy.io/examples/dashboard/)** or **[read the docs](http://uppy.io/docs)**  for details on how to use Uppy and its plugins.
@@ -81,8 +81,8 @@ If you like, you can also use a pre-built bundle, for example from [unpkg CDN](h
 ``` html
 <script>
   var uppy = Uppy.Core()
-  uppy.use(Uppy.DragDrop, {target: '.UppyDragDrop'})
-  uppy.use(Uppy.Tus, {endpoint: '//master.tus.io/files/'})
+  uppy.use(Uppy.DragDrop, { target: '.UppyDragDrop' })
+  uppy.use(Uppy.Tus, { endpoint: '//master.tus.io/files/' })
   uppy.run()
 </script>
 ```
@@ -119,13 +119,13 @@ If you like, you can also use a pre-built bundle, for example from [unpkg CDN](h
   <img src="https://saucelabs.com/browser-matrix/transloadit-uppy.svg" alt="Sauce Test Status"/>
 </a>
 
-Note: we aim to support IE10+ and recent versions of Safari, Edge, Chrome, Firefox and Opera. IE6 on the chart above means we recommend setting Uppy to target a `<form>` element, so when Uppy has not yet loaded or is not supported, upload still works. Even on the refrigerator browser. Or, yes, IE6.
+Note: we aim to support IE10+ and recent versions of Safari, Edge, Chrome, Firefox and Opera.
 
 ## FAQ
 
 ### React support?
 
-Yep. Uppy-React component is in the works, in the meantime you can just use it as any other lib with React, [see here](https://github.com/transloadit/uppy/tree/uppy-react/src/uppy-react).
+Yep, see [Uppy React docs](https://uppy.io/docs/react/).
 
 ### Can I use it with Rails/Node/Go/PHP?
 

+ 7 - 0
bin/endtoend-build

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+echo "Preparing for end to end test: copying static HTML and CSS, building JS"
+rm -rf ./test/endtoend/dist && mkdir ./test/endtoend/dist
+cp ./dist/uppy.min.css ./test/endtoend/dist 
+cp ./test/endtoend/src/index.html ./test/endtoend/dist 
+browserify ./test/endtoend/src/main.js -o ./test/endtoend/dist/bundle.js -t babelify

+ 0 - 2
examples/aws-presigned-url/main.js

@@ -1,6 +1,5 @@
 const Uppy = require('uppy/lib/core/Core.js')
 const Dashboard = require('uppy/lib/plugins/Dashboard')
-const XHRUpload = require('uppy/lib/plugins/XHRUpload')
 const AwsS3 = require('uppy/lib/plugins/AwsS3')
 
 const uppy = Uppy({
@@ -12,7 +11,6 @@ uppy.use(Dashboard, {
   inline: true,
   target: 'body'
 })
-uppy.use(XHRUpload)
 uppy.use(AwsS3, {
   getUploadParameters (file) {
     // Send a request to our PHP signing endpoint.

+ 1 - 0
examples/aws-uppy-server/.gitignore

@@ -0,0 +1 @@
+uppy.min.css

+ 7 - 0
examples/aws-uppy-server/aliasify.js

@@ -0,0 +1,7 @@
+const path = require('path')
+
+module.exports = {
+  replacements: {
+    '^uppy/lib/(.*?)$': path.join(__dirname, '../../src/$1')
+  }
+}

+ 12 - 0
examples/aws-uppy-server/index.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Uppy AWS Example</title>
+    <link href="uppy.min.css" rel="stylesheet">
+  </head>
+  <body>
+    <script src="bundle.js"></script>
+  </body>
+</html>

+ 18 - 0
examples/aws-uppy-server/main.js

@@ -0,0 +1,18 @@
+const Uppy = require('uppy/lib/core')
+const Dashboard = require('uppy/lib/plugins/Dashboard')
+const AwsS3 = require('uppy/lib/plugins/AwsS3')
+
+const uppy = Uppy({
+  debug: true,
+  autoProceed: false
+})
+
+uppy.use(Dashboard, {
+  inline: true,
+  target: 'body'
+})
+uppy.use(AwsS3, {
+  host: 'http://localhost:3020'
+})
+
+uppy.run()

+ 4016 - 0
examples/aws-uppy-server/package-lock.json

@@ -0,0 +1,4016 @@
+{
+  "name": "uppy-server-aws-example",
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "JSONStream": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
+      "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=",
+      "requires": {
+        "jsonparse": "1.3.1",
+        "through": "2.3.8"
+      }
+    },
+    "accepts": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz",
+      "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=",
+      "requires": {
+        "mime-types": "2.1.17",
+        "negotiator": "0.6.1"
+      }
+    },
+    "acorn": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz",
+      "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w=="
+    },
+    "aliasify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/aliasify/-/aliasify-2.1.0.tgz",
+      "integrity": "sha1-fDCCW5RQueYYW6J1M+r24gZ9S0I=",
+      "requires": {
+        "browserify-transform-tools": "1.7.0"
+      }
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+    },
+    "ansi-styles": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+    },
+    "anymatch": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+      "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+      "requires": {
+        "micromatch": "2.3.11",
+        "normalize-path": "2.1.1"
+      }
+    },
+    "arr-diff": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+      "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+      "requires": {
+        "arr-flatten": "1.1.0"
+      }
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg=="
+    },
+    "array-filter": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
+      "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw="
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E="
+    },
+    "array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+    },
+    "array-map": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
+      "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI="
+    },
+    "array-reduce": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
+      "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys="
+    },
+    "array-unique": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+      "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM="
+    },
+    "asn1.js": {
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.2.tgz",
+      "integrity": "sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg==",
+      "requires": {
+        "bn.js": "4.11.8",
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.0"
+      }
+    },
+    "assert": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+      "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+      "requires": {
+        "util": "0.10.3"
+      }
+    },
+    "astw": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz",
+      "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=",
+      "requires": {
+        "acorn": "4.0.13"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "4.0.13",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+          "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c="
+        }
+      }
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0="
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "requires": {
+        "chalk": "1.1.3",
+        "esutils": "2.0.2",
+        "js-tokens": "3.0.2"
+      }
+    },
+    "babel-core": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz",
+      "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=",
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "babel-generator": "6.26.0",
+        "babel-helpers": "6.24.1",
+        "babel-messages": "6.23.0",
+        "babel-register": "6.26.0",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "convert-source-map": "1.5.1",
+        "debug": "2.6.9",
+        "json5": "0.5.1",
+        "lodash": "4.17.4",
+        "minimatch": "3.0.4",
+        "path-is-absolute": "1.0.1",
+        "private": "0.1.8",
+        "slash": "1.0.0",
+        "source-map": "0.5.7"
+      }
+    },
+    "babel-generator": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz",
+      "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=",
+      "requires": {
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "detect-indent": "4.0.0",
+        "jsesc": "1.3.0",
+        "lodash": "4.17.4",
+        "source-map": "0.5.7",
+        "trim-right": "1.0.1"
+      }
+    },
+    "babel-helpers": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz",
+      "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=",
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0"
+      }
+    },
+    "babel-messages": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+      "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-register": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz",
+      "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+      "requires": {
+        "babel-core": "6.26.0",
+        "babel-runtime": "6.26.0",
+        "core-js": "2.5.1",
+        "home-or-tmp": "2.0.0",
+        "lodash": "4.17.4",
+        "mkdirp": "0.5.1",
+        "source-map-support": "0.4.18"
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "requires": {
+        "core-js": "2.5.1",
+        "regenerator-runtime": "0.11.0"
+      }
+    },
+    "babel-template": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+      "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-traverse": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+      "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "debug": "2.6.9",
+        "globals": "9.18.0",
+        "invariant": "2.2.2",
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "esutils": "2.0.2",
+        "lodash": "4.17.4",
+        "to-fast-properties": "1.0.3"
+      }
+    },
+    "babelify": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
+      "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=",
+      "requires": {
+        "babel-core": "6.26.0",
+        "object-assign": "4.1.1"
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+      "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ=="
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+    },
+    "base64-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz",
+      "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw=="
+    },
+    "binary-extensions": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz",
+      "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU="
+    },
+    "bn.js": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+    },
+    "body-parser": {
+      "version": "1.18.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
+      "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
+      "requires": {
+        "bytes": "3.0.0",
+        "content-type": "1.0.4",
+        "debug": "2.6.9",
+        "depd": "1.1.1",
+        "http-errors": "1.6.2",
+        "iconv-lite": "0.4.19",
+        "on-finished": "2.3.0",
+        "qs": "6.5.1",
+        "raw-body": "2.3.2",
+        "type-is": "1.6.15"
+      }
+    },
+    "bole": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/bole/-/bole-2.0.0.tgz",
+      "integrity": "sha1-2KocaQRnv7T+Ebh0rLLoOH44JhU=",
+      "requires": {
+        "core-util-is": "1.0.2",
+        "individual": "3.0.0",
+        "json-stringify-safe": "5.0.1"
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
+      "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
+      "requires": {
+        "balanced-match": "1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+      "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+      "requires": {
+        "expand-range": "1.8.2",
+        "preserve": "0.2.0",
+        "repeat-element": "1.1.2"
+      }
+    },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+    },
+    "browser-pack": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.2.tgz",
+      "integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=",
+      "requires": {
+        "JSONStream": "1.3.1",
+        "combine-source-map": "0.7.2",
+        "defined": "1.0.0",
+        "through2": "2.0.3",
+        "umd": "3.0.1"
+      }
+    },
+    "browser-resolve": {
+      "version": "1.11.2",
+      "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz",
+      "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=",
+      "requires": {
+        "resolve": "1.1.7"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+          "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
+        }
+      }
+    },
+    "browserify": {
+      "version": "14.5.0",
+      "resolved": "https://registry.npmjs.org/browserify/-/browserify-14.5.0.tgz",
+      "integrity": "sha512-gKfOsNQv/toWz+60nSPfYzuwSEdzvV2WdxrVPUbPD/qui44rAkB3t3muNtmmGYHqrG56FGwX9SUEQmzNLAeS7g==",
+      "requires": {
+        "JSONStream": "1.3.1",
+        "assert": "1.4.1",
+        "browser-pack": "6.0.2",
+        "browser-resolve": "1.11.2",
+        "browserify-zlib": "0.2.0",
+        "buffer": "5.0.8",
+        "cached-path-relative": "1.0.1",
+        "concat-stream": "1.5.2",
+        "console-browserify": "1.1.0",
+        "constants-browserify": "1.0.0",
+        "crypto-browserify": "3.12.0",
+        "defined": "1.0.0",
+        "deps-sort": "2.0.0",
+        "domain-browser": "1.1.7",
+        "duplexer2": "0.1.4",
+        "events": "1.1.1",
+        "glob": "7.1.2",
+        "has": "1.0.1",
+        "htmlescape": "1.1.1",
+        "https-browserify": "1.0.0",
+        "inherits": "2.0.3",
+        "insert-module-globals": "7.0.1",
+        "labeled-stream-splicer": "2.0.0",
+        "module-deps": "4.1.1",
+        "os-browserify": "0.3.0",
+        "parents": "1.0.1",
+        "path-browserify": "0.0.0",
+        "process": "0.11.10",
+        "punycode": "1.4.1",
+        "querystring-es3": "0.2.1",
+        "read-only-stream": "2.0.0",
+        "readable-stream": "2.3.3",
+        "resolve": "1.5.0",
+        "shasum": "1.0.2",
+        "shell-quote": "1.6.1",
+        "stream-browserify": "2.0.1",
+        "stream-http": "2.7.2",
+        "string_decoder": "1.0.3",
+        "subarg": "1.0.0",
+        "syntax-error": "1.3.0",
+        "through2": "2.0.3",
+        "timers-browserify": "1.4.2",
+        "tty-browserify": "0.0.0",
+        "url": "0.11.0",
+        "util": "0.10.3",
+        "vm-browserify": "0.0.4",
+        "xtend": "4.0.1"
+      }
+    },
+    "browserify-aes": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz",
+      "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==",
+      "requires": {
+        "buffer-xor": "1.0.3",
+        "cipher-base": "1.0.4",
+        "create-hash": "1.1.3",
+        "evp_bytestokey": "1.0.3",
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "browserify-cipher": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz",
+      "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=",
+      "requires": {
+        "browserify-aes": "1.1.1",
+        "browserify-des": "1.0.0",
+        "evp_bytestokey": "1.0.3"
+      }
+    },
+    "browserify-des": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz",
+      "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=",
+      "requires": {
+        "cipher-base": "1.0.4",
+        "des.js": "1.0.0",
+        "inherits": "2.0.3"
+      }
+    },
+    "browserify-rsa": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "requires": {
+        "bn.js": "4.11.8",
+        "randombytes": "2.0.5"
+      }
+    },
+    "browserify-sign": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+      "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+      "requires": {
+        "bn.js": "4.11.8",
+        "browserify-rsa": "4.0.1",
+        "create-hash": "1.1.3",
+        "create-hmac": "1.1.6",
+        "elliptic": "6.4.0",
+        "inherits": "2.0.3",
+        "parse-asn1": "5.1.0"
+      }
+    },
+    "browserify-transform-tools": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/browserify-transform-tools/-/browserify-transform-tools-1.7.0.tgz",
+      "integrity": "sha1-g+J3Ih9jJZvtLn6yooOpcKUB9MQ=",
+      "requires": {
+        "falafel": "2.1.0",
+        "through": "2.3.8"
+      }
+    },
+    "browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+      "requires": {
+        "pako": "1.0.6"
+      }
+    },
+    "budo": {
+      "version": "10.0.4",
+      "resolved": "https://registry.npmjs.org/budo/-/budo-10.0.4.tgz",
+      "integrity": "sha512-fJcz4EGwMno+e2xrD7QwclvZ77InghizqG8GGqMIYzanMUuWTOSrio+SUKpQRxLoFiSLiP+lceFcNbPseeew9A==",
+      "requires": {
+        "bole": "2.0.0",
+        "browserify": "14.5.0",
+        "chokidar": "1.7.0",
+        "connect-pushstate": "1.1.0",
+        "escape-html": "1.0.3",
+        "events": "1.1.1",
+        "garnish": "5.2.0",
+        "get-ports": "1.0.3",
+        "inject-lr-script": "2.1.0",
+        "internal-ip": "1.2.0",
+        "micromatch": "2.3.11",
+        "on-finished": "2.3.0",
+        "on-headers": "1.0.1",
+        "once": "1.4.0",
+        "opn": "3.0.3",
+        "path-is-absolute": "1.0.1",
+        "pem": "1.12.3",
+        "reload-css": "1.0.2",
+        "resolve": "1.5.0",
+        "serve-static": "1.13.1",
+        "simple-html-index": "1.5.0",
+        "stacked": "1.1.1",
+        "stdout-stream": "1.4.0",
+        "strip-ansi": "3.0.1",
+        "subarg": "1.0.0",
+        "term-color": "1.0.1",
+        "url-trim": "1.0.0",
+        "watchify-middleware": "1.6.0",
+        "ws": "1.1.5",
+        "xtend": "4.0.1"
+      }
+    },
+    "buffer": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.0.8.tgz",
+      "integrity": "sha512-xXvjQhVNz50v2nPeoOsNqWCLGfiv4ji/gXZM28jnVwdLJxH4mFyqgqCKfaK9zf1KUbG6zTkjLOy7ou+jSMarGA==",
+      "requires": {
+        "base64-js": "1.2.1",
+        "ieee754": "1.1.8"
+      }
+    },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
+    },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
+    },
+    "bytes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+    },
+    "cached-path-relative": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz",
+      "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc="
+    },
+    "camelcase": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
+    },
+    "camelcase-keys": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+      "requires": {
+        "camelcase": "2.1.1",
+        "map-obj": "1.0.1"
+      }
+    },
+    "chalk": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+      "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+      "requires": {
+        "ansi-styles": "2.2.1",
+        "escape-string-regexp": "1.0.5",
+        "has-ansi": "2.0.0",
+        "strip-ansi": "3.0.1",
+        "supports-color": "2.0.0"
+      }
+    },
+    "charenc": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+      "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
+    },
+    "chokidar": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+      "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+      "requires": {
+        "anymatch": "1.3.2",
+        "async-each": "1.0.1",
+        "fsevents": "1.1.3",
+        "glob-parent": "2.0.0",
+        "inherits": "2.0.3",
+        "is-binary-path": "1.0.1",
+        "is-glob": "2.0.1",
+        "path-is-absolute": "1.0.1",
+        "readdirp": "2.1.0"
+      }
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "requires": {
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz",
+      "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==",
+      "requires": {
+        "color-name": "1.1.3"
+      }
+    },
+    "color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+    },
+    "combine-source-map": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz",
+      "integrity": "sha1-CHAxKFazB6h8xKxIbzqaYq7MwJ4=",
+      "requires": {
+        "convert-source-map": "1.1.3",
+        "inline-source-map": "0.6.2",
+        "lodash.memoize": "3.0.4",
+        "source-map": "0.5.7"
+      },
+      "dependencies": {
+        "convert-source-map": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz",
+          "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA="
+        }
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "concat-stream": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz",
+      "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=",
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.0.6",
+        "typedarray": "0.0.6"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+        },
+        "readable-stream": {
+          "version": "2.0.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+          "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+          "requires": {
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "1.0.0",
+            "process-nextick-args": "1.0.7",
+            "string_decoder": "0.10.31",
+            "util-deprecate": "1.0.2"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        }
+      }
+    },
+    "connect-pushstate": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/connect-pushstate/-/connect-pushstate-1.1.0.tgz",
+      "integrity": "sha1-vKsiQnHEOWBKD7D2FMCl9WPojiQ="
+    },
+    "console-browserify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+      "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+      "requires": {
+        "date-now": "0.1.4"
+      }
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U="
+    },
+    "content-disposition": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+    },
+    "convert-source-map": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz",
+      "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU="
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+    },
+    "core-js": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz",
+      "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs="
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "cors": {
+      "version": "2.8.4",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz",
+      "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=",
+      "requires": {
+        "object-assign": "4.1.1",
+        "vary": "1.1.2"
+      }
+    },
+    "create-ecdh": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz",
+      "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=",
+      "requires": {
+        "bn.js": "4.11.8",
+        "elliptic": "6.4.0"
+      }
+    },
+    "create-hash": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz",
+      "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=",
+      "requires": {
+        "cipher-base": "1.0.4",
+        "inherits": "2.0.3",
+        "ripemd160": "2.0.1",
+        "sha.js": "2.4.9"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz",
+      "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=",
+      "requires": {
+        "cipher-base": "1.0.4",
+        "create-hash": "1.1.3",
+        "inherits": "2.0.3",
+        "ripemd160": "2.0.1",
+        "safe-buffer": "5.1.1",
+        "sha.js": "2.4.9"
+      }
+    },
+    "cross-spawn": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+      "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+      "requires": {
+        "lru-cache": "4.1.1",
+        "shebang-command": "1.2.0",
+        "which": "1.3.0"
+      }
+    },
+    "crypt": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+      "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
+    },
+    "crypto-browserify": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+      "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+      "requires": {
+        "browserify-cipher": "1.0.0",
+        "browserify-sign": "4.0.4",
+        "create-ecdh": "4.0.0",
+        "create-hash": "1.1.3",
+        "create-hmac": "1.1.6",
+        "diffie-hellman": "5.0.2",
+        "inherits": "2.0.3",
+        "pbkdf2": "3.0.14",
+        "public-encrypt": "4.0.0",
+        "randombytes": "2.0.5",
+        "randomfill": "1.0.3"
+      }
+    },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "requires": {
+        "array-find-index": "1.0.2"
+      }
+    },
+    "date-now": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+      "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs="
+    },
+    "debounce": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz",
+      "integrity": "sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ=="
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+    },
+    "define-properties": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz",
+      "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=",
+      "requires": {
+        "foreach": "2.0.5",
+        "object-keys": "1.0.11"
+      }
+    },
+    "defined": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+      "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM="
+    },
+    "depd": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
+      "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
+    },
+    "deps-sort": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz",
+      "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=",
+      "requires": {
+        "JSONStream": "1.3.1",
+        "shasum": "1.0.2",
+        "subarg": "1.0.0",
+        "through2": "2.0.3"
+      }
+    },
+    "des.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+      "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+      "requires": {
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.0"
+      }
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+    },
+    "detect-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+      "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+      "requires": {
+        "repeating": "2.0.1"
+      }
+    },
+    "detective": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.0.tgz",
+      "integrity": "sha512-4mBqSEdMfBpRAo/DQZnTcAXenpiSIJmVKbCMSotS+SFWWcrP/CKM6iBRPdTiEO+wZhlfEsoZlGqpG6ycl5vTqw==",
+      "requires": {
+        "acorn": "5.2.1",
+        "defined": "1.0.0"
+      }
+    },
+    "diffie-hellman": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz",
+      "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=",
+      "requires": {
+        "bn.js": "4.11.8",
+        "miller-rabin": "4.0.1",
+        "randombytes": "2.0.5"
+      }
+    },
+    "domain-browser": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz",
+      "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw="
+    },
+    "duplexer": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
+      "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E="
+    },
+    "duplexer2": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+      "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
+      "requires": {
+        "readable-stream": "2.3.3"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+    },
+    "elliptic": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
+      "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=",
+      "requires": {
+        "bn.js": "4.11.8",
+        "brorand": "1.1.0",
+        "hash.js": "1.1.3",
+        "hmac-drbg": "1.0.1",
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.0",
+        "minimalistic-crypto-utils": "1.0.1"
+      }
+    },
+    "encodeurl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
+      "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA="
+    },
+    "error-ex": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
+      "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
+      "requires": {
+        "is-arrayish": "0.2.1"
+      }
+    },
+    "es-abstract": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.10.0.tgz",
+      "integrity": "sha512-/uh/DhdqIOSkAWifU+8nG78vlQxdLckUdI/sPgy0VhuXi2qJ7T8czBmqIYtLQVpCIFYafChnsRsB5pyb1JdmCQ==",
+      "requires": {
+        "es-to-primitive": "1.1.1",
+        "function-bind": "1.1.1",
+        "has": "1.0.1",
+        "is-callable": "1.1.3",
+        "is-regex": "1.0.4"
+      }
+    },
+    "es-to-primitive": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz",
+      "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=",
+      "requires": {
+        "is-callable": "1.1.3",
+        "is-date-object": "1.0.1",
+        "is-symbol": "1.0.1"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+    },
+    "event-stream": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
+      "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
+      "requires": {
+        "duplexer": "0.1.1",
+        "from": "0.1.7",
+        "map-stream": "0.1.0",
+        "pause-stream": "0.0.11",
+        "split": "0.3.3",
+        "stream-combiner": "0.0.4",
+        "through": "2.3.8"
+      }
+    },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "requires": {
+        "md5.js": "1.3.4",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "expand-brackets": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+      "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+      "requires": {
+        "is-posix-bracket": "0.1.1"
+      }
+    },
+    "expand-range": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
+      "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
+      "requires": {
+        "fill-range": "2.2.3"
+      }
+    },
+    "express": {
+      "version": "4.16.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz",
+      "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=",
+      "requires": {
+        "accepts": "1.3.4",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.18.2",
+        "content-disposition": "0.5.2",
+        "content-type": "1.0.4",
+        "cookie": "0.3.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "1.1.1",
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "etag": "1.8.1",
+        "finalhandler": "1.1.0",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "1.1.2",
+        "on-finished": "2.3.0",
+        "parseurl": "1.3.2",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "2.0.2",
+        "qs": "6.5.1",
+        "range-parser": "1.2.0",
+        "safe-buffer": "5.1.1",
+        "send": "0.16.1",
+        "serve-static": "1.13.1",
+        "setprototypeof": "1.1.0",
+        "statuses": "1.3.1",
+        "type-is": "1.6.15",
+        "utils-merge": "1.0.1",
+        "vary": "1.1.2"
+      },
+      "dependencies": {
+        "setprototypeof": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+          "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+        }
+      }
+    },
+    "extglob": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+      "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+      "requires": {
+        "is-extglob": "1.0.0"
+      }
+    },
+    "falafel": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz",
+      "integrity": "sha1-lrsXdh2rqU9G0AFzizzt86Z/4Gw=",
+      "requires": {
+        "acorn": "5.2.1",
+        "foreach": "2.0.5",
+        "isarray": "0.0.1",
+        "object-keys": "1.0.11"
+      }
+    },
+    "filename-regex": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
+      "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY="
+    },
+    "fill-range": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",
+      "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=",
+      "requires": {
+        "is-number": "2.1.0",
+        "isobject": "2.1.0",
+        "randomatic": "1.1.7",
+        "repeat-element": "1.1.2",
+        "repeat-string": "1.6.1"
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
+      "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "on-finished": "2.3.0",
+        "parseurl": "1.3.2",
+        "statuses": "1.3.1",
+        "unpipe": "1.0.0"
+      }
+    },
+    "find-up": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+      "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+      "requires": {
+        "path-exists": "2.1.0",
+        "pinkie-promise": "2.0.1"
+      }
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA="
+    },
+    "for-own": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
+      "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+      "requires": {
+        "for-in": "1.0.2"
+      }
+    },
+    "foreach": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
+      "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
+    },
+    "forwarded": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+      "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+    },
+    "from": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
+      "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4="
+    },
+    "from2": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
+      "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "from2-string": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/from2-string/-/from2-string-1.1.0.tgz",
+      "integrity": "sha1-GCgrJ9CKJnyzAwzSuLSw8hKvdSo=",
+      "requires": {
+        "from2": "2.3.0"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+    },
+    "fsevents": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz",
+      "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==",
+      "optional": true,
+      "requires": {
+        "nan": "2.8.0",
+        "node-pre-gyp": "0.6.39"
+      },
+      "dependencies": {
+        "abbrev": {
+          "version": "1.1.0",
+          "bundled": true,
+          "optional": true
+        },
+        "ajv": {
+          "version": "4.11.8",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "co": "4.6.0",
+            "json-stable-stringify": "1.0.1"
+          }
+        },
+        "ansi-regex": {
+          "version": "2.1.1",
+          "bundled": true
+        },
+        "aproba": {
+          "version": "1.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "are-we-there-yet": {
+          "version": "1.1.4",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "delegates": "1.0.0",
+            "readable-stream": "2.2.9"
+          }
+        },
+        "asn1": {
+          "version": "0.2.3",
+          "bundled": true,
+          "optional": true
+        },
+        "assert-plus": {
+          "version": "0.2.0",
+          "bundled": true,
+          "optional": true
+        },
+        "asynckit": {
+          "version": "0.4.0",
+          "bundled": true,
+          "optional": true
+        },
+        "aws-sign2": {
+          "version": "0.6.0",
+          "bundled": true,
+          "optional": true
+        },
+        "aws4": {
+          "version": "1.6.0",
+          "bundled": true,
+          "optional": true
+        },
+        "balanced-match": {
+          "version": "0.4.2",
+          "bundled": true
+        },
+        "bcrypt-pbkdf": {
+          "version": "1.0.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "tweetnacl": "0.14.5"
+          }
+        },
+        "block-stream": {
+          "version": "0.0.9",
+          "bundled": true,
+          "requires": {
+            "inherits": "2.0.3"
+          }
+        },
+        "boom": {
+          "version": "2.10.1",
+          "bundled": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "brace-expansion": {
+          "version": "1.1.7",
+          "bundled": true,
+          "requires": {
+            "balanced-match": "0.4.2",
+            "concat-map": "0.0.1"
+          }
+        },
+        "buffer-shims": {
+          "version": "1.0.0",
+          "bundled": true
+        },
+        "caseless": {
+          "version": "0.12.0",
+          "bundled": true,
+          "optional": true
+        },
+        "co": {
+          "version": "4.6.0",
+          "bundled": true,
+          "optional": true
+        },
+        "code-point-at": {
+          "version": "1.1.0",
+          "bundled": true
+        },
+        "combined-stream": {
+          "version": "1.0.5",
+          "bundled": true,
+          "requires": {
+            "delayed-stream": "1.0.0"
+          }
+        },
+        "concat-map": {
+          "version": "0.0.1",
+          "bundled": true
+        },
+        "console-control-strings": {
+          "version": "1.1.0",
+          "bundled": true
+        },
+        "core-util-is": {
+          "version": "1.0.2",
+          "bundled": true
+        },
+        "cryptiles": {
+          "version": "2.0.5",
+          "bundled": true,
+          "requires": {
+            "boom": "2.10.1"
+          }
+        },
+        "dashdash": {
+          "version": "1.14.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "assert-plus": "1.0.0"
+          },
+          "dependencies": {
+            "assert-plus": {
+              "version": "1.0.0",
+              "bundled": true,
+              "optional": true
+            }
+          }
+        },
+        "debug": {
+          "version": "2.6.8",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "deep-extend": {
+          "version": "0.4.2",
+          "bundled": true,
+          "optional": true
+        },
+        "delayed-stream": {
+          "version": "1.0.0",
+          "bundled": true
+        },
+        "delegates": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "detect-libc": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "ecc-jsbn": {
+          "version": "0.1.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "jsbn": "0.1.1"
+          }
+        },
+        "extend": {
+          "version": "3.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "extsprintf": {
+          "version": "1.0.2",
+          "bundled": true
+        },
+        "forever-agent": {
+          "version": "0.6.1",
+          "bundled": true,
+          "optional": true
+        },
+        "form-data": {
+          "version": "2.1.4",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "asynckit": "0.4.0",
+            "combined-stream": "1.0.5",
+            "mime-types": "2.1.15"
+          }
+        },
+        "fs.realpath": {
+          "version": "1.0.0",
+          "bundled": true
+        },
+        "fstream": {
+          "version": "1.0.11",
+          "bundled": true,
+          "requires": {
+            "graceful-fs": "4.1.11",
+            "inherits": "2.0.3",
+            "mkdirp": "0.5.1",
+            "rimraf": "2.6.1"
+          }
+        },
+        "fstream-ignore": {
+          "version": "1.0.5",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "fstream": "1.0.11",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4"
+          }
+        },
+        "gauge": {
+          "version": "2.7.4",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "aproba": "1.1.1",
+            "console-control-strings": "1.1.0",
+            "has-unicode": "2.0.1",
+            "object-assign": "4.1.1",
+            "signal-exit": "3.0.2",
+            "string-width": "1.0.2",
+            "strip-ansi": "3.0.1",
+            "wide-align": "1.1.2"
+          }
+        },
+        "getpass": {
+          "version": "0.1.7",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "assert-plus": "1.0.0"
+          },
+          "dependencies": {
+            "assert-plus": {
+              "version": "1.0.0",
+              "bundled": true,
+              "optional": true
+            }
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "bundled": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "graceful-fs": {
+          "version": "4.1.11",
+          "bundled": true
+        },
+        "har-schema": {
+          "version": "1.0.5",
+          "bundled": true,
+          "optional": true
+        },
+        "har-validator": {
+          "version": "4.2.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "ajv": "4.11.8",
+            "har-schema": "1.0.5"
+          }
+        },
+        "has-unicode": {
+          "version": "2.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "hawk": {
+          "version": "3.1.3",
+          "bundled": true,
+          "requires": {
+            "boom": "2.10.1",
+            "cryptiles": "2.0.5",
+            "hoek": "2.16.3",
+            "sntp": "1.0.9"
+          }
+        },
+        "hoek": {
+          "version": "2.16.3",
+          "bundled": true
+        },
+        "http-signature": {
+          "version": "1.1.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "assert-plus": "0.2.0",
+            "jsprim": "1.4.0",
+            "sshpk": "1.13.0"
+          }
+        },
+        "inflight": {
+          "version": "1.0.6",
+          "bundled": true,
+          "requires": {
+            "once": "1.4.0",
+            "wrappy": "1.0.2"
+          }
+        },
+        "inherits": {
+          "version": "2.0.3",
+          "bundled": true
+        },
+        "ini": {
+          "version": "1.3.4",
+          "bundled": true,
+          "optional": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "1.0.0",
+          "bundled": true,
+          "requires": {
+            "number-is-nan": "1.0.1"
+          }
+        },
+        "is-typedarray": {
+          "version": "1.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "bundled": true
+        },
+        "isstream": {
+          "version": "0.1.2",
+          "bundled": true,
+          "optional": true
+        },
+        "jodid25519": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "jsbn": "0.1.1"
+          }
+        },
+        "jsbn": {
+          "version": "0.1.1",
+          "bundled": true
+        },
+        "json-schema": {
+          "version": "0.2.3",
+          "bundled": true,
+          "optional": true
+        },
+        "json-stable-stringify": {
+          "version": "1.0.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "jsonify": "0.0.0"
+          }
+        },
+        "json-stringify-safe": {
+          "version": "5.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "jsonify": {
+          "version": "0.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "jsprim": {
+          "version": "1.4.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "assert-plus": "1.0.0",
+            "extsprintf": "1.0.2",
+            "json-schema": "0.2.3",
+            "verror": "1.3.6"
+          },
+          "dependencies": {
+            "assert-plus": {
+              "version": "1.0.0",
+              "bundled": true,
+              "optional": true
+            }
+          }
+        },
+        "mime-db": {
+          "version": "1.27.0",
+          "bundled": true
+        },
+        "mime-types": {
+          "version": "2.1.15",
+          "bundled": true,
+          "requires": {
+            "mime-db": "1.27.0"
+          }
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "bundled": true,
+          "requires": {
+            "brace-expansion": "1.1.7"
+          }
+        },
+        "minimist": {
+          "version": "0.0.8",
+          "bundled": true
+        },
+        "mkdirp": {
+          "version": "0.5.1",
+          "bundled": true,
+          "requires": {
+            "minimist": "0.0.8"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "node-pre-gyp": {
+          "version": "0.6.39",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "detect-libc": "1.0.2",
+            "hawk": "3.1.3",
+            "mkdirp": "0.5.1",
+            "nopt": "4.0.1",
+            "npmlog": "4.1.0",
+            "rc": "1.2.1",
+            "request": "2.81.0",
+            "rimraf": "2.6.1",
+            "semver": "5.3.0",
+            "tar": "2.2.1",
+            "tar-pack": "3.4.0"
+          }
+        },
+        "nopt": {
+          "version": "4.0.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "abbrev": "1.1.0",
+            "osenv": "0.1.4"
+          }
+        },
+        "npmlog": {
+          "version": "4.1.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "are-we-there-yet": "1.1.4",
+            "console-control-strings": "1.1.0",
+            "gauge": "2.7.4",
+            "set-blocking": "2.0.0"
+          }
+        },
+        "number-is-nan": {
+          "version": "1.0.1",
+          "bundled": true
+        },
+        "oauth-sign": {
+          "version": "0.8.2",
+          "bundled": true,
+          "optional": true
+        },
+        "object-assign": {
+          "version": "4.1.1",
+          "bundled": true,
+          "optional": true
+        },
+        "once": {
+          "version": "1.4.0",
+          "bundled": true,
+          "requires": {
+            "wrappy": "1.0.2"
+          }
+        },
+        "os-homedir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "os-tmpdir": {
+          "version": "1.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "osenv": {
+          "version": "0.1.4",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "os-homedir": "1.0.2",
+            "os-tmpdir": "1.0.2"
+          }
+        },
+        "path-is-absolute": {
+          "version": "1.0.1",
+          "bundled": true
+        },
+        "performance-now": {
+          "version": "0.2.0",
+          "bundled": true,
+          "optional": true
+        },
+        "process-nextick-args": {
+          "version": "1.0.7",
+          "bundled": true
+        },
+        "punycode": {
+          "version": "1.4.1",
+          "bundled": true,
+          "optional": true
+        },
+        "qs": {
+          "version": "6.4.0",
+          "bundled": true,
+          "optional": true
+        },
+        "rc": {
+          "version": "1.2.1",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "deep-extend": "0.4.2",
+            "ini": "1.3.4",
+            "minimist": "1.2.0",
+            "strip-json-comments": "2.0.1"
+          },
+          "dependencies": {
+            "minimist": {
+              "version": "1.2.0",
+              "bundled": true,
+              "optional": true
+            }
+          }
+        },
+        "readable-stream": {
+          "version": "2.2.9",
+          "bundled": true,
+          "requires": {
+            "buffer-shims": "1.0.0",
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "1.0.0",
+            "process-nextick-args": "1.0.7",
+            "string_decoder": "1.0.1",
+            "util-deprecate": "1.0.2"
+          }
+        },
+        "request": {
+          "version": "2.81.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "aws-sign2": "0.6.0",
+            "aws4": "1.6.0",
+            "caseless": "0.12.0",
+            "combined-stream": "1.0.5",
+            "extend": "3.0.1",
+            "forever-agent": "0.6.1",
+            "form-data": "2.1.4",
+            "har-validator": "4.2.1",
+            "hawk": "3.1.3",
+            "http-signature": "1.1.1",
+            "is-typedarray": "1.0.0",
+            "isstream": "0.1.2",
+            "json-stringify-safe": "5.0.1",
+            "mime-types": "2.1.15",
+            "oauth-sign": "0.8.2",
+            "performance-now": "0.2.0",
+            "qs": "6.4.0",
+            "safe-buffer": "5.0.1",
+            "stringstream": "0.0.5",
+            "tough-cookie": "2.3.2",
+            "tunnel-agent": "0.6.0",
+            "uuid": "3.0.1"
+          }
+        },
+        "rimraf": {
+          "version": "2.6.1",
+          "bundled": true,
+          "requires": {
+            "glob": "7.1.2"
+          }
+        },
+        "safe-buffer": {
+          "version": "5.0.1",
+          "bundled": true
+        },
+        "semver": {
+          "version": "5.3.0",
+          "bundled": true,
+          "optional": true
+        },
+        "set-blocking": {
+          "version": "2.0.0",
+          "bundled": true,
+          "optional": true
+        },
+        "signal-exit": {
+          "version": "3.0.2",
+          "bundled": true,
+          "optional": true
+        },
+        "sntp": {
+          "version": "1.0.9",
+          "bundled": true,
+          "requires": {
+            "hoek": "2.16.3"
+          }
+        },
+        "sshpk": {
+          "version": "1.13.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "asn1": "0.2.3",
+            "assert-plus": "1.0.0",
+            "bcrypt-pbkdf": "1.0.1",
+            "dashdash": "1.14.1",
+            "ecc-jsbn": "0.1.1",
+            "getpass": "0.1.7",
+            "jodid25519": "1.0.2",
+            "jsbn": "0.1.1",
+            "tweetnacl": "0.14.5"
+          },
+          "dependencies": {
+            "assert-plus": {
+              "version": "1.0.0",
+              "bundled": true,
+              "optional": true
+            }
+          }
+        },
+        "string-width": {
+          "version": "1.0.2",
+          "bundled": true,
+          "requires": {
+            "code-point-at": "1.1.0",
+            "is-fullwidth-code-point": "1.0.0",
+            "strip-ansi": "3.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.0.1",
+          "bundled": true,
+          "requires": {
+            "safe-buffer": "5.0.1"
+          }
+        },
+        "stringstream": {
+          "version": "0.0.5",
+          "bundled": true,
+          "optional": true
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "bundled": true,
+          "requires": {
+            "ansi-regex": "2.1.1"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "tar": {
+          "version": "2.2.1",
+          "bundled": true,
+          "requires": {
+            "block-stream": "0.0.9",
+            "fstream": "1.0.11",
+            "inherits": "2.0.3"
+          }
+        },
+        "tar-pack": {
+          "version": "3.4.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "debug": "2.6.8",
+            "fstream": "1.0.11",
+            "fstream-ignore": "1.0.5",
+            "once": "1.4.0",
+            "readable-stream": "2.2.9",
+            "rimraf": "2.6.1",
+            "tar": "2.2.1",
+            "uid-number": "0.0.6"
+          }
+        },
+        "tough-cookie": {
+          "version": "2.3.2",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "punycode": "1.4.1"
+          }
+        },
+        "tunnel-agent": {
+          "version": "0.6.0",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "safe-buffer": "5.0.1"
+          }
+        },
+        "tweetnacl": {
+          "version": "0.14.5",
+          "bundled": true,
+          "optional": true
+        },
+        "uid-number": {
+          "version": "0.0.6",
+          "bundled": true,
+          "optional": true
+        },
+        "util-deprecate": {
+          "version": "1.0.2",
+          "bundled": true
+        },
+        "uuid": {
+          "version": "3.0.1",
+          "bundled": true,
+          "optional": true
+        },
+        "verror": {
+          "version": "1.3.6",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "extsprintf": "1.0.2"
+          }
+        },
+        "wide-align": {
+          "version": "1.1.2",
+          "bundled": true,
+          "optional": true,
+          "requires": {
+            "string-width": "1.0.2"
+          }
+        },
+        "wrappy": {
+          "version": "1.0.2",
+          "bundled": true
+        }
+      }
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+    },
+    "garnish": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/garnish/-/garnish-5.2.0.tgz",
+      "integrity": "sha1-vtQ2WTguSxmOM8eTiXvnxwHmVXc=",
+      "requires": {
+        "chalk": "0.5.1",
+        "minimist": "1.2.0",
+        "pad-left": "2.1.0",
+        "pad-right": "0.2.2",
+        "prettier-bytes": "1.0.4",
+        "pretty-ms": "2.1.0",
+        "right-now": "1.0.0",
+        "split2": "0.2.1",
+        "stdout-stream": "1.4.0",
+        "url-trim": "1.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz",
+          "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk="
+        },
+        "ansi-styles": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz",
+          "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94="
+        },
+        "chalk": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz",
+          "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=",
+          "requires": {
+            "ansi-styles": "1.1.0",
+            "escape-string-regexp": "1.0.5",
+            "has-ansi": "0.1.0",
+            "strip-ansi": "0.3.0",
+            "supports-color": "0.2.0"
+          }
+        },
+        "has-ansi": {
+          "version": "0.1.0",
+          "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz",
+          "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=",
+          "requires": {
+            "ansi-regex": "0.2.1"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        },
+        "strip-ansi": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz",
+          "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=",
+          "requires": {
+            "ansi-regex": "0.2.1"
+          }
+        },
+        "supports-color": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz",
+          "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo="
+        }
+      }
+    },
+    "get-ports": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/get-ports/-/get-ports-1.0.3.tgz",
+      "integrity": "sha1-9AvVgKyn7A77e5bL/L6wPviUteg=",
+      "requires": {
+        "map-limit": "0.0.1"
+      }
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4="
+    },
+    "glob": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+      "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+      "requires": {
+        "fs.realpath": "1.0.0",
+        "inflight": "1.0.6",
+        "inherits": "2.0.3",
+        "minimatch": "3.0.4",
+        "once": "1.4.0",
+        "path-is-absolute": "1.0.1"
+      }
+    },
+    "glob-base": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
+      "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+      "requires": {
+        "glob-parent": "2.0.0",
+        "is-glob": "2.0.1"
+      }
+    },
+    "glob-parent": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+      "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+      "requires": {
+        "is-glob": "2.0.1"
+      }
+    },
+    "globals": {
+      "version": "9.18.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+      "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ=="
+    },
+    "graceful-fs": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+      "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
+    },
+    "has": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz",
+      "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=",
+      "requires": {
+        "function-bind": "1.1.1"
+      }
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "requires": {
+        "ansi-regex": "2.1.1"
+      }
+    },
+    "has-flag": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+      "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE="
+    },
+    "hash-base": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz",
+      "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=",
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "hash.js": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz",
+      "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==",
+      "requires": {
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.0"
+      }
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "requires": {
+        "hash.js": "1.1.3",
+        "minimalistic-assert": "1.0.0",
+        "minimalistic-crypto-utils": "1.0.1"
+      }
+    },
+    "home-or-tmp": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
+      "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=",
+      "requires": {
+        "os-homedir": "1.0.2",
+        "os-tmpdir": "1.0.2"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
+      "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg=="
+    },
+    "htmlescape": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz",
+      "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E="
+    },
+    "http-errors": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
+      "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+      "requires": {
+        "depd": "1.1.1",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.0.3",
+        "statuses": "1.3.1"
+      }
+    },
+    "https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM="
+    },
+    "iconv-lite": {
+      "version": "0.4.19",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
+      "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
+    },
+    "ieee754": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz",
+      "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q="
+    },
+    "indent-string": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+      "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+      "requires": {
+        "repeating": "2.0.1"
+      }
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+    },
+    "individual": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz",
+      "integrity": "sha1-58pPhfiVewGHNPKFdQ3CLsL5hi0="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "requires": {
+        "once": "1.4.0",
+        "wrappy": "1.0.2"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+    },
+    "inject-lr-script": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/inject-lr-script/-/inject-lr-script-2.1.0.tgz",
+      "integrity": "sha1-5htehMEYczkGy+oB7D10Zpijn2U=",
+      "requires": {
+        "resp-modifier": "6.0.2"
+      }
+    },
+    "inline-source-map": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz",
+      "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=",
+      "requires": {
+        "source-map": "0.5.7"
+      }
+    },
+    "insert-module-globals": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.1.tgz",
+      "integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=",
+      "requires": {
+        "JSONStream": "1.3.1",
+        "combine-source-map": "0.7.2",
+        "concat-stream": "1.5.2",
+        "is-buffer": "1.1.6",
+        "lexical-scope": "1.2.0",
+        "process": "0.11.10",
+        "through2": "2.0.3",
+        "xtend": "4.0.1"
+      }
+    },
+    "internal-ip": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz",
+      "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=",
+      "requires": {
+        "meow": "3.7.0"
+      }
+    },
+    "invariant": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz",
+      "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=",
+      "requires": {
+        "loose-envify": "1.3.1"
+      }
+    },
+    "ipaddr.js": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz",
+      "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A="
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "requires": {
+        "binary-extensions": "1.11.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+    },
+    "is-builtin-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+      "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+      "requires": {
+        "builtin-modules": "1.1.1"
+      }
+    },
+    "is-callable": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz",
+      "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI="
+    },
+    "is-date-object": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
+    },
+    "is-dotfile": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
+      "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE="
+    },
+    "is-equal-shallow": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
+      "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
+      "requires": {
+        "is-primitive": "2.0.0"
+      }
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik="
+    },
+    "is-extglob": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+      "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA="
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "requires": {
+        "number-is-nan": "1.0.1"
+      }
+    },
+    "is-glob": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+      "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+      "requires": {
+        "is-extglob": "1.0.0"
+      }
+    },
+    "is-number": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
+      "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+      "requires": {
+        "kind-of": "3.2.2"
+      }
+    },
+    "is-posix-bracket": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
+      "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q="
+    },
+    "is-primitive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
+      "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU="
+    },
+    "is-regex": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
+      "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+      "requires": {
+        "has": "1.0.1"
+      }
+    },
+    "is-symbol": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz",
+      "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI="
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI="
+    },
+    "isarray": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+    },
+    "isobject": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+      "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+      "requires": {
+        "isarray": "1.0.0"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+        }
+      }
+    },
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
+    },
+    "jsesc": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+      "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s="
+    },
+    "json-parse-better-errors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz",
+      "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw=="
+    },
+    "json-stable-stringify": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz",
+      "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=",
+      "requires": {
+        "jsonify": "0.0.0"
+      }
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+    },
+    "json5": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+      "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
+    },
+    "jsonify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+      "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
+    },
+    "jsonparse": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+      "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
+    },
+    "kind-of": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+      "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+      "requires": {
+        "is-buffer": "1.1.6"
+      }
+    },
+    "labeled-stream-splicer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz",
+      "integrity": "sha1-pS4dE4AkwAuGscDJH2d5GLiuClk=",
+      "requires": {
+        "inherits": "2.0.3",
+        "isarray": "0.0.1",
+        "stream-splicer": "2.0.0"
+      }
+    },
+    "lexical-scope": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz",
+      "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=",
+      "requires": {
+        "astw": "2.2.0"
+      }
+    },
+    "load-json-file": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "parse-json": "2.2.0",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1",
+        "strip-bom": "2.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.4",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
+      "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
+    },
+    "lodash.memoize": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz",
+      "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8="
+    },
+    "loose-envify": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
+      "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
+      "requires": {
+        "js-tokens": "3.0.2"
+      }
+    },
+    "loud-rejection": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+      "requires": {
+        "currently-unhandled": "0.4.1",
+        "signal-exit": "3.0.2"
+      }
+    },
+    "lru-cache": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
+      "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==",
+      "requires": {
+        "pseudomap": "1.0.2",
+        "yallist": "2.1.2"
+      }
+    },
+    "map-limit": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz",
+      "integrity": "sha1-63lhAxwPDo0AG/LVb6toXViCLzg=",
+      "requires": {
+        "once": "1.3.3"
+      },
+      "dependencies": {
+        "once": {
+          "version": "1.3.3",
+          "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
+          "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=",
+          "requires": {
+            "wrappy": "1.0.2"
+          }
+        }
+      }
+    },
+    "map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0="
+    },
+    "map-stream": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
+      "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ="
+    },
+    "md5": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
+      "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
+      "requires": {
+        "charenc": "0.0.2",
+        "crypt": "0.0.2",
+        "is-buffer": "1.1.6"
+      }
+    },
+    "md5.js": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
+      "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=",
+      "requires": {
+        "hash-base": "3.0.4",
+        "inherits": "2.0.3"
+      },
+      "dependencies": {
+        "hash-base": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+          "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+          "requires": {
+            "inherits": "2.0.3",
+            "safe-buffer": "5.1.1"
+          }
+        }
+      }
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+    },
+    "memorystream": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+      "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI="
+    },
+    "meow": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+      "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+      "requires": {
+        "camelcase-keys": "2.1.0",
+        "decamelize": "1.2.0",
+        "loud-rejection": "1.6.0",
+        "map-obj": "1.0.1",
+        "minimist": "1.2.0",
+        "normalize-package-data": "2.4.0",
+        "object-assign": "4.1.1",
+        "read-pkg-up": "1.0.1",
+        "redent": "1.0.0",
+        "trim-newlines": "1.0.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        }
+      }
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+    },
+    "micromatch": {
+      "version": "2.3.11",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+      "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+      "requires": {
+        "arr-diff": "2.0.0",
+        "array-unique": "0.2.1",
+        "braces": "1.8.5",
+        "expand-brackets": "0.1.5",
+        "extglob": "0.3.2",
+        "filename-regex": "2.0.1",
+        "is-extglob": "1.0.0",
+        "is-glob": "2.0.1",
+        "kind-of": "3.2.2",
+        "normalize-path": "2.1.1",
+        "object.omit": "2.0.1",
+        "parse-glob": "3.0.4",
+        "regex-cache": "0.4.4"
+      }
+    },
+    "miller-rabin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+      "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+      "requires": {
+        "bn.js": "4.11.8",
+        "brorand": "1.1.0"
+      }
+    },
+    "mime": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+      "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
+    },
+    "mime-db": {
+      "version": "1.30.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
+      "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE="
+    },
+    "mime-types": {
+      "version": "2.1.17",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
+      "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
+      "requires": {
+        "mime-db": "1.30.0"
+      }
+    },
+    "minimalistic-assert": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz",
+      "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M="
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "requires": {
+        "brace-expansion": "1.1.8"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "module-deps": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz",
+      "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=",
+      "requires": {
+        "JSONStream": "1.3.1",
+        "browser-resolve": "1.11.2",
+        "cached-path-relative": "1.0.1",
+        "concat-stream": "1.5.2",
+        "defined": "1.0.0",
+        "detective": "4.7.0",
+        "duplexer2": "0.1.4",
+        "inherits": "2.0.3",
+        "parents": "1.0.1",
+        "readable-stream": "2.3.3",
+        "resolve": "1.5.0",
+        "stream-combiner2": "1.1.1",
+        "subarg": "1.0.0",
+        "through2": "2.0.3",
+        "xtend": "4.0.1"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "nan": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
+      "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=",
+      "optional": true
+    },
+    "negotiator": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+      "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
+    },
+    "normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+      "requires": {
+        "hosted-git-info": "2.5.0",
+        "is-builtin-module": "1.0.0",
+        "semver": "5.4.1",
+        "validate-npm-package-license": "3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "requires": {
+        "remove-trailing-separator": "1.1.0"
+      }
+    },
+    "npm-run-all": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.2.tgz",
+      "integrity": "sha512-Z2aRlajMK4SQ8u19ZA75NZZu7wupfCNQWdYosIi8S6FgBdGf/8Y6Hgyjdc8zU2cYmIRVCx1nM80tJPkdEd+UYg==",
+      "requires": {
+        "ansi-styles": "3.2.0",
+        "chalk": "2.3.0",
+        "cross-spawn": "5.1.0",
+        "memorystream": "0.3.1",
+        "minimatch": "3.0.4",
+        "ps-tree": "1.1.0",
+        "read-pkg": "3.0.0",
+        "shell-quote": "1.6.1",
+        "string.prototype.padend": "3.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
+          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
+          "requires": {
+            "color-convert": "1.9.1"
+          }
+        },
+        "chalk": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
+          "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==",
+          "requires": {
+            "ansi-styles": "3.2.0",
+            "escape-string-regexp": "1.0.5",
+            "supports-color": "4.5.0"
+          }
+        },
+        "load-json-file": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+          "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=",
+          "requires": {
+            "graceful-fs": "4.1.11",
+            "parse-json": "4.0.0",
+            "pify": "3.0.0",
+            "strip-bom": "3.0.0"
+          }
+        },
+        "parse-json": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+          "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+          "requires": {
+            "error-ex": "1.3.1",
+            "json-parse-better-errors": "1.0.1"
+          }
+        },
+        "path-type": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+          "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+          "requires": {
+            "pify": "3.0.0"
+          }
+        },
+        "pify": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+          "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
+        },
+        "read-pkg": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+          "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+          "requires": {
+            "load-json-file": "4.0.0",
+            "normalize-package-data": "2.4.0",
+            "path-type": "3.0.0"
+          }
+        },
+        "strip-bom": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+          "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM="
+        },
+        "supports-color": {
+          "version": "4.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
+          "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
+          "requires": {
+            "has-flag": "2.0.0"
+          }
+        }
+      }
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+    },
+    "object-keys": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz",
+      "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0="
+    },
+    "object.omit": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
+      "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
+      "requires": {
+        "for-own": "0.1.5",
+        "is-extendable": "0.1.1"
+      }
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "on-headers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
+      "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "requires": {
+        "wrappy": "1.0.2"
+      }
+    },
+    "opn": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz",
+      "integrity": "sha1-ttmec5n3jWXDuq/+8fsojpuFJDo=",
+      "requires": {
+        "object-assign": "4.1.1"
+      }
+    },
+    "options": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
+      "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8="
+    },
+    "os-browserify": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+      "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+    },
+    "outpipe": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz",
+      "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=",
+      "requires": {
+        "shell-quote": "1.6.1"
+      }
+    },
+    "pad-left": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pad-left/-/pad-left-2.1.0.tgz",
+      "integrity": "sha1-FuajstRKjhOMsIOMx8tAOk/J6ZQ=",
+      "requires": {
+        "repeat-string": "1.6.1"
+      }
+    },
+    "pad-right": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz",
+      "integrity": "sha1-b7ySQEXSRPKiokRQMGDTv8YAl3Q=",
+      "requires": {
+        "repeat-string": "1.6.1"
+      }
+    },
+    "pako": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
+      "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg=="
+    },
+    "parents": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz",
+      "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=",
+      "requires": {
+        "path-platform": "0.11.15"
+      }
+    },
+    "parse-asn1": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz",
+      "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=",
+      "requires": {
+        "asn1.js": "4.9.2",
+        "browserify-aes": "1.1.1",
+        "create-hash": "1.1.3",
+        "evp_bytestokey": "1.0.3",
+        "pbkdf2": "3.0.14"
+      }
+    },
+    "parse-glob": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
+      "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+      "requires": {
+        "glob-base": "0.3.0",
+        "is-dotfile": "1.0.3",
+        "is-extglob": "1.0.0",
+        "is-glob": "2.0.1"
+      }
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "requires": {
+        "error-ex": "1.3.1"
+      }
+    },
+    "parse-ms": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz",
+      "integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0="
+    },
+    "parseurl": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+    },
+    "path-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
+      "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo="
+    },
+    "path-exists": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+      "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+      "requires": {
+        "pinkie-promise": "2.0.1"
+      }
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "path-parse": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz",
+      "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME="
+    },
+    "path-platform": {
+      "version": "0.11.15",
+      "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz",
+      "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I="
+    },
+    "path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+    },
+    "path-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+      "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1"
+      }
+    },
+    "pause-stream": {
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
+      "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=",
+      "requires": {
+        "through": "2.3.8"
+      }
+    },
+    "pbkdf2": {
+      "version": "3.0.14",
+      "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz",
+      "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==",
+      "requires": {
+        "create-hash": "1.1.3",
+        "create-hmac": "1.1.6",
+        "ripemd160": "2.0.1",
+        "safe-buffer": "5.1.1",
+        "sha.js": "2.4.9"
+      }
+    },
+    "pem": {
+      "version": "1.12.3",
+      "resolved": "https://registry.npmjs.org/pem/-/pem-1.12.3.tgz",
+      "integrity": "sha512-hT7GwvQL35+0iqgYUl8vn5I5pAVR0HcJas07TXL8bNaR4c5kAFRquk4ZqQk1F9YMcQOr6WjGdY5OnDC0RBnzig==",
+      "requires": {
+        "md5": "2.2.1",
+        "os-tmpdir": "1.0.2",
+        "safe-buffer": "5.1.1",
+        "which": "1.3.0"
+      }
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "requires": {
+        "pinkie": "2.0.4"
+      }
+    },
+    "plur": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz",
+      "integrity": "sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY="
+    },
+    "preserve": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
+      "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks="
+    },
+    "prettier-bytes": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz",
+      "integrity": "sha1-mUsCqkb2mcULYle1+qp/4lV+YtY="
+    },
+    "pretty-ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz",
+      "integrity": "sha1-QlfCVt8/sLRR1q/6qwIYhBJpgdw=",
+      "requires": {
+        "is-finite": "1.0.2",
+        "parse-ms": "1.0.1",
+        "plur": "1.0.0"
+      }
+    },
+    "private": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
+      "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg=="
+    },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
+    },
+    "process-nextick-args": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+      "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
+    },
+    "proxy-addr": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz",
+      "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=",
+      "requires": {
+        "forwarded": "0.1.2",
+        "ipaddr.js": "1.5.2"
+      }
+    },
+    "ps-tree": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz",
+      "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=",
+      "requires": {
+        "event-stream": "3.3.4"
+      }
+    },
+    "pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
+    },
+    "public-encrypt": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz",
+      "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=",
+      "requires": {
+        "bn.js": "4.11.8",
+        "browserify-rsa": "4.0.1",
+        "create-hash": "1.1.3",
+        "parse-asn1": "5.1.0",
+        "randombytes": "2.0.5"
+      }
+    },
+    "punycode": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+      "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+    },
+    "qs": {
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
+      "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
+    },
+    "query-string": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
+      "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
+      "requires": {
+        "object-assign": "4.1.1",
+        "strict-uri-encode": "1.1.0"
+      }
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
+    },
+    "querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+      "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
+    },
+    "randomatic": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
+      "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==",
+      "requires": {
+        "is-number": "3.0.0",
+        "kind-of": "4.0.0"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "requires": {
+            "kind-of": "3.2.2"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "3.2.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+              "requires": {
+                "is-buffer": "1.1.6"
+              }
+            }
+          }
+        },
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "requires": {
+            "is-buffer": "1.1.6"
+          }
+        }
+      }
+    },
+    "randombytes": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz",
+      "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==",
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "randomfill": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz",
+      "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==",
+      "requires": {
+        "randombytes": "2.0.5",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+    },
+    "raw-body": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
+      "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
+      "requires": {
+        "bytes": "3.0.0",
+        "http-errors": "1.6.2",
+        "iconv-lite": "0.4.19",
+        "unpipe": "1.0.0"
+      }
+    },
+    "read-only-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz",
+      "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=",
+      "requires": {
+        "readable-stream": "2.3.3"
+      }
+    },
+    "read-pkg": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+      "requires": {
+        "load-json-file": "1.1.0",
+        "normalize-package-data": "2.4.0",
+        "path-type": "1.1.0"
+      }
+    },
+    "read-pkg-up": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+      "requires": {
+        "find-up": "1.1.2",
+        "read-pkg": "1.1.0"
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
+      "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
+      "requires": {
+        "core-util-is": "1.0.2",
+        "inherits": "2.0.3",
+        "isarray": "1.0.0",
+        "process-nextick-args": "1.0.7",
+        "safe-buffer": "5.1.1",
+        "string_decoder": "1.0.3",
+        "util-deprecate": "1.0.2"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+        }
+      }
+    },
+    "readdirp": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
+      "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "minimatch": "3.0.4",
+        "readable-stream": "2.3.3",
+        "set-immediate-shim": "1.0.1"
+      }
+    },
+    "redent": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+      "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+      "requires": {
+        "indent-string": "2.1.0",
+        "strip-indent": "1.0.1"
+      }
+    },
+    "regenerator-runtime": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz",
+      "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A=="
+    },
+    "regex-cache": {
+      "version": "0.4.4",
+      "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
+      "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
+      "requires": {
+        "is-equal-shallow": "0.1.3"
+      }
+    },
+    "reload-css": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/reload-css/-/reload-css-1.0.2.tgz",
+      "integrity": "sha1-avsRFi4jFP7M2tbcX96CH9cxgzE=",
+      "requires": {
+        "query-string": "4.3.4"
+      }
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
+    },
+    "repeat-element": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz",
+      "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo="
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "requires": {
+        "is-finite": "1.0.2"
+      }
+    },
+    "resolve": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
+      "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==",
+      "requires": {
+        "path-parse": "1.0.5"
+      }
+    },
+    "resp-modifier": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz",
+      "integrity": "sha1-sSTeXE+6/LpUH0j/pzlw9KpFa08=",
+      "requires": {
+        "debug": "2.6.9",
+        "minimatch": "3.0.4"
+      }
+    },
+    "right-now": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/right-now/-/right-now-1.0.0.tgz",
+      "integrity": "sha1-bolgne69fc2vja7Mmuo5z1haCRg="
+    },
+    "ripemd160": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz",
+      "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=",
+      "requires": {
+        "hash-base": "2.0.2",
+        "inherits": "2.0.3"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+      "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
+    },
+    "semver": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
+      "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
+    },
+    "send": {
+      "version": "0.16.1",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz",
+      "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==",
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "1.1.1",
+        "destroy": "1.0.4",
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "etag": "1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "1.6.2",
+        "mime": "1.4.1",
+        "ms": "2.0.0",
+        "on-finished": "2.3.0",
+        "range-parser": "1.2.0",
+        "statuses": "1.3.1"
+      }
+    },
+    "serve-static": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz",
+      "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==",
+      "requires": {
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "parseurl": "1.3.2",
+        "send": "0.16.1"
+      }
+    },
+    "set-immediate-shim": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
+    },
+    "setprototypeof": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
+      "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
+    },
+    "sha.js": {
+      "version": "2.4.9",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz",
+      "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==",
+      "requires": {
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "shasum": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz",
+      "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=",
+      "requires": {
+        "json-stable-stringify": "0.0.1",
+        "sha.js": "2.4.9"
+      }
+    },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "requires": {
+        "shebang-regex": "1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
+    },
+    "shell-quote": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
+      "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
+      "requires": {
+        "array-filter": "0.0.1",
+        "array-map": "0.0.0",
+        "array-reduce": "0.0.0",
+        "jsonify": "0.0.0"
+      }
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
+    },
+    "simple-html-index": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/simple-html-index/-/simple-html-index-1.5.0.tgz",
+      "integrity": "sha1-LJPurrrAAdihNfwAIr1K3o9YmW8=",
+      "requires": {
+        "from2-string": "1.1.0"
+      }
+    },
+    "slash": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+      "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU="
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+    },
+    "source-map-support": {
+      "version": "0.4.18",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+      "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+      "requires": {
+        "source-map": "0.5.7"
+      }
+    },
+    "spdx-correct": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz",
+      "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=",
+      "requires": {
+        "spdx-license-ids": "1.2.2"
+      }
+    },
+    "spdx-expression-parse": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz",
+      "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw="
+    },
+    "spdx-license-ids": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz",
+      "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc="
+    },
+    "split": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
+      "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
+      "requires": {
+        "through": "2.3.8"
+      }
+    },
+    "split2": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-0.2.1.tgz",
+      "integrity": "sha1-At2smtwD7Au3jBKC7Aecpuha6QA=",
+      "requires": {
+        "through2": "0.6.5"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "1.0.34",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+          "requires": {
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "0.0.1",
+            "string_decoder": "0.10.31"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        },
+        "through2": {
+          "version": "0.6.5",
+          "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz",
+          "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=",
+          "requires": {
+            "readable-stream": "1.0.34",
+            "xtend": "4.0.1"
+          }
+        }
+      }
+    },
+    "stacked": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/stacked/-/stacked-1.1.1.tgz",
+      "integrity": "sha1-LH+jjMfjejQRp3zY55LeRI+faXU="
+    },
+    "statuses": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+      "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
+    },
+    "stdout-stream": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
+      "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=",
+      "requires": {
+        "readable-stream": "2.3.3"
+      }
+    },
+    "stream-browserify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
+      "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "stream-combiner": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
+      "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=",
+      "requires": {
+        "duplexer": "0.1.1"
+      }
+    },
+    "stream-combiner2": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz",
+      "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=",
+      "requires": {
+        "duplexer2": "0.1.4",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "stream-http": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz",
+      "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==",
+      "requires": {
+        "builtin-status-codes": "3.0.0",
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3",
+        "to-arraybuffer": "1.0.1",
+        "xtend": "4.0.1"
+      }
+    },
+    "stream-splicer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz",
+      "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=",
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "strict-uri-encode": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
+      "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
+    },
+    "string.prototype.padend": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz",
+      "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=",
+      "requires": {
+        "define-properties": "1.1.2",
+        "es-abstract": "1.10.0",
+        "function-bind": "1.1.1"
+      }
+    },
+    "string_decoder": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+      "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "strip-ansi": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+      "requires": {
+        "ansi-regex": "2.1.1"
+      }
+    },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+      "requires": {
+        "is-utf8": "0.2.1"
+      }
+    },
+    "strip-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+      "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+      "requires": {
+        "get-stdin": "4.0.1"
+      }
+    },
+    "subarg": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz",
+      "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=",
+      "requires": {
+        "minimist": "1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+        }
+      }
+    },
+    "supports-color": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+      "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+    },
+    "syntax-error": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz",
+      "integrity": "sha1-HtkmbE1AvnXcVb+bsct3Biu5bKE=",
+      "requires": {
+        "acorn": "4.0.13"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "4.0.13",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+          "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c="
+        }
+      }
+    },
+    "term-color": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/term-color/-/term-color-1.0.1.tgz",
+      "integrity": "sha1-OOGSVTpHPjXkFgT/UZmEa/gRejo=",
+      "requires": {
+        "ansi-styles": "2.0.1",
+        "supports-color": "1.3.1"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.0.1.tgz",
+          "integrity": "sha1-sDP1f5Pi0oreuLwRE4+hPaD9IKM="
+        },
+        "supports-color": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz",
+          "integrity": "sha1-FXWN8J2P87SswwdTn6vicJXhBC0="
+        }
+      }
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+    },
+    "through2": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+      "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+      "requires": {
+        "readable-stream": "2.3.3",
+        "xtend": "4.0.1"
+      }
+    },
+    "timers-browserify": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz",
+      "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=",
+      "requires": {
+        "process": "0.11.10"
+      }
+    },
+    "to-arraybuffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+      "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M="
+    },
+    "to-fast-properties": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+      "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc="
+    },
+    "trim-newlines": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM="
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
+    },
+    "tty-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+      "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
+    },
+    "type-is": {
+      "version": "1.6.15",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz",
+      "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=",
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "2.1.17"
+      }
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+    },
+    "ultron": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
+      "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po="
+    },
+    "umd": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz",
+      "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4="
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
+        }
+      }
+    },
+    "url-trim": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/url-trim/-/url-trim-1.0.0.tgz",
+      "integrity": "sha1-QAV+LxZLiOXaynJp2kfm0d2Detw="
+    },
+    "util": {
+      "version": "0.10.3",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+      "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+      "requires": {
+        "inherits": "2.0.1"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+          "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
+        }
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
+      "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=",
+      "requires": {
+        "spdx-correct": "1.0.2",
+        "spdx-expression-parse": "1.0.4"
+      }
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+    },
+    "vm-browserify": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
+      "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+      "requires": {
+        "indexof": "0.0.1"
+      }
+    },
+    "watchify": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.9.0.tgz",
+      "integrity": "sha1-8HX9LoqGrN6Eztum5cKgvt1SPZ4=",
+      "requires": {
+        "anymatch": "1.3.2",
+        "browserify": "14.5.0",
+        "chokidar": "1.7.0",
+        "defined": "1.0.0",
+        "outpipe": "1.1.1",
+        "through2": "2.0.3",
+        "xtend": "4.0.1"
+      }
+    },
+    "watchify-middleware": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/watchify-middleware/-/watchify-middleware-1.6.0.tgz",
+      "integrity": "sha1-bbbijwJ53hyhIJrk8afwY3RYd8Q=",
+      "requires": {
+        "concat-stream": "1.5.2",
+        "debounce": "1.1.0",
+        "events": "1.1.1",
+        "object-assign": "4.1.1",
+        "strip-ansi": "3.0.1",
+        "watchify": "3.9.0"
+      }
+    },
+    "which": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",
+      "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==",
+      "requires": {
+        "isexe": "2.0.0"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    },
+    "ws": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz",
+      "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==",
+      "requires": {
+        "options": "0.0.6",
+        "ultron": "1.0.2"
+      }
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
+    },
+    "yallist": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+      "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
+    }
+  }
+}

+ 20 - 0
examples/aws-uppy-server/package.json

@@ -0,0 +1,20 @@
+{
+  "private": true,
+  "name": "uppy-server-aws-example",
+  "scripts": {
+    "copy": "cp ../../dist/uppy.min.css .",
+    "start:client": "budo main.js:bundle.js -- -t babelify -g aliasify",
+    "start:server": "node server.js",
+    "start": "npm-run-all --serial copy --parallel start:*"
+  },
+  "aliasify": "./aliasify.js",
+  "dependencies": {
+    "aliasify": "^2.1.0",
+    "babelify": "^7.3.0",
+    "body-parser": "^1.18.2",
+    "budo": "^10.0.4",
+    "cors": "^2.8.4",
+    "express": "^4.16.2",
+    "npm-run-all": "^4.1.2"
+  }
+}

+ 15 - 0
examples/aws-uppy-server/readme.md

@@ -0,0 +1,15 @@
+# Uppy + AWS S3 Example
+
+This example uses uppy-server with a custom AWS S3 configuration.
+Files are uploaded to a randomly named directory inside the `whatever/` directory in a bucket.
+
+## Run it
+
+First set up the `UPPYSERVER_AWS_KEY`, `UPPYSERVER_AWS_SECRET`, `UPPYSERVER_AWS_REGION`, and `UPPYSERVER_AWS_BUCKET` environment variables for uppy-server.
+
+Move into this directory, then:
+
+```bash
+npm install
+npm start
+```

+ 23 - 0
examples/aws-uppy-server/server.js

@@ -0,0 +1,23 @@
+const uppy = require('uppy-server')
+const app = require('express')()
+
+app.use(require('cors')())
+app.use(require('body-parser').json())
+
+app.use(uppy.app({
+  providerOptions: {
+    s3: {
+      getKey: (req, filename) =>
+        `whatever/${Math.random().toString(32).slice(2)}/${filename}`,
+      key: process.env.UPPYSERVER_AWS_KEY,
+      secret: process.env.UPPYSERVER_AWS_SECRET,
+      bucket: process.env.UPPYSERVER_AWS_BUCKET,
+      region: process.env.UPPYSERVER_AWS_REGION
+    }
+  },
+  server: { host: 'localhost:3020' }
+}))
+
+app.listen(3020, () => {
+  console.log('listening on port 3020')
+})

+ 4 - 3
examples/bundled-example/index.html

@@ -22,11 +22,12 @@
       <h1>Uppy is here</h1>
 
       <form id="upload-form" action="/">
-        <button type="button" id="uppyModalOpener">Select Files</button>
+        <button type="button" id="pick-files">Pick Files</button>
         <br><br>
+        True ? <input type="checkbox" name="check_test" value="1" checked><br>
+        Something: <input type="text" name="yo" value="1">
         <input type="hidden" name="bla" value="12333">
-        <input type="text" name="yo" value="1">
-        <button type="submit">Submit</button>
+        <button type="submit">Read to Submit</button>
       </form>
     </main>
 

+ 1 - 1
examples/cdn-example/index.html

@@ -26,7 +26,7 @@
 
       uppy.run()
 
-      uppy.on('core:success', (fileCount) => {
+      uppy.on('success', (fileCount) => {
         console.log(`${fileCount} files uploaded`)
       })
 

+ 1 - 0
examples/redux/.gitignore

@@ -0,0 +1 @@
+uppy.min.css

+ 7 - 0
examples/redux/aliasify.js

@@ -0,0 +1,7 @@
+const path = require('path')
+
+module.exports = {
+  replacements: {
+    '^uppy/lib/(.*?)$': path.join(__dirname, '../../src/$1')
+  }
+}

+ 27 - 0
examples/redux/index.html

@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Uppy example: Redux</title>
+  </head>
+  <body>
+    <main id="app">
+      <h1>A counter</h1>
+      <div>
+        <p>
+          Clicked: <span id="value">0</span> times
+          <button id="increment">+</button>
+          <button id="decrement">-</button>
+          <button id="incrementIfOdd">Increment if odd</button>
+          <button id="incrementAsync">Increment async</button>
+        </p>
+      </div>
+      <h1>An Uppy</h1>
+      <div id="uppy"></div>
+    </main>
+
+    <link href="uppy.min.css" rel="stylesheet">
+    <script src="bundle.js"></script>
+  </body>
+</html>

+ 84 - 0
examples/redux/main.js

@@ -0,0 +1,84 @@
+const { createStore, compose, combineReducers, applyMiddleware } = require('redux')
+const logger = require('redux-logger').default
+const Uppy = require('uppy/lib/core')
+const uppyReduxStore = require('uppy/lib/store/ReduxStore')
+const Dashboard = require('uppy/lib/plugins/Dashboard')
+const Tus = require('uppy/lib/plugins/Tus')
+
+function counter (state = 0, action) {
+  switch (action.type) {
+    case 'INCREMENT':
+      return state + 1
+    case 'DECREMENT':
+      return state - 1
+    default:
+      return state
+  }
+}
+
+const reducer = combineReducers({
+  counter: counter,
+  // You don't have to use the `uppy` key. But if you don't,
+  // you need to provide a custom `selector` to the `uppyReduxStore` call below.
+  uppy: uppyReduxStore.reducer
+})
+
+let enhancer = applyMiddleware(
+  uppyReduxStore.middleware(),
+  logger
+)
+if (window.__REDUX_DEVTOOLS_EXTENSION__) {
+  enhancer = compose(enhancer, window.__REDUX_DEVTOOLS_EXTENSION__())
+}
+
+const store = createStore(reducer, enhancer)
+
+// Counter example from https://github.com/reactjs/redux/blob/master/examples/counter-vanilla/index.html
+const valueEl = document.querySelector('#value')
+
+function getCounter () { return store.getState().counter }
+function render () {
+  valueEl.innerHTML = getCounter().toString()
+}
+render()
+store.subscribe(render)
+
+document.querySelector('#increment').onclick = () => {
+  store.dispatch({ type: 'INCREMENT' })
+}
+document.querySelector('#decrement').onclick = () => {
+  store.dispatch({ type: 'DECREMENT' })
+}
+document.querySelector('#incrementIfOdd').onclick = () => {
+  if (getCounter() % 2 !== 0) {
+    store.dispatch({ type: 'INCREMENT' })
+  }
+}
+document.querySelector('#incrementAsync').onclick = () => {
+  setTimeout(() => store.dispatch({ type: 'INCREMENT' }), 1000)
+}
+
+// Uppy using the same store
+const uppy = Uppy({
+  autoProceed: false,
+  id: 'redux',
+  store: uppyReduxStore({ store: store }),
+  // If we had placed our `reducer` elsewhere in Redux, eg. under an `uppy` key in the state for a profile page,
+  // we'd do something like:
+  //
+  // store: uppyReduxStore({
+  //   store: store,
+  //   id: 'avatar',
+  //   selector: state => state.pages.profile.uppy
+  // }),
+  debug: true
+})
+uppy.use(Dashboard, {
+  target: '#app',
+  inline: true,
+  maxWidth: 400
+})
+uppy.use(Tus, { endpoint: 'https://master.tus.io/' })
+uppy.run()
+
+window.uppy = uppy

+ 3092 - 0
examples/redux/package-lock.json

@@ -0,0 +1,3092 @@
+{
+  "name": "uppy-redux-example",
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "JSONStream": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
+      "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=",
+      "dev": true,
+      "requires": {
+        "jsonparse": "1.3.1",
+        "through": "2.3.8"
+      }
+    },
+    "acorn": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz",
+      "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==",
+      "dev": true
+    },
+    "aliasify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/aliasify/-/aliasify-2.1.0.tgz",
+      "integrity": "sha1-fDCCW5RQueYYW6J1M+r24gZ9S0I=",
+      "dev": true,
+      "requires": {
+        "browserify-transform-tools": "1.7.0"
+      }
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+      "dev": true
+    },
+    "anymatch": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
+      "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+      "dev": true,
+      "requires": {
+        "micromatch": "2.3.11",
+        "normalize-path": "2.1.1"
+      }
+    },
+    "arr-diff": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
+      "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
+      "dev": true,
+      "requires": {
+        "arr-flatten": "1.1.0"
+      }
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
+      "dev": true
+    },
+    "array-filter": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
+      "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
+      "dev": true
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+      "dev": true
+    },
+    "array-map": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
+      "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
+      "dev": true
+    },
+    "array-reduce": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
+      "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
+      "dev": true
+    },
+    "array-unique": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+      "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+      "dev": true
+    },
+    "asn1.js": {
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.2.tgz",
+      "integrity": "sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg==",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.0"
+      }
+    },
+    "assert": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+      "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
+      "dev": true,
+      "requires": {
+        "util": "0.10.3"
+      }
+    },
+    "astw": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz",
+      "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=",
+      "dev": true,
+      "requires": {
+        "acorn": "4.0.13"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "4.0.13",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+          "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+          "dev": true
+        }
+      }
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+      "dev": true
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "dev": true,
+      "requires": {
+        "chalk": "1.1.3",
+        "esutils": "2.0.2",
+        "js-tokens": "3.0.2"
+      }
+    },
+    "babel-core": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz",
+      "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "babel-generator": "6.26.0",
+        "babel-helpers": "6.24.1",
+        "babel-messages": "6.23.0",
+        "babel-register": "6.26.0",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "convert-source-map": "1.5.0",
+        "debug": "2.6.9",
+        "json5": "0.5.1",
+        "lodash": "4.17.4",
+        "minimatch": "3.0.4",
+        "path-is-absolute": "1.0.1",
+        "private": "0.1.8",
+        "slash": "1.0.0",
+        "source-map": "0.5.7"
+      }
+    },
+    "babel-generator": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz",
+      "integrity": "sha1-rBriAHC3n248odMmlhMFN3TyDcU=",
+      "dev": true,
+      "requires": {
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "detect-indent": "4.0.0",
+        "jsesc": "1.3.0",
+        "lodash": "4.17.4",
+        "source-map": "0.5.7",
+        "trim-right": "1.0.1"
+      }
+    },
+    "babel-helpers": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz",
+      "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0"
+      }
+    },
+    "babel-messages": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+      "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-register": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz",
+      "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+      "dev": true,
+      "requires": {
+        "babel-core": "6.26.0",
+        "babel-runtime": "6.26.0",
+        "core-js": "2.5.1",
+        "home-or-tmp": "2.0.0",
+        "lodash": "4.17.4",
+        "mkdirp": "0.5.1",
+        "source-map-support": "0.4.18"
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "dev": true,
+      "requires": {
+        "core-js": "2.5.1",
+        "regenerator-runtime": "0.11.0"
+      }
+    },
+    "babel-template": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+      "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-traverse": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+      "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "debug": "2.6.9",
+        "globals": "9.18.0",
+        "invariant": "2.2.2",
+        "lodash": "4.17.4"
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "esutils": "2.0.2",
+        "lodash": "4.17.4",
+        "to-fast-properties": "1.0.3"
+      }
+    },
+    "babelify": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/babelify/-/babelify-7.3.0.tgz",
+      "integrity": "sha1-qlau3nBn/XvVSWZu4W3ChQh+iOU=",
+      "dev": true,
+      "requires": {
+        "babel-core": "6.26.0",
+        "object-assign": "4.1.1"
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+      "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "base64-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz",
+      "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==",
+      "dev": true
+    },
+    "binary-extensions": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.10.0.tgz",
+      "integrity": "sha1-muuabF6IY4qtFx4Wf1kAq+JINdA=",
+      "dev": true
+    },
+    "bn.js": {
+      "version": "4.11.8",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+      "dev": true
+    },
+    "bole": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/bole/-/bole-2.0.0.tgz",
+      "integrity": "sha1-2KocaQRnv7T+Ebh0rLLoOH44JhU=",
+      "dev": true,
+      "requires": {
+        "core-util-is": "1.0.2",
+        "individual": "3.0.0",
+        "json-stringify-safe": "5.0.1"
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
+      "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
+      "dev": true,
+      "requires": {
+        "balanced-match": "1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "1.8.5",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
+      "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
+      "dev": true,
+      "requires": {
+        "expand-range": "1.8.2",
+        "preserve": "0.2.0",
+        "repeat-element": "1.1.2"
+      }
+    },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "dev": true
+    },
+    "browser-pack": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.2.tgz",
+      "integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=",
+      "dev": true,
+      "requires": {
+        "JSONStream": "1.3.1",
+        "combine-source-map": "0.7.2",
+        "defined": "1.0.0",
+        "through2": "2.0.3",
+        "umd": "3.0.1"
+      }
+    },
+    "browser-resolve": {
+      "version": "1.11.2",
+      "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.2.tgz",
+      "integrity": "sha1-j/CbCixCFxihBRwmCzLkj0QpOM4=",
+      "dev": true,
+      "requires": {
+        "resolve": "1.1.7"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+          "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=",
+          "dev": true
+        }
+      }
+    },
+    "browserify": {
+      "version": "14.5.0",
+      "resolved": "https://registry.npmjs.org/browserify/-/browserify-14.5.0.tgz",
+      "integrity": "sha512-gKfOsNQv/toWz+60nSPfYzuwSEdzvV2WdxrVPUbPD/qui44rAkB3t3muNtmmGYHqrG56FGwX9SUEQmzNLAeS7g==",
+      "dev": true,
+      "requires": {
+        "JSONStream": "1.3.1",
+        "assert": "1.4.1",
+        "browser-pack": "6.0.2",
+        "browser-resolve": "1.11.2",
+        "browserify-zlib": "0.2.0",
+        "buffer": "5.0.8",
+        "cached-path-relative": "1.0.1",
+        "concat-stream": "1.5.2",
+        "console-browserify": "1.1.0",
+        "constants-browserify": "1.0.0",
+        "crypto-browserify": "3.12.0",
+        "defined": "1.0.0",
+        "deps-sort": "2.0.0",
+        "domain-browser": "1.1.7",
+        "duplexer2": "0.1.4",
+        "events": "1.1.1",
+        "glob": "7.1.2",
+        "has": "1.0.1",
+        "htmlescape": "1.1.1",
+        "https-browserify": "1.0.0",
+        "inherits": "2.0.3",
+        "insert-module-globals": "7.0.1",
+        "labeled-stream-splicer": "2.0.0",
+        "module-deps": "4.1.1",
+        "os-browserify": "0.3.0",
+        "parents": "1.0.1",
+        "path-browserify": "0.0.0",
+        "process": "0.11.10",
+        "punycode": "1.4.1",
+        "querystring-es3": "0.2.1",
+        "read-only-stream": "2.0.0",
+        "readable-stream": "2.3.3",
+        "resolve": "1.5.0",
+        "shasum": "1.0.2",
+        "shell-quote": "1.6.1",
+        "stream-browserify": "2.0.1",
+        "stream-http": "2.7.2",
+        "string_decoder": "1.0.3",
+        "subarg": "1.0.0",
+        "syntax-error": "1.3.0",
+        "through2": "2.0.3",
+        "timers-browserify": "1.4.2",
+        "tty-browserify": "0.0.0",
+        "url": "0.11.0",
+        "util": "0.10.3",
+        "vm-browserify": "0.0.4",
+        "xtend": "4.0.1"
+      }
+    },
+    "browserify-aes": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz",
+      "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==",
+      "dev": true,
+      "requires": {
+        "buffer-xor": "1.0.3",
+        "cipher-base": "1.0.4",
+        "create-hash": "1.1.3",
+        "evp_bytestokey": "1.0.3",
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "browserify-cipher": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz",
+      "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=",
+      "dev": true,
+      "requires": {
+        "browserify-aes": "1.1.1",
+        "browserify-des": "1.0.0",
+        "evp_bytestokey": "1.0.3"
+      }
+    },
+    "browserify-des": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz",
+      "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=",
+      "dev": true,
+      "requires": {
+        "cipher-base": "1.0.4",
+        "des.js": "1.0.0",
+        "inherits": "2.0.3"
+      }
+    },
+    "browserify-rsa": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "randombytes": "2.0.5"
+      }
+    },
+    "browserify-sign": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+      "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "browserify-rsa": "4.0.1",
+        "create-hash": "1.1.3",
+        "create-hmac": "1.1.6",
+        "elliptic": "6.4.0",
+        "inherits": "2.0.3",
+        "parse-asn1": "5.1.0"
+      }
+    },
+    "browserify-transform-tools": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/browserify-transform-tools/-/browserify-transform-tools-1.7.0.tgz",
+      "integrity": "sha1-g+J3Ih9jJZvtLn6yooOpcKUB9MQ=",
+      "dev": true,
+      "requires": {
+        "falafel": "2.1.0",
+        "through": "2.3.8"
+      }
+    },
+    "browserify-zlib": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+      "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+      "dev": true,
+      "requires": {
+        "pako": "1.0.6"
+      }
+    },
+    "budo": {
+      "version": "10.0.4",
+      "resolved": "https://registry.npmjs.org/budo/-/budo-10.0.4.tgz",
+      "integrity": "sha512-fJcz4EGwMno+e2xrD7QwclvZ77InghizqG8GGqMIYzanMUuWTOSrio+SUKpQRxLoFiSLiP+lceFcNbPseeew9A==",
+      "dev": true,
+      "requires": {
+        "bole": "2.0.0",
+        "browserify": "14.5.0",
+        "chokidar": "1.7.0",
+        "connect-pushstate": "1.1.0",
+        "escape-html": "1.0.3",
+        "events": "1.1.1",
+        "garnish": "5.2.0",
+        "get-ports": "1.0.3",
+        "inject-lr-script": "2.1.0",
+        "internal-ip": "1.2.0",
+        "micromatch": "2.3.11",
+        "on-finished": "2.3.0",
+        "on-headers": "1.0.1",
+        "once": "1.4.0",
+        "opn": "3.0.3",
+        "path-is-absolute": "1.0.1",
+        "pem": "1.12.3",
+        "reload-css": "1.0.2",
+        "resolve": "1.5.0",
+        "serve-static": "1.13.1",
+        "simple-html-index": "1.5.0",
+        "stacked": "1.1.1",
+        "stdout-stream": "1.4.0",
+        "strip-ansi": "3.0.1",
+        "subarg": "1.0.0",
+        "term-color": "1.0.1",
+        "url-trim": "1.0.0",
+        "watchify-middleware": "1.6.0",
+        "ws": "1.1.5",
+        "xtend": "4.0.1"
+      }
+    },
+    "buffer": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.0.8.tgz",
+      "integrity": "sha512-xXvjQhVNz50v2nPeoOsNqWCLGfiv4ji/gXZM28jnVwdLJxH4mFyqgqCKfaK9zf1KUbG6zTkjLOy7ou+jSMarGA==",
+      "dev": true,
+      "requires": {
+        "base64-js": "1.2.1",
+        "ieee754": "1.1.8"
+      }
+    },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "dev": true
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+      "dev": true
+    },
+    "builtin-status-codes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+      "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
+      "dev": true
+    },
+    "cached-path-relative": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz",
+      "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+      "dev": true
+    },
+    "camelcase-keys": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+      "dev": true,
+      "requires": {
+        "camelcase": "2.1.1",
+        "map-obj": "1.0.1"
+      }
+    },
+    "chalk": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+      "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "2.2.1",
+        "escape-string-regexp": "1.0.5",
+        "has-ansi": "2.0.0",
+        "strip-ansi": "3.0.1",
+        "supports-color": "2.0.0"
+      }
+    },
+    "charenc": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+      "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=",
+      "dev": true
+    },
+    "chokidar": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
+      "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+      "dev": true,
+      "requires": {
+        "anymatch": "1.3.2",
+        "async-each": "1.0.1",
+        "glob-parent": "2.0.0",
+        "inherits": "2.0.3",
+        "is-binary-path": "1.0.1",
+        "is-glob": "2.0.1",
+        "path-is-absolute": "1.0.1",
+        "readdirp": "2.1.0"
+      }
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "combine-source-map": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz",
+      "integrity": "sha1-CHAxKFazB6h8xKxIbzqaYq7MwJ4=",
+      "dev": true,
+      "requires": {
+        "convert-source-map": "1.1.3",
+        "inline-source-map": "0.6.2",
+        "lodash.memoize": "3.0.4",
+        "source-map": "0.5.7"
+      },
+      "dependencies": {
+        "convert-source-map": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz",
+          "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=",
+          "dev": true
+        }
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "concat-stream": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz",
+      "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.0.6",
+        "typedarray": "0.0.6"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.0.6",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+          "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+          "dev": true,
+          "requires": {
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "1.0.0",
+            "process-nextick-args": "1.0.7",
+            "string_decoder": "0.10.31",
+            "util-deprecate": "1.0.2"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+          "dev": true
+        }
+      }
+    },
+    "connect-pushstate": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/connect-pushstate/-/connect-pushstate-1.1.0.tgz",
+      "integrity": "sha1-vKsiQnHEOWBKD7D2FMCl9WPojiQ=",
+      "dev": true
+    },
+    "console-browserify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+      "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
+      "dev": true,
+      "requires": {
+        "date-now": "0.1.4"
+      }
+    },
+    "constants-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+      "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz",
+      "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=",
+      "dev": true
+    },
+    "core-js": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz",
+      "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=",
+      "dev": true
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+      "dev": true
+    },
+    "create-ecdh": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz",
+      "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "elliptic": "6.4.0"
+      }
+    },
+    "create-hash": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz",
+      "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=",
+      "dev": true,
+      "requires": {
+        "cipher-base": "1.0.4",
+        "inherits": "2.0.3",
+        "ripemd160": "2.0.1",
+        "sha.js": "2.4.9"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz",
+      "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=",
+      "dev": true,
+      "requires": {
+        "cipher-base": "1.0.4",
+        "create-hash": "1.1.3",
+        "inherits": "2.0.3",
+        "ripemd160": "2.0.1",
+        "safe-buffer": "5.1.1",
+        "sha.js": "2.4.9"
+      }
+    },
+    "crypt": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+      "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=",
+      "dev": true
+    },
+    "crypto-browserify": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+      "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+      "dev": true,
+      "requires": {
+        "browserify-cipher": "1.0.0",
+        "browserify-sign": "4.0.4",
+        "create-ecdh": "4.0.0",
+        "create-hash": "1.1.3",
+        "create-hmac": "1.1.6",
+        "diffie-hellman": "5.0.2",
+        "inherits": "2.0.3",
+        "pbkdf2": "3.0.14",
+        "public-encrypt": "4.0.0",
+        "randombytes": "2.0.5",
+        "randomfill": "1.0.3"
+      }
+    },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "dev": true,
+      "requires": {
+        "array-find-index": "1.0.2"
+      }
+    },
+    "date-now": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+      "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
+      "dev": true
+    },
+    "debounce": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz",
+      "integrity": "sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ==",
+      "dev": true
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dev": true,
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "deep-diff": {
+      "version": "0.3.8",
+      "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
+      "integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ="
+    },
+    "defined": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+      "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
+      "dev": true
+    },
+    "depd": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
+      "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=",
+      "dev": true
+    },
+    "deps-sort": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz",
+      "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=",
+      "dev": true,
+      "requires": {
+        "JSONStream": "1.3.1",
+        "shasum": "1.0.2",
+        "subarg": "1.0.0",
+        "through2": "2.0.3"
+      }
+    },
+    "des.js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+      "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.0"
+      }
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
+      "dev": true
+    },
+    "detect-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+      "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+      "dev": true,
+      "requires": {
+        "repeating": "2.0.1"
+      }
+    },
+    "detective": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/detective/-/detective-4.5.0.tgz",
+      "integrity": "sha1-blqMaybmx6JUsca210kNmOyR7dE=",
+      "dev": true,
+      "requires": {
+        "acorn": "4.0.13",
+        "defined": "1.0.0"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "4.0.13",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+          "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+          "dev": true
+        }
+      }
+    },
+    "diffie-hellman": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz",
+      "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "miller-rabin": "4.0.1",
+        "randombytes": "2.0.5"
+      }
+    },
+    "domain-browser": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz",
+      "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=",
+      "dev": true
+    },
+    "duplexer2": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+      "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.3"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+      "dev": true
+    },
+    "elliptic": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz",
+      "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "brorand": "1.1.0",
+        "hash.js": "1.1.3",
+        "hmac-drbg": "1.0.1",
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.0",
+        "minimalistic-crypto-utils": "1.0.1"
+      }
+    },
+    "encodeurl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
+      "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=",
+      "dev": true
+    },
+    "error-ex": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
+      "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "0.2.1"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+      "dev": true
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+      "dev": true
+    },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
+      "dev": true
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "dev": true,
+      "requires": {
+        "md5.js": "1.3.4",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "expand-brackets": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
+      "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
+      "dev": true,
+      "requires": {
+        "is-posix-bracket": "0.1.1"
+      }
+    },
+    "expand-range": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
+      "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
+      "dev": true,
+      "requires": {
+        "fill-range": "2.2.3"
+      }
+    },
+    "extglob": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
+      "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "1.0.0"
+      }
+    },
+    "falafel": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz",
+      "integrity": "sha1-lrsXdh2rqU9G0AFzizzt86Z/4Gw=",
+      "dev": true,
+      "requires": {
+        "acorn": "5.2.1",
+        "foreach": "2.0.5",
+        "isarray": "0.0.1",
+        "object-keys": "1.0.11"
+      }
+    },
+    "filename-regex": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
+      "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=",
+      "dev": true
+    },
+    "fill-range": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",
+      "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=",
+      "dev": true,
+      "requires": {
+        "is-number": "2.1.0",
+        "isobject": "2.1.0",
+        "randomatic": "1.1.7",
+        "repeat-element": "1.1.2",
+        "repeat-string": "1.6.1"
+      }
+    },
+    "find-up": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+      "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+      "dev": true,
+      "requires": {
+        "path-exists": "2.1.0",
+        "pinkie-promise": "2.0.1"
+      }
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+      "dev": true
+    },
+    "for-own": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
+      "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
+      "dev": true,
+      "requires": {
+        "for-in": "1.0.2"
+      }
+    },
+    "foreach": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
+      "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
+      "dev": true
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+      "dev": true
+    },
+    "from2": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
+      "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "from2-string": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/from2-string/-/from2-string-1.1.0.tgz",
+      "integrity": "sha1-GCgrJ9CKJnyzAwzSuLSw8hKvdSo=",
+      "dev": true,
+      "requires": {
+        "from2": "2.3.0"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "function-bind": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+      "dev": true
+    },
+    "garnish": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/garnish/-/garnish-5.2.0.tgz",
+      "integrity": "sha1-vtQ2WTguSxmOM8eTiXvnxwHmVXc=",
+      "dev": true,
+      "requires": {
+        "chalk": "0.5.1",
+        "minimist": "1.2.0",
+        "pad-left": "2.1.0",
+        "pad-right": "0.2.2",
+        "prettier-bytes": "1.0.4",
+        "pretty-ms": "2.1.0",
+        "right-now": "1.0.0",
+        "split2": "0.2.1",
+        "stdout-stream": "1.4.0",
+        "url-trim": "1.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "0.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz",
+          "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz",
+          "integrity": "sha1-6uy/Zs1waIJ2Cy9GkVgrj1XXp94=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "0.5.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz",
+          "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "1.1.0",
+            "escape-string-regexp": "1.0.5",
+            "has-ansi": "0.1.0",
+            "strip-ansi": "0.3.0",
+            "supports-color": "0.2.0"
+          }
+        },
+        "has-ansi": {
+          "version": "0.1.0",
+          "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz",
+          "integrity": "sha1-hPJlqujA5qiKEtcCKJS3VoiUxi4=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "0.2.1"
+          }
+        },
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz",
+          "integrity": "sha1-JfSOoiynkYfzF0pNuHWTR7sSYiA=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "0.2.1"
+          }
+        },
+        "supports-color": {
+          "version": "0.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz",
+          "integrity": "sha1-2S3iaU6z9nMjlz1649i1W0wiGQo=",
+          "dev": true
+        }
+      }
+    },
+    "get-ports": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/get-ports/-/get-ports-1.0.3.tgz",
+      "integrity": "sha1-9AvVgKyn7A77e5bL/L6wPviUteg=",
+      "dev": true,
+      "requires": {
+        "map-limit": "0.0.1"
+      }
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+      "dev": true
+    },
+    "glob": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+      "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+      "dev": true,
+      "requires": {
+        "fs.realpath": "1.0.0",
+        "inflight": "1.0.6",
+        "inherits": "2.0.3",
+        "minimatch": "3.0.4",
+        "once": "1.4.0",
+        "path-is-absolute": "1.0.1"
+      }
+    },
+    "glob-base": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
+      "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
+      "dev": true,
+      "requires": {
+        "glob-parent": "2.0.0",
+        "is-glob": "2.0.1"
+      }
+    },
+    "glob-parent": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
+      "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+      "dev": true,
+      "requires": {
+        "is-glob": "2.0.1"
+      }
+    },
+    "globals": {
+      "version": "9.18.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+      "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
+      "dev": true
+    },
+    "graceful-fs": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+      "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+      "dev": true
+    },
+    "has": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz",
+      "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=",
+      "dev": true,
+      "requires": {
+        "function-bind": "1.1.1"
+      }
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "2.1.1"
+      }
+    },
+    "hash-base": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz",
+      "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "hash.js": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz",
+      "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "minimalistic-assert": "1.0.0"
+      }
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "dev": true,
+      "requires": {
+        "hash.js": "1.1.3",
+        "minimalistic-assert": "1.0.0",
+        "minimalistic-crypto-utils": "1.0.1"
+      }
+    },
+    "home-or-tmp": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
+      "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=",
+      "dev": true,
+      "requires": {
+        "os-homedir": "1.0.2",
+        "os-tmpdir": "1.0.2"
+      }
+    },
+    "hosted-git-info": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
+      "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==",
+      "dev": true
+    },
+    "htmlescape": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz",
+      "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=",
+      "dev": true
+    },
+    "http-errors": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
+      "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+      "dev": true,
+      "requires": {
+        "depd": "1.1.1",
+        "inherits": "2.0.3",
+        "setprototypeof": "1.0.3",
+        "statuses": "1.3.1"
+      }
+    },
+    "https-browserify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+      "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
+      "dev": true
+    },
+    "ieee754": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz",
+      "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=",
+      "dev": true
+    },
+    "indent-string": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+      "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+      "dev": true,
+      "requires": {
+        "repeating": "2.0.1"
+      }
+    },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
+      "dev": true
+    },
+    "individual": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz",
+      "integrity": "sha1-58pPhfiVewGHNPKFdQ3CLsL5hi0=",
+      "dev": true
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "dev": true,
+      "requires": {
+        "once": "1.4.0",
+        "wrappy": "1.0.2"
+      }
+    },
+    "inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+      "dev": true
+    },
+    "inject-lr-script": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/inject-lr-script/-/inject-lr-script-2.1.0.tgz",
+      "integrity": "sha1-5htehMEYczkGy+oB7D10Zpijn2U=",
+      "dev": true,
+      "requires": {
+        "resp-modifier": "6.0.2"
+      }
+    },
+    "inline-source-map": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz",
+      "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=",
+      "dev": true,
+      "requires": {
+        "source-map": "0.5.7"
+      }
+    },
+    "insert-module-globals": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.1.tgz",
+      "integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=",
+      "dev": true,
+      "requires": {
+        "JSONStream": "1.3.1",
+        "combine-source-map": "0.7.2",
+        "concat-stream": "1.5.2",
+        "is-buffer": "1.1.6",
+        "lexical-scope": "1.2.0",
+        "process": "0.11.10",
+        "through2": "2.0.3",
+        "xtend": "4.0.1"
+      }
+    },
+    "internal-ip": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz",
+      "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=",
+      "dev": true,
+      "requires": {
+        "meow": "3.7.0"
+      }
+    },
+    "invariant": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz",
+      "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=",
+      "dev": true,
+      "requires": {
+        "loose-envify": "1.3.1"
+      }
+    },
+    "is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "dev": true
+    },
+    "is-binary-path": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
+      "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "1.10.0"
+      }
+    },
+    "is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "dev": true
+    },
+    "is-builtin-module": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+      "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+      "dev": true,
+      "requires": {
+        "builtin-modules": "1.1.1"
+      }
+    },
+    "is-dotfile": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
+      "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=",
+      "dev": true
+    },
+    "is-equal-shallow": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
+      "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
+      "dev": true,
+      "requires": {
+        "is-primitive": "2.0.0"
+      }
+    },
+    "is-extendable": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+      "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=",
+      "dev": true
+    },
+    "is-extglob": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
+      "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
+      "dev": true
+    },
+    "is-finite": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+      "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+      "dev": true,
+      "requires": {
+        "number-is-nan": "1.0.1"
+      }
+    },
+    "is-glob": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
+      "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+      "dev": true,
+      "requires": {
+        "is-extglob": "1.0.0"
+      }
+    },
+    "is-number": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
+      "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
+      "dev": true,
+      "requires": {
+        "kind-of": "3.2.2"
+      }
+    },
+    "is-posix-bracket": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
+      "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=",
+      "dev": true
+    },
+    "is-primitive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
+      "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
+      "dev": true
+    },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+      "dev": true
+    },
+    "isarray": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+      "dev": true
+    },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
+    "isobject": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
+      "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
+      "dev": true,
+      "requires": {
+        "isarray": "1.0.0"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+          "dev": true
+        }
+      }
+    },
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
+    },
+    "jsesc": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
+      "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
+      "dev": true
+    },
+    "json-stable-stringify": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz",
+      "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=",
+      "dev": true,
+      "requires": {
+        "jsonify": "0.0.0"
+      }
+    },
+    "json-stringify-safe": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
+      "dev": true
+    },
+    "json5": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
+      "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
+      "dev": true
+    },
+    "jsonify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+      "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
+      "dev": true
+    },
+    "jsonparse": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+      "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
+      "dev": true
+    },
+    "kind-of": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+      "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+      "dev": true,
+      "requires": {
+        "is-buffer": "1.1.6"
+      }
+    },
+    "labeled-stream-splicer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz",
+      "integrity": "sha1-pS4dE4AkwAuGscDJH2d5GLiuClk=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "isarray": "0.0.1",
+        "stream-splicer": "2.0.0"
+      }
+    },
+    "lexical-scope": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.2.0.tgz",
+      "integrity": "sha1-/Ope3HBKSzqHls3KQZw6CvryLfQ=",
+      "dev": true,
+      "requires": {
+        "astw": "2.2.0"
+      }
+    },
+    "load-json-file": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+      "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "parse-json": "2.2.0",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1",
+        "strip-bom": "2.0.0"
+      }
+    },
+    "lodash": {
+      "version": "4.17.4",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
+      "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
+    },
+    "lodash-es": {
+      "version": "4.17.4",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz",
+      "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc="
+    },
+    "lodash.memoize": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz",
+      "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=",
+      "dev": true
+    },
+    "loose-envify": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
+      "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
+      "requires": {
+        "js-tokens": "3.0.2"
+      }
+    },
+    "loud-rejection": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+      "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+      "dev": true,
+      "requires": {
+        "currently-unhandled": "0.4.1",
+        "signal-exit": "3.0.2"
+      }
+    },
+    "map-limit": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz",
+      "integrity": "sha1-63lhAxwPDo0AG/LVb6toXViCLzg=",
+      "dev": true,
+      "requires": {
+        "once": "1.3.3"
+      },
+      "dependencies": {
+        "once": {
+          "version": "1.3.3",
+          "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz",
+          "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=",
+          "dev": true,
+          "requires": {
+            "wrappy": "1.0.2"
+          }
+        }
+      }
+    },
+    "map-obj": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+      "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+      "dev": true
+    },
+    "md5": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
+      "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
+      "dev": true,
+      "requires": {
+        "charenc": "0.0.2",
+        "crypt": "0.0.2",
+        "is-buffer": "1.1.6"
+      }
+    },
+    "md5.js": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
+      "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=",
+      "dev": true,
+      "requires": {
+        "hash-base": "3.0.4",
+        "inherits": "2.0.3"
+      },
+      "dependencies": {
+        "hash-base": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+          "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+          "dev": true,
+          "requires": {
+            "inherits": "2.0.3",
+            "safe-buffer": "5.1.1"
+          }
+        }
+      }
+    },
+    "meow": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+      "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+      "dev": true,
+      "requires": {
+        "camelcase-keys": "2.1.0",
+        "decamelize": "1.2.0",
+        "loud-rejection": "1.6.0",
+        "map-obj": "1.0.1",
+        "minimist": "1.2.0",
+        "normalize-package-data": "2.4.0",
+        "object-assign": "4.1.1",
+        "read-pkg-up": "1.0.1",
+        "redent": "1.0.0",
+        "trim-newlines": "1.0.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "micromatch": {
+      "version": "2.3.11",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
+      "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
+      "dev": true,
+      "requires": {
+        "arr-diff": "2.0.0",
+        "array-unique": "0.2.1",
+        "braces": "1.8.5",
+        "expand-brackets": "0.1.5",
+        "extglob": "0.3.2",
+        "filename-regex": "2.0.1",
+        "is-extglob": "1.0.0",
+        "is-glob": "2.0.1",
+        "kind-of": "3.2.2",
+        "normalize-path": "2.1.1",
+        "object.omit": "2.0.1",
+        "parse-glob": "3.0.4",
+        "regex-cache": "0.4.4"
+      }
+    },
+    "miller-rabin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+      "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "brorand": "1.1.0"
+      }
+    },
+    "mime": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+      "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
+      "dev": true
+    },
+    "minimalistic-assert": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz",
+      "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=",
+      "dev": true
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "dev": true
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "dev": true,
+      "requires": {
+        "brace-expansion": "1.1.8"
+      }
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+      "dev": true
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+      "dev": true,
+      "requires": {
+        "minimist": "0.0.8"
+      }
+    },
+    "module-deps": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz",
+      "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=",
+      "dev": true,
+      "requires": {
+        "JSONStream": "1.3.1",
+        "browser-resolve": "1.11.2",
+        "cached-path-relative": "1.0.1",
+        "concat-stream": "1.5.2",
+        "defined": "1.0.0",
+        "detective": "4.5.0",
+        "duplexer2": "0.1.4",
+        "inherits": "2.0.3",
+        "parents": "1.0.1",
+        "readable-stream": "2.3.3",
+        "resolve": "1.5.0",
+        "stream-combiner2": "1.1.1",
+        "subarg": "1.0.0",
+        "through2": "2.0.3",
+        "xtend": "4.0.1"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+      "dev": true
+    },
+    "normalize-package-data": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+      "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "2.5.0",
+        "is-builtin-module": "1.0.0",
+        "semver": "5.4.1",
+        "validate-npm-package-license": "3.0.1"
+      }
+    },
+    "normalize-path": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+      "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+      "dev": true,
+      "requires": {
+        "remove-trailing-separator": "1.1.0"
+      }
+    },
+    "number-is-nan": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+      "dev": true
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+      "dev": true
+    },
+    "object-keys": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz",
+      "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=",
+      "dev": true
+    },
+    "object.omit": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
+      "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
+      "dev": true,
+      "requires": {
+        "for-own": "0.1.5",
+        "is-extendable": "0.1.1"
+      }
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "dev": true,
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "on-headers": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
+      "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=",
+      "dev": true
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "dev": true,
+      "requires": {
+        "wrappy": "1.0.2"
+      }
+    },
+    "opn": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/opn/-/opn-3.0.3.tgz",
+      "integrity": "sha1-ttmec5n3jWXDuq/+8fsojpuFJDo=",
+      "dev": true,
+      "requires": {
+        "object-assign": "4.1.1"
+      }
+    },
+    "options": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
+      "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=",
+      "dev": true
+    },
+    "os-browserify": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+      "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=",
+      "dev": true
+    },
+    "os-homedir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+      "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
+      "dev": true
+    },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+      "dev": true
+    },
+    "outpipe": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz",
+      "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=",
+      "dev": true,
+      "requires": {
+        "shell-quote": "1.6.1"
+      }
+    },
+    "pad-left": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pad-left/-/pad-left-2.1.0.tgz",
+      "integrity": "sha1-FuajstRKjhOMsIOMx8tAOk/J6ZQ=",
+      "dev": true,
+      "requires": {
+        "repeat-string": "1.6.1"
+      }
+    },
+    "pad-right": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz",
+      "integrity": "sha1-b7ySQEXSRPKiokRQMGDTv8YAl3Q=",
+      "dev": true,
+      "requires": {
+        "repeat-string": "1.6.1"
+      }
+    },
+    "pako": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
+      "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==",
+      "dev": true
+    },
+    "parents": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz",
+      "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=",
+      "dev": true,
+      "requires": {
+        "path-platform": "0.11.15"
+      }
+    },
+    "parse-asn1": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz",
+      "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=",
+      "dev": true,
+      "requires": {
+        "asn1.js": "4.9.2",
+        "browserify-aes": "1.1.1",
+        "create-hash": "1.1.3",
+        "evp_bytestokey": "1.0.3",
+        "pbkdf2": "3.0.14"
+      }
+    },
+    "parse-glob": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
+      "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
+      "dev": true,
+      "requires": {
+        "glob-base": "0.3.0",
+        "is-dotfile": "1.0.3",
+        "is-extglob": "1.0.0",
+        "is-glob": "2.0.1"
+      }
+    },
+    "parse-json": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+      "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+      "dev": true,
+      "requires": {
+        "error-ex": "1.3.1"
+      }
+    },
+    "parse-ms": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz",
+      "integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=",
+      "dev": true
+    },
+    "parseurl": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+      "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=",
+      "dev": true
+    },
+    "path-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
+      "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=",
+      "dev": true
+    },
+    "path-exists": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+      "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+      "dev": true,
+      "requires": {
+        "pinkie-promise": "2.0.1"
+      }
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+      "dev": true
+    },
+    "path-parse": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz",
+      "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
+      "dev": true
+    },
+    "path-platform": {
+      "version": "0.11.15",
+      "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz",
+      "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=",
+      "dev": true
+    },
+    "path-type": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+      "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1"
+      }
+    },
+    "pbkdf2": {
+      "version": "3.0.14",
+      "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz",
+      "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==",
+      "dev": true,
+      "requires": {
+        "create-hash": "1.1.3",
+        "create-hmac": "1.1.6",
+        "ripemd160": "2.0.1",
+        "safe-buffer": "5.1.1",
+        "sha.js": "2.4.9"
+      }
+    },
+    "pem": {
+      "version": "1.12.3",
+      "resolved": "https://registry.npmjs.org/pem/-/pem-1.12.3.tgz",
+      "integrity": "sha512-hT7GwvQL35+0iqgYUl8vn5I5pAVR0HcJas07TXL8bNaR4c5kAFRquk4ZqQk1F9YMcQOr6WjGdY5OnDC0RBnzig==",
+      "dev": true,
+      "requires": {
+        "md5": "2.2.1",
+        "os-tmpdir": "1.0.2",
+        "safe-buffer": "5.1.1",
+        "which": "1.3.0"
+      }
+    },
+    "pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+      "dev": true
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+      "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+      "dev": true
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+      "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+      "dev": true,
+      "requires": {
+        "pinkie": "2.0.4"
+      }
+    },
+    "plur": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz",
+      "integrity": "sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY=",
+      "dev": true
+    },
+    "preserve": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
+      "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
+      "dev": true
+    },
+    "prettier-bytes": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz",
+      "integrity": "sha1-mUsCqkb2mcULYle1+qp/4lV+YtY=",
+      "dev": true
+    },
+    "pretty-ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz",
+      "integrity": "sha1-QlfCVt8/sLRR1q/6qwIYhBJpgdw=",
+      "dev": true,
+      "requires": {
+        "is-finite": "1.0.2",
+        "parse-ms": "1.0.1",
+        "plur": "1.0.0"
+      }
+    },
+    "private": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
+      "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==",
+      "dev": true
+    },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+      "dev": true
+    },
+    "process-nextick-args": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+      "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
+      "dev": true
+    },
+    "public-encrypt": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz",
+      "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=",
+      "dev": true,
+      "requires": {
+        "bn.js": "4.11.8",
+        "browserify-rsa": "4.0.1",
+        "create-hash": "1.1.3",
+        "parse-asn1": "5.1.0",
+        "randombytes": "2.0.5"
+      }
+    },
+    "punycode": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+      "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+      "dev": true
+    },
+    "query-string": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
+      "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
+      "dev": true,
+      "requires": {
+        "object-assign": "4.1.1",
+        "strict-uri-encode": "1.1.0"
+      }
+    },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
+      "dev": true
+    },
+    "querystring-es3": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+      "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
+      "dev": true
+    },
+    "randomatic": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
+      "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==",
+      "dev": true,
+      "requires": {
+        "is-number": "3.0.0",
+        "kind-of": "4.0.0"
+      },
+      "dependencies": {
+        "is-number": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
+          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
+          "dev": true,
+          "requires": {
+            "kind-of": "3.2.2"
+          },
+          "dependencies": {
+            "kind-of": {
+              "version": "3.2.2",
+              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+              "dev": true,
+              "requires": {
+                "is-buffer": "1.1.6"
+              }
+            }
+          }
+        },
+        "kind-of": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
+          "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
+          "dev": true,
+          "requires": {
+            "is-buffer": "1.1.6"
+          }
+        }
+      }
+    },
+    "randombytes": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz",
+      "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "randomfill": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.3.tgz",
+      "integrity": "sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==",
+      "dev": true,
+      "requires": {
+        "randombytes": "2.0.5",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
+      "dev": true
+    },
+    "read-only-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz",
+      "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.3"
+      }
+    },
+    "read-pkg": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+      "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+      "dev": true,
+      "requires": {
+        "load-json-file": "1.1.0",
+        "normalize-package-data": "2.4.0",
+        "path-type": "1.1.0"
+      }
+    },
+    "read-pkg-up": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+      "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+      "dev": true,
+      "requires": {
+        "find-up": "1.1.2",
+        "read-pkg": "1.1.0"
+      }
+    },
+    "readable-stream": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
+      "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
+      "dev": true,
+      "requires": {
+        "core-util-is": "1.0.2",
+        "inherits": "2.0.3",
+        "isarray": "1.0.0",
+        "process-nextick-args": "1.0.7",
+        "safe-buffer": "5.1.1",
+        "string_decoder": "1.0.3",
+        "util-deprecate": "1.0.2"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+          "dev": true
+        }
+      }
+    },
+    "readdirp": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
+      "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "minimatch": "3.0.4",
+        "readable-stream": "2.3.3",
+        "set-immediate-shim": "1.0.1"
+      }
+    },
+    "redent": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+      "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+      "dev": true,
+      "requires": {
+        "indent-string": "2.1.0",
+        "strip-indent": "1.0.1"
+      }
+    },
+    "redux": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
+      "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
+      "requires": {
+        "lodash": "4.17.4",
+        "lodash-es": "4.17.4",
+        "loose-envify": "1.3.1",
+        "symbol-observable": "1.0.4"
+      }
+    },
+    "redux-logger": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
+      "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=",
+      "requires": {
+        "deep-diff": "0.3.8"
+      }
+    },
+    "regenerator-runtime": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz",
+      "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==",
+      "dev": true
+    },
+    "regex-cache": {
+      "version": "0.4.4",
+      "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
+      "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
+      "dev": true,
+      "requires": {
+        "is-equal-shallow": "0.1.3"
+      }
+    },
+    "reload-css": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/reload-css/-/reload-css-1.0.2.tgz",
+      "integrity": "sha1-avsRFi4jFP7M2tbcX96CH9cxgzE=",
+      "dev": true,
+      "requires": {
+        "query-string": "4.3.4"
+      }
+    },
+    "remove-trailing-separator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+      "dev": true
+    },
+    "repeat-element": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz",
+      "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=",
+      "dev": true
+    },
+    "repeat-string": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
+      "dev": true
+    },
+    "repeating": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+      "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+      "dev": true,
+      "requires": {
+        "is-finite": "1.0.2"
+      }
+    },
+    "resolve": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
+      "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==",
+      "dev": true,
+      "requires": {
+        "path-parse": "1.0.5"
+      }
+    },
+    "resp-modifier": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz",
+      "integrity": "sha1-sSTeXE+6/LpUH0j/pzlw9KpFa08=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "minimatch": "3.0.4"
+      }
+    },
+    "right-now": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/right-now/-/right-now-1.0.0.tgz",
+      "integrity": "sha1-bolgne69fc2vja7Mmuo5z1haCRg=",
+      "dev": true
+    },
+    "ripemd160": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz",
+      "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=",
+      "dev": true,
+      "requires": {
+        "hash-base": "2.0.2",
+        "inherits": "2.0.3"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+      "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
+      "dev": true
+    },
+    "semver": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
+      "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
+      "dev": true
+    },
+    "send": {
+      "version": "0.16.1",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz",
+      "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "1.1.1",
+        "destroy": "1.0.4",
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "etag": "1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "1.6.2",
+        "mime": "1.4.1",
+        "ms": "2.0.0",
+        "on-finished": "2.3.0",
+        "range-parser": "1.2.0",
+        "statuses": "1.3.1"
+      }
+    },
+    "serve-static": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz",
+      "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==",
+      "dev": true,
+      "requires": {
+        "encodeurl": "1.0.1",
+        "escape-html": "1.0.3",
+        "parseurl": "1.3.2",
+        "send": "0.16.1"
+      }
+    },
+    "set-immediate-shim": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
+      "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
+      "dev": true
+    },
+    "setprototypeof": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
+      "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=",
+      "dev": true
+    },
+    "sha.js": {
+      "version": "2.4.9",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.9.tgz",
+      "integrity": "sha512-G8zektVqbiPHrylgew9Zg1VRB1L/DtXNUVAM6q4QLy8NE3qtHlFXTf8VLL4k1Yl6c7NMjtZUTdXV+X44nFaT6A==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "shasum": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz",
+      "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=",
+      "dev": true,
+      "requires": {
+        "json-stable-stringify": "0.0.1",
+        "sha.js": "2.4.9"
+      }
+    },
+    "shell-quote": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
+      "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
+      "dev": true,
+      "requires": {
+        "array-filter": "0.0.1",
+        "array-map": "0.0.0",
+        "array-reduce": "0.0.0",
+        "jsonify": "0.0.0"
+      }
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+      "dev": true
+    },
+    "simple-html-index": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/simple-html-index/-/simple-html-index-1.5.0.tgz",
+      "integrity": "sha1-LJPurrrAAdihNfwAIr1K3o9YmW8=",
+      "dev": true,
+      "requires": {
+        "from2-string": "1.1.0"
+      }
+    },
+    "slash": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+      "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
+      "dev": true
+    },
+    "source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+      "dev": true
+    },
+    "source-map-support": {
+      "version": "0.4.18",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
+      "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==",
+      "dev": true,
+      "requires": {
+        "source-map": "0.5.7"
+      }
+    },
+    "spdx-correct": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz",
+      "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=",
+      "dev": true,
+      "requires": {
+        "spdx-license-ids": "1.2.2"
+      }
+    },
+    "spdx-expression-parse": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz",
+      "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=",
+      "dev": true
+    },
+    "spdx-license-ids": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz",
+      "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=",
+      "dev": true
+    },
+    "split2": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-0.2.1.tgz",
+      "integrity": "sha1-At2smtwD7Au3jBKC7Aecpuha6QA=",
+      "dev": true,
+      "requires": {
+        "through2": "0.6.5"
+      },
+      "dependencies": {
+        "readable-stream": {
+          "version": "1.0.34",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+          "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+          "dev": true,
+          "requires": {
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "0.0.1",
+            "string_decoder": "0.10.31"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+          "dev": true
+        },
+        "through2": {
+          "version": "0.6.5",
+          "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz",
+          "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=",
+          "dev": true,
+          "requires": {
+            "readable-stream": "1.0.34",
+            "xtend": "4.0.1"
+          }
+        }
+      }
+    },
+    "stacked": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/stacked/-/stacked-1.1.1.tgz",
+      "integrity": "sha1-LH+jjMfjejQRp3zY55LeRI+faXU=",
+      "dev": true
+    },
+    "statuses": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+      "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
+      "dev": true
+    },
+    "stdout-stream": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
+      "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.3"
+      }
+    },
+    "stream-browserify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
+      "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "stream-combiner2": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz",
+      "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=",
+      "dev": true,
+      "requires": {
+        "duplexer2": "0.1.4",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "stream-http": {
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz",
+      "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==",
+      "dev": true,
+      "requires": {
+        "builtin-status-codes": "3.0.0",
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3",
+        "to-arraybuffer": "1.0.1",
+        "xtend": "4.0.1"
+      }
+    },
+    "stream-splicer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz",
+      "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3"
+      }
+    },
+    "strict-uri-encode": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
+      "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
+      "dev": true
+    },
+    "string_decoder": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+      "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "strip-ansi": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "2.1.1"
+      }
+    },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+      "dev": true,
+      "requires": {
+        "is-utf8": "0.2.1"
+      }
+    },
+    "strip-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+      "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+      "dev": true,
+      "requires": {
+        "get-stdin": "4.0.1"
+      }
+    },
+    "subarg": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz",
+      "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=",
+      "dev": true,
+      "requires": {
+        "minimist": "1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "supports-color": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+      "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
+      "dev": true
+    },
+    "symbol-observable": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz",
+      "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0="
+    },
+    "syntax-error": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz",
+      "integrity": "sha1-HtkmbE1AvnXcVb+bsct3Biu5bKE=",
+      "dev": true,
+      "requires": {
+        "acorn": "4.0.13"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "4.0.13",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
+          "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=",
+          "dev": true
+        }
+      }
+    },
+    "term-color": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/term-color/-/term-color-1.0.1.tgz",
+      "integrity": "sha1-OOGSVTpHPjXkFgT/UZmEa/gRejo=",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "2.0.1",
+        "supports-color": "1.3.1"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.0.1.tgz",
+          "integrity": "sha1-sDP1f5Pi0oreuLwRE4+hPaD9IKM=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz",
+          "integrity": "sha1-FXWN8J2P87SswwdTn6vicJXhBC0=",
+          "dev": true
+        }
+      }
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "through2": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+      "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.3",
+        "xtend": "4.0.1"
+      }
+    },
+    "timers-browserify": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz",
+      "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=",
+      "dev": true,
+      "requires": {
+        "process": "0.11.10"
+      }
+    },
+    "to-arraybuffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+      "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
+      "dev": true
+    },
+    "to-fast-properties": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
+      "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=",
+      "dev": true
+    },
+    "trim-newlines": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+      "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+      "dev": true
+    },
+    "trim-right": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+      "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
+      "dev": true
+    },
+    "tty-browserify": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
+      "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
+      "dev": true
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+      "dev": true
+    },
+    "ultron": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz",
+      "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=",
+      "dev": true
+    },
+    "umd": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz",
+      "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=",
+      "dev": true
+    },
+    "url": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+      "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+      "dev": true,
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=",
+          "dev": true
+        }
+      }
+    },
+    "url-trim": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/url-trim/-/url-trim-1.0.0.tgz",
+      "integrity": "sha1-QAV+LxZLiOXaynJp2kfm0d2Detw=",
+      "dev": true
+    },
+    "util": {
+      "version": "0.10.3",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+      "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.1"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+          "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=",
+          "dev": true
+        }
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
+    "validate-npm-package-license": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
+      "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "1.0.2",
+        "spdx-expression-parse": "1.0.4"
+      }
+    },
+    "vm-browserify": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
+      "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+      "dev": true,
+      "requires": {
+        "indexof": "0.0.1"
+      }
+    },
+    "watchify": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.9.0.tgz",
+      "integrity": "sha1-8HX9LoqGrN6Eztum5cKgvt1SPZ4=",
+      "dev": true,
+      "requires": {
+        "anymatch": "1.3.2",
+        "browserify": "14.5.0",
+        "chokidar": "1.7.0",
+        "defined": "1.0.0",
+        "outpipe": "1.1.1",
+        "through2": "2.0.3",
+        "xtend": "4.0.1"
+      }
+    },
+    "watchify-middleware": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/watchify-middleware/-/watchify-middleware-1.6.0.tgz",
+      "integrity": "sha1-bbbijwJ53hyhIJrk8afwY3RYd8Q=",
+      "dev": true,
+      "requires": {
+        "concat-stream": "1.5.2",
+        "debounce": "1.1.0",
+        "events": "1.1.1",
+        "object-assign": "4.1.1",
+        "strip-ansi": "3.0.1",
+        "watchify": "3.9.0"
+      }
+    },
+    "which": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",
+      "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==",
+      "dev": true,
+      "requires": {
+        "isexe": "2.0.0"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+      "dev": true
+    },
+    "ws": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz",
+      "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==",
+      "dev": true,
+      "requires": {
+        "options": "0.0.6",
+        "ultron": "1.0.2"
+      }
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+      "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+      "dev": true
+    }
+  }
+}

+ 18 - 0
examples/redux/package.json

@@ -0,0 +1,18 @@
+{
+  "private": true,
+  "name": "uppy-redux-example",
+  "scripts": {
+    "css": "cp ../../dist/uppy.min.css .",
+    "start": "npm run css && budo main.js:bundle.js -- -t babelify -g aliasify"
+  },
+  "aliasify": "./aliasify.js",
+  "dependencies": {
+    "redux": "^3.7.2",
+    "redux-logger": "^3.0.6"
+  },
+  "devDependencies": {
+    "aliasify": "^2.1.0",
+    "babelify": "^7.3.0",
+    "budo": "^10.0.4"
+  }
+}

+ 16 - 0
examples/redux/readme.md

@@ -0,0 +1,16 @@
+# Redux
+
+This example uses Uppy with a Redux store.
+The same Redux store is also used for other parts of the application, namely the counter example.
+Each action is logged to the console using [redux-logger](https://github.com/theaqua/redux-logger).
+
+This example supports the [Redux Devtools extension](https://github.com/zalmoxisus/redux-devtools-extension), including time travel.
+
+## Run it
+
+Move into this directory, then:
+
+```bash
+npm install
+npm start
+```

File diff suppressed because it is too large
+ 978 - 219
package-lock.json


+ 24 - 15
package.json

@@ -1,6 +1,6 @@
 {
   "name": "uppy",
-  "version": "0.20.3",
+  "version": "0.21.1",
   "description": "Extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:",
   "main": "lib/index.js",
   "jsnext:main": "src/index.js",
@@ -39,17 +39,22 @@
     "url": "https://github.com/transloadit/uppy/issues"
   },
   "homepage": "https://github.com/transloadit/uppy#readme",
+  "jest": {
+    "testPathIgnorePatterns": [
+      "lib"
+    ]
+  },
   "devDependencies": {
     "autoprefixer": "6.3.7",
     "babel-cli": "6.11.4",
     "babel-core": "6.13.2",
     "babel-eslint": "6.1.2",
-    "babel-jest": "^20.0.3",
+    "babel-jest": "^22.0.0",
     "babel-plugin-add-module-exports": "0.2.1",
     "babel-plugin-es6-promise": "1.0.0",
     "babel-plugin-transform-object-assign": "6.8.0",
     "babel-plugin-transform-proto-to-assign": "6.9.0",
-    "babel-plugin-yo-yoify": "0.6.0",
+    "babel-plugin-transform-react-jsx": "^6.24.1",
     "babel-polyfill": "6.9.1",
     "babel-preset-es2015": "6.24.0",
     "babel-register": "6.9.0",
@@ -61,8 +66,9 @@
     "disc": "1.3.2",
     "eslint": "^3.19.0",
     "eslint-config-standard": "^10.2.1",
+    "eslint-config-standard-preact": "^1.1.6",
     "eslint-plugin-import": "^2.7.0",
-    "eslint-plugin-jest": "^21.1.0",
+    "eslint-plugin-jest": "^21.5.0",
     "eslint-plugin-node": "^4.2.3",
     "eslint-plugin-promise": "^3.5.0",
     "eslint-plugin-standard": "^3.0.1",
@@ -70,7 +76,7 @@
     "fakefile": "0.0.8",
     "glob": "7.1.1",
     "isomorphic-fetch": "2.2.1",
-    "jest": "^20.0.4",
+    "jest": "^22.0.0",
     "lint-staged": "2.0.2",
     "minify-stream": "^1.1.0",
     "mkdirp": "0.5.1",
@@ -84,14 +90,19 @@
     "onchange": "3.2.1",
     "postcss": "5.1.0",
     "pre-commit": "1.1.3",
+    "redux": "^3.7.2",
     "sass": "0.5.0",
-    "selenium-webdriver": "2.53.3",
     "tape": "^4.8.0",
     "tinyify": "^1.0.0",
     "uppy-server": "0.0.7",
-    "watchify": "3.7.0"
+    "watchify": "3.7.0",
+    "wdio-mocha-framework": "^0.5.11",
+    "wdio-sauce-service": "^0.4.4",
+    "wdio-static-server-service": "^1.0.1",
+    "webdriverio": "^4.9.5"
   },
   "dependencies": {
+    "classnames": "^2.2.5",
     "cuid": "^1.3.8",
     "drag-drop": "2.13.2",
     "es6-promise": "3.2.1",
@@ -99,16 +110,13 @@
     "lodash.throttle": "4.1.1",
     "mime-match": "^1.0.2",
     "namespace-emitter": "^2.0.0",
-    "nanoraf": "3.0.1",
-    "on-load": "3.2.0",
+    "preact": "^8.2.7",
     "prettier-bytes": "1.0.4",
     "prop-types": "^15.5.10",
-    "socket.io-client": "2.0.1",
-    "tus-js-client": "^1.4.4",
+    "socket.io-client": "2.0.2",
+    "tus-js-client": "^1.4.5",
     "url-parse": "1.1.9",
-    "whatwg-fetch": "2.0.3",
-    "yo-yo": "1.4.0",
-    "yo-yoify": "3.7.2"
+    "whatwg-fetch": "2.0.3"
   },
   "scripts": {
     "build:bundle": "node ./bin/build-js.js",
@@ -129,7 +137,8 @@
     "start:server": "cd ./node_modules/uppy-server && npm run start",
     "start": "npm-run-all --parallel watch start:server web:preview",
     "test:acceptance:handleservers": "bin/bootandkill-servers node test/acceptance/index.js",
-    "test:acceptance": "node test/acceptance/index.js",
+    "test:acceptance": "./bin/endtoend-build && wdio test/endtoend/wdio.remote.conf.js",
+    "test:acceptance:local": "./bin/endtoend-build && wdio test/endtoend/wdio.local.conf.js",
     "test:unit": "jest --testPathPattern=src --coverage",
     "test": "npm run lint && npm run test:unit",
     "test:watch": "jest --watch --testPathPattern=src",

+ 216 - 198
src/core/Core.js

@@ -1,11 +1,11 @@
 const Utils = require('../core/Utils')
-const Translator = require('./Translator')
-const UppySocket = require('./UppySocket')
+const Translator = require('../core/Translator')
 const ee = require('namespace-emitter')
 const cuid = require('cuid')
 const throttle = require('lodash.throttle')
 const prettyBytes = require('prettier-bytes')
 const match = require('mime-match')
+const DefaultStore = require('../store/DefaultStore')
 // const deepFreeze = require('deep-freeze-strict')
 
 /**
@@ -45,16 +45,14 @@ class Uppy {
       meta: {},
       onBeforeFileAdded: (currentFile, files) => Promise.resolve(),
       onBeforeUpload: (files, done) => Promise.resolve(),
-      locale: defaultLocale
+      locale: defaultLocale,
+      store: new DefaultStore(),
+      thumbnailGeneration: true
     }
 
     // Merge default options with the ones set by user
     this.opts = Object.assign({}, defaultOptions, opts)
 
-    // // Dictates in what order different plugin types are ran:
-    // this.types = [ 'presetter', 'orchestrator', 'progressindicator',
-    //                 'acquirer', 'modifier', 'uploader', 'presenter', 'debugger']
-
     this.locale = Object.assign({}, defaultLocale, this.opts.locale)
     this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
 
@@ -69,15 +67,16 @@ class Uppy {
     this.i18n = this.translator.translate.bind(this.translator)
     this.getState = this.getState.bind(this)
     this.getPlugin = this.getPlugin.bind(this)
-    this.updateMeta = this.updateMeta.bind(this)
-    this.initSocket = this.initSocket.bind(this)
+    this.setFileMeta = this.setFileMeta.bind(this)
+    this.setFileState = this.setFileState.bind(this)
+    // this._initSocket = this._initSocket.bind(this)
     this.log = this.log.bind(this)
     this.info = this.info.bind(this)
     this.hideInfo = this.hideInfo.bind(this)
     this.addFile = this.addFile.bind(this)
     this.removeFile = this.removeFile.bind(this)
     this.pauseResume = this.pauseResume.bind(this)
-    this.calculateProgress = this.calculateProgress.bind(this)
+    this._calculateProgress = this._calculateProgress.bind(this)
     this.resetProgress = this.resetProgress.bind(this)
 
     this.pauseAll = this.pauseAll.bind(this)
@@ -85,6 +84,7 @@ class Uppy {
     this.retryAll = this.retryAll.bind(this)
     this.cancelAll = this.cancelAll.bind(this)
     this.retryUpload = this.retryUpload.bind(this)
+    this.upload = this.upload.bind(this)
 
     // this.bus = this.emitter = ee()
     this.emitter = ee()
@@ -97,9 +97,11 @@ class Uppy {
     this.uploaders = []
     this.postProcessors = []
 
-    this.state = {
+    this.store = this.opts.store
+    this.setState({
       plugins: {},
       files: {},
+      currentUploads: {},
       capabilities: {
         resumableUploads: false
       },
@@ -110,15 +112,18 @@ class Uppy {
         type: 'info',
         message: ''
       }
-    }
+    })
+
+    this._storeUnsubscribe = this.store.subscribe((prevState, nextState, patch) => {
+      this.emit('state-update', prevState, nextState, patch)
+      this.updateAll(nextState)
+    })
 
     // for debugging and testing
     // this.updateNum = 0
     if (this.opts.debug) {
-      global.UppyState = this.state
       global.uppyLog = ''
-      // global.UppyAddFile = this.addFile.bind(this)
-      global._uppy = this
+      global[this.opts.id] = this
     }
   }
 
@@ -138,23 +143,30 @@ class Uppy {
    * @param {patch} object
    */
   setState (patch) {
-    const prevState = Object.assign({}, this.state)
-    const nextState = Object.assign({}, this.state, patch)
-
-    this.state = nextState
-    this.emit('core:state-update', prevState, nextState, patch)
-
-    this.updateAll(this.state)
+    this.store.setState(patch)
   }
 
   /**
    * Returns current state
-   *
    */
   getState () {
-    // use deepFreeze for debugging
-    // return deepFreeze(this.state)
-    return this.state
+    return this.store.getState()
+  }
+
+  // Back compat.
+  get state () {
+    return this.getState()
+  }
+
+  /**
+  * Shorthand to set state for a specific file
+  */
+  setFileState (fileID, state) {
+    this.setState({
+      files: Object.assign({}, this.getState().files, {
+        [fileID]: Object.assign({}, this.getState().files[fileID], state)
+      })
+    })
   }
 
   resetProgress () {
@@ -164,7 +176,7 @@ class Uppy {
       uploadComplete: false,
       uploadStarted: false
     }
-    const files = Object.assign({}, this.state.files)
+    const files = Object.assign({}, this.getState().files)
     const updatedFiles = {}
     Object.keys(files).forEach(fileID => {
       const updatedFile = Object.assign({}, files[fileID])
@@ -178,7 +190,7 @@ class Uppy {
     })
 
     // TODO Document on the website
-    this.emit('core:reset-progress')
+    this.emit('reset-progress')
   }
 
   addPreProcessor (fn) {
@@ -215,14 +227,14 @@ class Uppy {
   }
 
   setMeta (data) {
-    const newMeta = Object.assign({}, this.state.meta, data)
+    const newMeta = Object.assign({}, this.getState().meta, data)
     this.log('Adding metadata:')
     this.log(data)
-    this.setState({meta: newMeta})
+    this.setState({ meta: newMeta })
   }
 
-  updateMeta (data, fileID) {
-    const updatedFiles = Object.assign({}, this.state.files)
+  setFileMeta (fileID, data) {
+    const updatedFiles = Object.assign({}, this.getState().files)
     if (!updatedFiles[fileID]) {
       this.log('Was trying to set metadata for a file that’s not with us anymore: ', fileID)
       return
@@ -234,15 +246,24 @@ class Uppy {
     this.setState({files: updatedFiles})
   }
 
+  /**
+   * Get a file object.
+   *
+   * @param {string} fileID The ID of the file object to return.
+   */
+  getFile (fileID) {
+    return this.getState().files[fileID]
+  }
+
   /**
   * Check if minNumberOfFiles restriction is reached before uploading
   *
   * @return {boolean}
   * @private
   */
-  checkMinNumberOfFiles () {
+  _checkMinNumberOfFiles () {
     const {minNumberOfFiles} = this.opts.restrictions
-    if (Object.keys(this.state.files).length < minNumberOfFiles) {
+    if (Object.keys(this.getState().files).length < minNumberOfFiles) {
       this.info(`${this.i18n('youHaveToAtLeastSelectX', {smart_count: minNumberOfFiles})}`, 'error', 5000)
       return false
     }
@@ -257,11 +278,11 @@ class Uppy {
   * @return {boolean}
   * @private
   */
-  checkRestrictions (file) {
+  _checkRestrictions (file) {
     const {maxFileSize, maxNumberOfFiles, allowedFileTypes} = this.opts.restrictions
 
     if (maxNumberOfFiles) {
-      if (Object.keys(this.state.files).length + 1 > maxNumberOfFiles) {
+      if (Object.keys(this.getState().files).length + 1 > maxNumberOfFiles) {
         this.info(`${this.i18n('youCanOnlyUploadX', {smart_count: maxNumberOfFiles})}`, 'error', 5000)
         return false
       }
@@ -305,7 +326,7 @@ class Uppy {
       return Promise.reject(new Error(`onBeforeFileAdded: ${message}`))
     }).then(() => {
       return Utils.getFileType(file).then((fileType) => {
-        const updatedFiles = Object.assign({}, this.state.files)
+        const updatedFiles = Object.assign({}, this.getState().files)
         let fileName
         if (file.name) {
           fileName = file.name
@@ -343,13 +364,13 @@ class Uppy {
           preview: file.preview
         }
 
-        const isFileAllowed = this.checkRestrictions(newFile)
+        const isFileAllowed = this._checkRestrictions(newFile)
         if (!isFileAllowed) return Promise.reject(new Error('File not allowed'))
 
         updatedFiles[fileID] = newFile
         this.setState({files: updatedFiles})
 
-        this.emit('core:file-added', newFile)
+        this.emit('file-added', newFile)
         this.log(`Added file: ${fileName}, ${fileID}, mime type: ${fileType}`)
 
         if (this.opts.autoProceed && !this.scheduledAutoProceed) {
@@ -365,13 +386,38 @@ class Uppy {
   }
 
   removeFile (fileID) {
-    const updatedFiles = Object.assign({}, this.getState().files)
+    const { files, currentUploads } = this.state
+    const updatedFiles = Object.assign({}, files)
     const removedFile = updatedFiles[fileID]
     delete updatedFiles[fileID]
 
-    this.setState({files: updatedFiles})
-    this.calculateTotalProgress()
-    this.emit('core:file-removed', fileID)
+    // Remove this file from its `currentUpload`.
+    const updatedUploads = Object.assign({}, currentUploads)
+    const removeUploads = []
+    Object.keys(updatedUploads).forEach((uploadID) => {
+      const newFileIDs = currentUploads[uploadID].fileIDs.filter((uploadFileID) => uploadFileID !== fileID)
+      // Remove the upload if no files are associated with it anymore.
+      if (newFileIDs.length === 0) {
+        removeUploads.push(uploadID)
+        return
+      }
+
+      updatedUploads[uploadID] = Object.assign({}, currentUploads[uploadID], {
+        fileIDs: newFileIDs
+      })
+    })
+
+    this.setState({
+      currentUploads: updatedUploads,
+      files: updatedFiles
+    })
+
+    removeUploads.forEach((uploadID) => {
+      this.removeUpload(uploadID)
+    })
+
+    this._calculateTotalProgress()
+    this.emit('file-removed', fileID)
 
     // Clean up object URLs.
     if (removedFile.preview && Utils.isObjectURL(removedFile.preview)) {
@@ -381,22 +427,19 @@ class Uppy {
     this.log(`Removed file: ${fileID}`)
   }
 
-  /**
-   * Get a file object.
-   *
-   * @param {string} fileID The ID of the file object to return.
-   */
-  getFile (fileID) {
-    return this.getState().files[fileID]
-  }
-
   /**
    * Generate a preview image for the given file, if possible.
    */
   generatePreview (file) {
     if (Utils.isPreviewSupported(file.type) && !file.isRemote) {
-      Utils.createThumbnail(file, 200).then((thumbnail) => {
-        this.setPreviewURL(file.id, thumbnail)
+      let previewPromise
+      if (this.opts.thumbnailGeneration === true) {
+        previewPromise = Utils.createThumbnail(file, 200)
+      } else {
+        previewPromise = Promise.resolve(URL.createObjectURL(file.data))
+      }
+      previewPromise.then((preview) => {
+        this.setPreviewURL(file.id, preview)
       }).catch((err) => {
         console.warn(err.stack || err.message)
       })
@@ -407,14 +450,7 @@ class Uppy {
    * Set the preview URL for a file.
    */
   setPreviewURL (fileID, preview) {
-    const { files } = this.state
-    this.setState({
-      files: Object.assign({}, files, {
-        [fileID]: Object.assign({}, files[fileID], {
-          preview: preview
-        })
-      })
-    })
+    this.setFileState(fileID, { preview: preview })
   }
 
   pauseResume (fileID) {
@@ -432,7 +468,7 @@ class Uppy {
     updatedFiles[fileID] = updatedFile
     this.setState({files: updatedFiles})
 
-    this.emit('core:upload-pause', fileID, isPaused)
+    this.emit('upload-pause', fileID, isPaused)
 
     return isPaused
   }
@@ -452,7 +488,7 @@ class Uppy {
     })
     this.setState({files: updatedFiles})
 
-    this.emit('core:pause-all')
+    this.emit('pause-all')
   }
 
   resumeAll () {
@@ -471,7 +507,7 @@ class Uppy {
     })
     this.setState({files: updatedFiles})
 
-    this.emit('core:resume-all')
+    this.emit('resume-all')
   }
 
   retryAll () {
@@ -492,14 +528,19 @@ class Uppy {
       error: null
     })
 
-    this.emit('core:retry-all', filesToRetry)
+    this.emit('retry-all', filesToRetry)
 
-    const uploadID = this.createUpload(filesToRetry)
-    return this.runUpload(uploadID)
+    const uploadID = this._createUpload(filesToRetry)
+    return this._runUpload(uploadID)
+  }
+
+  cancelAll () {
+    this.emit('cancel-all')
+    this.setState({ files: {}, totalProgress: 0 })
   }
 
   retryUpload (fileID) {
-    const updatedFiles = Object.assign({}, this.state.files)
+    const updatedFiles = Object.assign({}, this.getState().files)
     const updatedFile = Object.assign({}, updatedFiles[fileID],
       { error: null, isPaused: false }
     )
@@ -508,50 +549,37 @@ class Uppy {
       files: updatedFiles
     })
 
-    this.emit('core:upload-retry', fileID)
+    this.emit('upload-retry', fileID)
 
-    const uploadID = this.createUpload([ fileID ])
-    return this.runUpload(uploadID)
+    const uploadID = this._createUpload([ fileID ])
+    return this._runUpload(uploadID)
   }
 
   reset () {
     this.cancelAll()
   }
 
-  cancelAll () {
-    this.emit('core:cancel-all')
-    this.setState({ files: {}, totalProgress: 0 })
-  }
-
-  calculateProgress (data) {
+  _calculateProgress (data) {
     const fileID = data.id
-    const updatedFiles = Object.assign({}, this.getState().files)
 
     // skip progress event for a file that’s been removed
-    if (!updatedFiles[fileID]) {
-      this.log('Trying to set progress for a file that’s not with us anymore: ', fileID)
+    if (!this.getState().files[fileID]) {
+      this.log('Trying to set progress for a file that’s been removed: ', fileID)
       return
     }
 
-    const updatedFile = Object.assign({}, updatedFiles[fileID],
-      Object.assign({}, {
-        progress: Object.assign({}, updatedFiles[fileID].progress, {
-          bytesUploaded: data.bytesUploaded,
-          bytesTotal: data.bytesTotal,
-          percentage: Math.floor((data.bytesUploaded / data.bytesTotal * 100).toFixed(2))
-        })
-      }
-    ))
-    updatedFiles[data.id] = updatedFile
-
-    this.setState({
-      files: updatedFiles
+    this.setFileState(fileID, {
+      progress: Object.assign({}, this.getState().files[fileID].progress, {
+        bytesUploaded: data.bytesUploaded,
+        bytesTotal: data.bytesTotal,
+        percentage: Math.floor((data.bytesUploaded / data.bytesTotal * 100).toFixed(2))
+      })
     })
 
-    this.calculateTotalProgress()
+    this._calculateTotalProgress()
   }
 
-  calculateTotalProgress () {
+  _calculateTotalProgress () {
     // calculate total progress, using the number of files currently uploading,
     // multiplied by 100 and the summ of individual progress of each file
     const files = Object.assign({}, this.getState().files)
@@ -574,13 +602,14 @@ class Uppy {
 
   /**
    * Registers listeners for all global actions, like:
-   * `file-add`, `file-remove`, `upload-progress`, `reset`
+   * `error`, `file-added`, `file-removed`, `upload-progress`
    *
    */
   actions () {
-    // this.bus.on('*', (payload) => {
-    //   console.log('emitted: ', this.event)
-    //   console.log('with payload: ', payload)
+    // const log = this.log
+    // this.on('*', function (payload) {
+    //   log(`[Core] Event: ${this.event}`)
+    //   log(payload)
     // })
 
     // stress-test re-rendering
@@ -588,19 +617,15 @@ class Uppy {
     //   this.setState({bla: 'bla'})
     // }, 20)
 
-    this.on('core:error', (error) => {
-      this.setState({ error })
+    this.on('error', (error) => {
+      this.setState({ error: error.message })
     })
 
-    this.on('core:upload-error', (fileID, error) => {
-      const updatedFiles = Object.assign({}, this.state.files)
-      const updatedFile = Object.assign({}, updatedFiles[fileID],
-        { error: error }
-      )
-      updatedFiles[fileID] = updatedFile
-      this.setState({ files: updatedFiles, error: error })
+    this.on('upload-error', (fileID, error) => {
+      this.setFileState(fileID, { error: error.message })
+      this.setState({ error: error.message })
 
-      const fileName = this.state.files[fileID].name
+      const fileName = this.getState().files[fileID].name
       let message = `Failed to upload ${fileName}`
       if (typeof error === 'object' && error.message) {
         message = { message: message, details: error.message }
@@ -608,83 +633,63 @@ class Uppy {
       this.info(message, 'error', 5000)
     })
 
-    this.on('core:upload', () => {
+    this.on('upload', () => {
       this.setState({ error: null })
     })
 
-    this.on('core:file-add', (data) => {
-      this.addFile(data)
-    })
+    // this.on('file-add', (data) => {
+    //   this.addFile(data)
+    // })
 
-    this.on('core:file-added', (file) => {
+    this.on('file-added', (file) => {
       this.generatePreview(file)
     })
 
-    this.on('core:file-remove', (fileID) => {
+    this.on('file-remove', (fileID) => {
       this.removeFile(fileID)
     })
 
-    this.on('core:upload-started', (fileID, upload) => {
-      const updatedFiles = Object.assign({}, this.getState().files)
-      const updatedFile = Object.assign({}, updatedFiles[fileID],
-        Object.assign({}, {
-          progress: Object.assign({}, updatedFiles[fileID].progress, {
-            uploadStarted: Date.now(),
-            uploadComplete: false,
-            percentage: 0,
-            bytesUploaded: 0
-          })
-        }
-      ))
-      updatedFiles[fileID] = updatedFile
-
-      this.setState({files: updatedFiles})
+    this.on('upload-started', (fileID, upload) => {
+      this.setFileState(fileID, {
+        progress: Object.assign({}, this.getState().files[fileID].progress, {
+          uploadStarted: Date.now(),
+          uploadComplete: false,
+          percentage: 0,
+          bytesUploaded: 0
+        })
+      })
     })
 
     // upload progress events can occur frequently, especially when you have a good
     // connection to the remote server. Therefore, we are throtteling them to
     // prevent accessive function calls.
     // see also: https://github.com/tus/tus-js-client/commit/9940f27b2361fd7e10ba58b09b60d82422183bbb
-    const throttledCalculateProgress = throttle(this.calculateProgress, 100, {leading: true, trailing: false})
+    const _throttledCalculateProgress = throttle(this._calculateProgress, 100, { leading: true, trailing: false })
 
-    this.on('core:upload-progress', (data) => {
-      throttledCalculateProgress(data)
-    })
+    this.on('upload-progress', _throttledCalculateProgress)
 
-    this.on('core:upload-success', (fileID, uploadResp, uploadURL) => {
-      const updatedFiles = Object.assign({}, this.getState().files)
-      const updatedFile = Object.assign({}, updatedFiles[fileID], {
-        progress: Object.assign({}, updatedFiles[fileID].progress, {
+    this.on('upload-success', (fileID, uploadResp, uploadURL) => {
+      this.setFileState(fileID, {
+        progress: Object.assign({}, this.getState().files[fileID].progress, {
           uploadComplete: true,
           percentage: 100
         }),
         uploadURL: uploadURL,
         isPaused: false
       })
-      updatedFiles[fileID] = updatedFile
-
-      this.setState({
-        files: updatedFiles
-      })
 
-      this.calculateTotalProgress()
+      this._calculateTotalProgress()
     })
 
-    this.on('core:update-meta', (data, fileID) => {
-      this.updateMeta(data, fileID)
-    })
-
-    this.on('core:preprocess-progress', (fileID, progress) => {
-      const files = Object.assign({}, this.getState().files)
-      files[fileID] = Object.assign({}, files[fileID], {
-        progress: Object.assign({}, files[fileID].progress, {
+    this.on('preprocess-progress', (fileID, progress) => {
+      this.setFileState(fileID, {
+        progress: Object.assign({}, this.getState().files[fileID].progress, {
           preprocess: progress
         })
       })
-
-      this.setState({ files: files })
     })
-    this.on('core:preprocess-complete', (fileID) => {
+
+    this.on('preprocess-complete', (fileID) => {
       const files = Object.assign({}, this.getState().files)
       files[fileID] = Object.assign({}, files[fileID], {
         progress: Object.assign({}, files[fileID].progress)
@@ -693,17 +698,16 @@ class Uppy {
 
       this.setState({ files: files })
     })
-    this.on('core:postprocess-progress', (fileID, progress) => {
-      const files = Object.assign({}, this.getState().files)
-      files[fileID] = Object.assign({}, files[fileID], {
-        progress: Object.assign({}, files[fileID].progress, {
+
+    this.on('postprocess-progress', (fileID, progress) => {
+      this.setFileState(fileID, {
+        progress: Object.assign({}, this.getState().files[fileID].progress, {
           postprocess: progress
         })
       })
-
-      this.setState({ files: files })
     })
-    this.on('core:postprocess-complete', (fileID) => {
+
+    this.on('postprocess-complete', (fileID) => {
       const files = Object.assign({}, this.getState().files)
       files[fileID] = Object.assign({}, files[fileID], {
         progress: Object.assign({}, files[fileID].progress)
@@ -716,6 +720,11 @@ class Uppy {
       this.setState({ files: files })
     })
 
+    this.on('restored', () => {
+      // Files may have changed--ensure progress is still accurate.
+      this._calculateTotalProgress()
+    })
+
     // show informer if offline
     if (typeof window !== 'undefined') {
       window.addEventListener('online', () => this.updateOnlineStatus())
@@ -814,7 +823,7 @@ class Uppy {
    * @param function method description
    */
   iteratePlugins (method) {
-    Object.keys(this.plugins).forEach((pluginType) => {
+    Object.keys(this.plugins).forEach(pluginType => {
       this.plugins[pluginType].forEach(method)
     })
   }
@@ -843,13 +852,11 @@ class Uppy {
   close () {
     this.reset()
 
+    this._storeUnsubscribe()
+
     this.iteratePlugins((plugin) => {
       plugin.uninstall()
     })
-
-    if (this.socket) {
-      this.socket.close()
-    }
   }
 
   /**
@@ -871,7 +878,7 @@ class Uppy {
       }
     })
 
-    this.emit('core:info-visible')
+    this.emit('info-visible')
 
     window.clearTimeout(this.infoTimeoutID)
     if (duration === 0) {
@@ -884,13 +891,13 @@ class Uppy {
   }
 
   hideInfo () {
-    const newInfo = Object.assign({}, this.state.info, {
+    const newInfo = Object.assign({}, this.getState().info, {
       isHidden: true
     })
     this.setState({
       info: newInfo
     })
-    this.emit('core:info-hidden')
+    this.emit('info-hidden')
   }
 
   /**
@@ -927,13 +934,13 @@ class Uppy {
     }
   }
 
-  initSocket (opts) {
-    if (!this.socket) {
-      this.socket = new UppySocket(opts)
-    }
+  // _initSocket (opts) {
+  //   if (!this.socket) {
+  //     this.socket = new UppySocket(opts)
+  //   }
 
-    return this.socket
-  }
+  //   return this.socket
+  // }
 
   /**
    * Initializes actions, installs all plugins (by iterating on them and calling `install`), sets options
@@ -952,12 +959,12 @@ class Uppy {
   restore (uploadID) {
     this.log(`Core: attempting to restore upload "${uploadID}"`)
 
-    if (!this.state.currentUploads[uploadID]) {
-      this.removeUpload(uploadID)
+    if (!this.getState().currentUploads[uploadID]) {
+      this._removeUpload(uploadID)
       return Promise.reject(new Error('Nonexistent upload'))
     }
 
-    return this.runUpload(uploadID)
+    return this._runUpload(uploadID)
   }
 
   /**
@@ -966,16 +973,16 @@ class Uppy {
    * @param {Array<string>} fileIDs File IDs to include in this upload.
    * @return {string} ID of this upload.
    */
-  createUpload (fileIDs) {
+  _createUpload (fileIDs) {
     const uploadID = cuid()
 
-    this.emit('core:upload', {
+    this.emit('upload', {
       id: uploadID,
       fileIDs: fileIDs
     })
 
     this.setState({
-      currentUploads: Object.assign({}, this.state.currentUploads, {
+      currentUploads: Object.assign({}, this.getState().currentUploads, {
         [uploadID]: {
           fileIDs: fileIDs,
           step: 0
@@ -991,8 +998,8 @@ class Uppy {
    *
    * @param {string} uploadID The ID of the upload.
    */
-  removeUpload (uploadID) {
-    const currentUploads = Object.assign({}, this.state.currentUploads)
+  _removeUpload (uploadID) {
+    const currentUploads = Object.assign({}, this.getState().currentUploads)
     delete currentUploads[uploadID]
 
     this.setState({
@@ -1005,8 +1012,8 @@ class Uppy {
    *
    * @private
    */
-  runUpload (uploadID) {
-    const uploadData = this.state.currentUploads[uploadID]
+  _runUpload (uploadID) {
+    const uploadData = this.getState().currentUploads[uploadID]
     const fileIDs = uploadData.fileIDs
     const restoreStep = uploadData.step
 
@@ -1023,11 +1030,12 @@ class Uppy {
       }
 
       lastStep = lastStep.then(() => {
-        const currentUpload = Object.assign({}, this.state.currentUploads[uploadID], {
+        const { currentUploads } = this.getState()
+        const currentUpload = Object.assign({}, currentUploads[uploadID], {
           step: step
         })
         this.setState({
-          currentUploads: Object.assign({}, this.state.currentUploads, {
+          currentUploads: Object.assign({}, currentUploads, {
             [uploadID]: currentUpload
           })
         })
@@ -1040,15 +1048,23 @@ class Uppy {
     // Not returning the `catch`ed promise, because we still want to return a rejected
     // promise from this method if the upload failed.
     lastStep.catch((err) => {
-      this.emit('core:error', err)
+      this.emit('error', err)
 
-      this.removeUpload(uploadID)
+      this._removeUpload(uploadID)
     })
 
     return lastStep.then(() => {
-      this.emit('core:success', fileIDs)
+      const files = fileIDs.map((fileID) => this.getFile(fileID))
+      const successful = files.filter((file) => !file.error)
+      const failed = files.filter((file) => file.error)
+      this.emit('complete', { successful, failed })
 
-      this.removeUpload(uploadID)
+      // Compatibility with pre-0.21
+      this.emit('success', fileIDs)
+
+      this._removeUpload(uploadID)
+
+      return { successful, failed }
     })
   }
 
@@ -1062,13 +1078,13 @@ class Uppy {
       this.log('No uploader type plugins are used', 'warning')
     }
 
-    const isMinNumberOfFilesReached = this.checkMinNumberOfFiles()
+    const isMinNumberOfFilesReached = this._checkMinNumberOfFiles()
     if (!isMinNumberOfFilesReached) {
       return Promise.reject(new Error('Minimum number of files has not been reached'))
     }
 
     const beforeUpload = Promise.resolve()
-      .then(() => this.opts.onBeforeUpload(this.state.files))
+      .then(() => this.opts.onBeforeUpload(this.getState().files))
 
     return beforeUpload.catch((err) => {
       const message = typeof err === 'object' ? err.message : err
@@ -1076,7 +1092,7 @@ class Uppy {
       return Promise.reject(new Error(`onBeforeUpload: ${message}`))
     }).then(() => {
       const waitingFileIDs = []
-      Object.keys(this.state.files).forEach((fileID) => {
+      Object.keys(this.getState().files).forEach((fileID) => {
         const file = this.getFile(fileID)
 
         if (!file.progress.uploadStarted || file.isRemote) {
@@ -1084,8 +1100,8 @@ class Uppy {
         }
       })
 
-      const uploadID = this.createUpload(waitingFileIDs)
-      return this.runUpload(uploadID)
+      const uploadID = this._createUpload(waitingFileIDs)
+      return this._runUpload(uploadID)
     })
   }
 }
@@ -1093,3 +1109,5 @@ class Uppy {
 module.exports = function (opts) {
   return new Uppy(opts)
 }
+// Expose class constructor.
+module.exports.Uppy = Uppy

File diff suppressed because it is too large
+ 1 - 1
src/core/Core.test.js


+ 19 - 24
src/plugins/Plugin.js → src/core/Plugin.js

@@ -1,5 +1,4 @@
-const yo = require('yo-yo')
-const nanoraf = require('nanoraf')
+const preact = require('preact')
 const { findDOMElement } = require('../core/Utils')
 
 /**
@@ -12,8 +11,8 @@ const { findDOMElement } = require('../core/Utils')
  * @return {array | string} files or success/fail message
  */
 module.exports = class Plugin {
-  constructor (core, opts) {
-    this.core = core
+  constructor (uppy, opts) {
+    this.uppy = uppy
     this.opts = opts || {}
 
     this.update = this.update.bind(this)
@@ -23,14 +22,14 @@ module.exports = class Plugin {
   }
 
   getPluginState () {
-    return this.core.state.plugins[this.id]
+    return this.uppy.state.plugins[this.id]
   }
 
   setPluginState (update) {
-    const plugins = Object.assign({}, this.core.state.plugins)
+    const plugins = Object.assign({}, this.uppy.state.plugins)
     plugins[this.id] = Object.assign({}, plugins[this.id], update)
 
-    this.core.setState({
+    this.uppy.setState({
       plugins: plugins
     })
   }
@@ -58,25 +57,21 @@ module.exports = class Plugin {
 
     const targetElement = findDOMElement(target)
 
-    // Set up nanoraf.
-    this.updateUI = nanoraf((state) => {
-      this.el = yo.update(this.el, this.render(state))
-    })
-
     if (targetElement) {
-      this.core.log(`Installing ${callerPluginName} to a DOM element`)
+      this.updateUI = (state) => {
+        this.el = preact.render(this.render(state), targetElement, this.el)
+      }
+
+      this.uppy.log(`Installing ${callerPluginName} to a DOM element`)
 
       // clear everything inside the target container
       if (this.opts.replaceTargetContent) {
         targetElement.innerHTML = ''
       }
 
-      this.el = plugin.render(this.core.state)
-      targetElement.appendChild(this.el)
-
-      this.target = targetElement
+      this.el = preact.render(this.render(this.uppy.state), targetElement)
 
-      return targetElement
+      return this.el
     }
 
     let targetPlugin
@@ -87,7 +82,7 @@ module.exports = class Plugin {
       // Targeting a plugin type
       const Target = target
       // Find the target plugin instance.
-      this.core.iteratePlugins((plugin) => {
+      this.uppy.iteratePlugins((plugin) => {
         if (plugin instanceof Target) {
           targetPlugin = plugin
           return false
@@ -97,12 +92,12 @@ module.exports = class Plugin {
 
     if (targetPlugin) {
       const targetPluginName = targetPlugin.id
-      this.core.log(`Installing ${callerPluginName} to ${targetPluginName}`)
-      this.target = targetPlugin
-      return targetPlugin.addTarget(plugin)
+      this.uppy.log(`Installing ${callerPluginName} to ${targetPluginName}`)
+      this.el = targetPlugin.addTarget(plugin)
+      return this.el
     }
 
-    this.core.log(`Not installing ${callerPluginName}`)
+    this.uppy.log(`Not installing ${callerPluginName}`)
     throw new Error(`Invalid target option given to ${callerPluginName}`)
   }
 
@@ -118,7 +113,7 @@ module.exports = class Plugin {
     if (this.el && this.el.parentNode) {
       this.el.parentNode.removeChild(this.el)
     }
-    this.target = null
+    // this.target = null
   }
 
   install () {

+ 46 - 13
src/core/Utils.js

@@ -121,7 +121,10 @@ function getFileType (file) {
     'markdown': 'text/markdown',
     'mp4': 'video/mp4',
     'mp3': 'audio/mp3',
-    'svg': 'image/svg+xml'
+    'svg': 'image/svg+xml',
+    'jpg': 'image/jpeg',
+    'png': 'image/png',
+    'gif': 'image/gif'
   }
 
   const fileExtension = file.name ? getFileNameAndExtension(file.name).extension : null
@@ -217,7 +220,6 @@ function getProportionalHeight (img, width) {
  */
 function createThumbnail (file, targetWidth) {
   const originalUrl = URL.createObjectURL(file.data)
-
   const onload = new Promise((resolve, reject) => {
     const image = new Image()
     image.src = originalUrl
@@ -490,10 +492,10 @@ function getSocketHost (url) {
 }
 
 function _emitSocketProgress (uploader, progressData, file) {
-  const {progress, bytesUploaded, bytesTotal} = progressData
+  const { progress, bytesUploaded, bytesTotal } = progressData
   if (progress) {
-    uploader.core.log(`Upload progress: ${progress}`)
-    uploader.core.emitter.emit('core:upload-progress', {
+    uploader.uppy.log(`Upload progress: ${progress}`)
+    uploader.uppy.emit('upload-progress', {
       uploader,
       id: file.id,
       bytesUploaded: bytesUploaded,
@@ -519,13 +521,6 @@ function settle (promises) {
   )
 
   return wait.then(() => {
-    if (rejections.length === promises.length) {
-      // Very ad-hoc multiple-error reporting, should wrap this in a
-      // CombinedError or whatever kind of error class instead.
-      const error = rejections[0]
-      error.errors = rejections
-      return Promise.reject(error)
-    }
     return {
       successful: resolutions,
       failed: rejections
@@ -533,6 +528,43 @@ function settle (promises) {
   })
 }
 
+/**
+ * Limit the amount of simultaneously pending Promises.
+ * Returns a function that, when passed a function `fn`,
+ * will make sure that at most `limit` calls to `fn` are pending.
+ *
+ * @param {number} limit
+ * @return {function()}
+ */
+function limitPromises (limit) {
+  let pending = 0
+  const queue = []
+  return (fn) => {
+    return (...args) => {
+      const call = () => {
+        pending++
+        const promise = fn(...args)
+        promise.then(onfinish, onfinish)
+        return promise
+      }
+
+      if (pending >= limit) {
+        return new Promise((resolve, reject) => {
+          queue.push(() => {
+            call().then(resolve, reject)
+          })
+        })
+      }
+      return call()
+    }
+  }
+  function onfinish () {
+    pending--
+    const next = queue.shift()
+    if (next) next()
+  }
+}
+
 module.exports = {
   generateFileID,
   toArray,
@@ -560,5 +592,6 @@ module.exports = {
   findAllDOMElements,
   getSocketHost,
   emitSocketProgress,
-  settle
+  settle,
+  limitPromises
 }

+ 61 - 4
src/core/Utils.test.js

@@ -261,6 +261,16 @@ describe('core/utils', () => {
   })
 
   describe('createThumbnail', () => {
+    const RealCreateObjectUrl = global.URL.createObjectURL
+
+    beforeEach(() => {
+      global.URL.createObjectURL = jest.fn().mockReturnValue('newUrl')
+    })
+
+    afterEach(() => {
+      global.URL.createObjectURL = RealCreateObjectUrl
+    })
+
     xit(
       'should create a thumbnail of the specified image at the specified width',
       () => {}
@@ -345,14 +355,15 @@ describe('core/utils', () => {
   })
 
   describe('settle', () => {
-    it('should reject if all input promises reject', () => {
+    it('should resolve even if all input promises reject', () => {
       return expect(
         utils.settle([
           Promise.reject(new Error('oops')),
           Promise.reject(new Error('this went wrong'))
         ])
-      ).rejects.toMatchObject({
-        message: 'oops'
+      ).resolves.toMatchObject({
+        successful: [],
+        failed: [ new Error('oops'), new Error('this went wrong') ]
       })
     })
 
@@ -365,7 +376,53 @@ describe('core/utils', () => {
         ])
       ).resolves.toMatchObject({
         successful: ['resolved', 'also-resolved'],
-        failed: [{ message: 'rejected' }]
+        failed: [new Error('rejected')]
+      })
+    })
+  })
+
+  describe('limitPromises', () => {
+    let pending = 0
+    function fn () {
+      pending++
+      return new Promise((resolve) => setTimeout(resolve, 10))
+        .then(() => pending--)
+    }
+
+    it('should run at most N promises at the same time', () => {
+      const limit = utils.limitPromises(4)
+      const fn2 = limit(fn)
+
+      const result = Promise.all([
+        fn2(), fn2(), fn2(), fn2(),
+        fn2(), fn2(), fn2(), fn2(),
+        fn2(), fn2()
+      ])
+
+      expect(pending).toBe(4)
+      setTimeout(() => {
+        expect(pending).toBe(4)
+      }, 10)
+
+      return result.then(() => {
+        expect(pending).toBe(0)
+      })
+    })
+
+    it('should accept Infinity as limit', () => {
+      const limit = utils.limitPromises(Infinity)
+      const fn2 = limit(fn)
+
+      const result = Promise.all([
+        fn2(), fn2(), fn2(), fn2(),
+        fn2(), fn2(), fn2(), fn2(),
+        fn2(), fn2()
+      ])
+
+      expect(pending).toBe(10)
+
+      return result.then(() => {
+        expect(pending).toBe(0)
       })
     })
   })

+ 0 - 21
src/generic-provider-views/AuthView.js

@@ -1,21 +0,0 @@
-const html = require('yo-yo')
-const onload = require('on-load')
-const LoaderView = require('./Loader')
-
-module.exports = (props) => {
-  const demoLink = props.demo ? html`<button class="UppyProvider-authBtnDemo" onclick=${props.handleDemoAuth}>Proceed with Demo Account</button>` : null
-  const AuthBlock = () => html`
-    <div class="UppyProvider-auth">
-      <h1 class="UppyProvider-authTitle">Please authenticate with <span class="UppyProvider-authTitleName">${props.pluginName}</span><br> to select files</h1>
-      <button type="button" class="UppyProvider-authBtn" onclick=${props.handleAuth}>Authenticate</button>
-      ${demoLink}
-    </div>
-  `
-  return onload(html`
-    <div style="height: 100%;">
-      ${props.checkAuthInProgress
-        ? LoaderView()
-        : AuthBlock()
-      }
-    </div>`, props.checkAuth, null, `auth${props.pluginName}`)
-}

+ 0 - 9
src/generic-provider-views/Breadcrumb.js

@@ -1,9 +0,0 @@
-const html = require('yo-yo')
-
-module.exports = (props) => {
-  return html`
-    <li>
-      <button type="button" onclick=${props.getFolder}>${props.title}</button>
-    </li>
-  `
-}

+ 0 - 9
src/generic-provider-views/Loader.js

@@ -1,9 +0,0 @@
-const html = require('yo-yo')
-
-module.exports = (props) => {
-  return html`
-    <div class="UppyProvider-loading">
-      <span>Loading...</span>
-    </div>
-  `
-}

+ 0 - 41
src/generic-provider-views/Table.js

@@ -1,41 +0,0 @@
-const html = require('yo-yo')
-const Row = require('./TableRow')
-
-module.exports = (props) => {
-  // const headers = props.columns.map((column) => {
-  //   return html`
-  //     <th class="BrowserTable-headerColumn BrowserTable-column" onclick=${props.sortByTitle}>
-  //       ${column.name}
-  //     </th>
-  //   `
-  // })
-
-  // <thead class="BrowserTable-header">
-  //   <tr>${headers}</tr>
-  // </thead>
-
-  return html`
-    <table class="BrowserTable" onscroll=${props.handleScroll}>
-      <tbody>
-        ${props.folders.map((folder) => {
-          return Row({
-            title: props.getItemName(folder),
-            active: props.activeRow(folder),
-            getItemIcon: () => props.getItemIcon(folder),
-            handleClick: () => props.handleFolderClick(folder),
-            columns: props.columns
-          })
-        })}
-        ${props.files.map((file) => {
-          return Row({
-            title: props.getItemName(file),
-            active: props.activeRow(file),
-            getItemIcon: () => props.getItemIcon(file),
-            handleClick: () => props.handleFileClick(file),
-            columns: props.columns
-          })
-        })}
-      </tbody>
-    </table>
-  `
-}

+ 0 - 9
src/generic-provider-views/TableColumn.js

@@ -1,9 +0,0 @@
-const html = require('yo-yo')
-
-module.exports = (props) => {
-  return html`
-    <td class="BrowserTable-rowColumn BrowserTable-column">
-      ${props.getItemIcon()} ${props.value}
-    </td>
-  `
-}

+ 0 - 14
src/generic-provider-views/TableRow.js

@@ -1,14 +0,0 @@
-const html = require('yo-yo')
-const Column = require('./TableColumn')
-
-module.exports = (props) => {
-  const classes = props.active ? 'BrowserTable-row is-active' : 'BrowserTable-row'
-  return html`
-    <tr onclick=${props.handleClick} class=${classes}>
-      ${Column({
-        getItemIcon: props.getItemIcon,
-        value: props.title
-      })}
-    </tr>
-  `
-}

+ 9 - 41
src/index.js

@@ -1,19 +1,19 @@
-const Core = require('./core/index.js')
+const Core = require('./core')
 
 // Parent
-const Plugin = require('./plugins/Plugin')
+const Plugin = require('./core/Plugin')
 
 // Orchestrators
-const Dashboard = require('./plugins/Dashboard/index.js')
+const Dashboard = require('./plugins/Dashboard')
 
 // Acquirers
 const Dummy = require('./plugins/Dummy')
-const DragDrop = require('./plugins/DragDrop/index.js')
+const DragDrop = require('./plugins/DragDrop')
 const FileInput = require('./plugins/FileInput.js')
-const GoogleDrive = require('./plugins/GoogleDrive/index.js')
-const Dropbox = require('./plugins/Dropbox/index.js')
-const Instagram = require('./plugins/Instagram/index.js')
-const Webcam = require('./plugins/Webcam/index.js')
+const GoogleDrive = require('./plugins/GoogleDrive')
+const Dropbox = require('./plugins/Dropbox')
+const Instagram = require('./plugins/Instagram')
+const Webcam = require('./plugins/Webcam')
 
 // Progressindicators
 const StatusBar = require('./plugins/StatusBar')
@@ -21,7 +21,6 @@ const ProgressBar = require('./plugins/ProgressBar.js')
 const Informer = require('./plugins/Informer.js')
 
 // Modifiers
-const MetaData = require('./plugins/MetaData.js')
 
 // Uploaders
 const Tus = require('./plugins/Tus')
@@ -29,7 +28,7 @@ const XHRUpload = require('./plugins/XHRUpload')
 const Transloadit = require('./plugins/Transloadit')
 const AwsS3 = require('./plugins/AwsS3')
 
-// Other?
+// Helpers and utilities
 const GoldenRetriever = require('./plugins/GoldenRetriever')
 const ReduxDevTools = require('./plugins/ReduxDevTools')
 const ReduxStore = require('./plugins/Redux')
@@ -51,39 +50,8 @@ module.exports = {
   Transloadit,
   AwsS3,
   Dashboard,
-  MetaData,
   Webcam,
   GoldenRetriever,
   ReduxDevTools,
   ReduxStore
 }
-
-Object.defineProperty(module.exports, 'RestoreFiles', {
-  enumerable: true,
-  configurable: true,
-  get: () => {
-    console.warn('Uppy.RestoreFiles is deprecated and will be removed in v0.22. Use Uppy.GoldenRetriever instead.')
-    Object.defineProperty(module.exports, 'RestoreFiles', {
-      enumerable: true,
-      configurable: true,
-      writable: true,
-      value: GoldenRetriever
-    })
-    return GoldenRetriever
-  }
-})
-
-Object.defineProperty(module.exports, 'Tus10', {
-  enumerable: true,
-  configurable: true,
-  get: () => {
-    console.warn('Uppy.Tus10 is deprecated and will be removed in v0.22. Use Uppy.Tus instead.')
-    Object.defineProperty(module.exports, 'Tus10', {
-      enumerable: true,
-      configurable: true,
-      writable: true,
-      value: Tus
-    })
-    return Tus
-  }
-})

+ 58 - 46
src/plugins/AwsS3/index.js

@@ -1,8 +1,10 @@
-const Plugin = require('../Plugin')
+const Plugin = require('../../core/Plugin')
+const Translator = require('../../core/Translator')
+const XHRUpload = require('../XHRUpload')
 
 module.exports = class AwsS3 extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'uploader'
     this.id = 'AwsS3'
     this.title = 'AWS S3'
@@ -22,6 +24,9 @@ module.exports = class AwsS3 extends Plugin {
     this.locale = Object.assign({}, defaultLocale, this.opts.locale)
     this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
 
+    this.translator = new Translator({ locale: this.locale })
+    this.i18n = this.translator.translate.bind(this.translator)
+
     this.prepareUpload = this.prepareUpload.bind(this)
   }
 
@@ -40,62 +45,37 @@ module.exports = class AwsS3 extends Plugin {
 
   prepareUpload (fileIDs) {
     fileIDs.forEach((id) => {
-      this.core.emit('core:preprocess-progress', id, {
+      this.uppy.emit('preprocess-progress', id, {
         mode: 'determinate',
-        message: this.locale.strings.preparingUpload,
+        message: this.i18n('preparingUpload'),
         value: 0
       })
     })
 
-    this.core.setState({
-      xhrUpload: Object.assign({}, this.core.state.xhrUpload, {
-        responseUrlFieldName: 'location',
-        getResponseData (xhr) {
-          // If no response, we've hopefully done a PUT request to the file
-          // in the bucket on its full URL.
-          if (!xhr.responseXML) {
-            return { location: xhr.responseURL }
-          }
-          function getValue (key) {
-            const el = xhr.responseXML.querySelector(key)
-            return el ? el.textContent : ''
-          }
-          return {
-            location: getValue('Location'),
-            bucket: getValue('Bucket'),
-            key: getValue('Key'),
-            etag: getValue('ETag')
-          }
-        },
-        getResponseError (xhr) {
-          // If no response, we don't have a specific error message, use the default.
-          if (!xhr.responseXML) {
-            return
-          }
-          const error = xhr.responseXML.querySelector('Error > Message')
-          return new Error(error.textContent)
-        }
-      })
-    })
-
     return Promise.all(
       fileIDs.map((id) => {
-        const file = this.core.getFile(id)
+        const file = this.uppy.getFile(id)
         const paramsPromise = Promise.resolve()
           .then(() => this.opts.getUploadParameters(file))
         return paramsPromise.then((params) => {
-          this.core.emit('core:preprocess-progress', file.id, {
+          this.uppy.emit('preprocess-progress', file.id, {
             mode: 'determinate',
-            message: this.locale.strings.preparingUpload,
+            message: this.i18n('preparingUpload'),
             value: 1
           })
           return params
+        }).catch((error) => {
+          this.uppy.emit('upload-error', file.id, error)
         })
       })
     ).then((responses) => {
       const updatedFiles = {}
       fileIDs.forEach((id, index) => {
-        const file = this.core.getFile(id)
+        const file = this.uppy.getFile(id)
+        if (file.error) {
+          return
+        }
+
         const {
           method = 'post',
           url,
@@ -106,7 +86,6 @@ module.exports = class AwsS3 extends Plugin {
           method,
           formData: method.toLowerCase() === 'post',
           endpoint: url,
-          fieldName: 'file',
           metaFields: Object.keys(fields)
         }
 
@@ -122,21 +101,54 @@ module.exports = class AwsS3 extends Plugin {
         updatedFiles[id] = updatedFile
       })
 
-      this.core.setState({
-        files: Object.assign({}, this.core.getState().files, updatedFiles)
+      this.uppy.setState({
+        files: Object.assign({}, this.uppy.getState().files, updatedFiles)
       })
 
       fileIDs.forEach((id) => {
-        this.core.emit('core:preprocess-complete', id)
+        this.uppy.emit('preprocess-complete', id)
       })
     })
   }
 
   install () {
-    this.core.addPreProcessor(this.prepareUpload)
+    this.uppy.addPreProcessor(this.prepareUpload)
+
+    this.uppy.use(XHRUpload, {
+      fieldName: 'file',
+      responseUrlFieldName: 'location',
+      getResponseData (xhr) {
+        // If no response, we've hopefully done a PUT request to the file
+        // in the bucket on its full URL.
+        if (!xhr.responseXML) {
+          return { location: xhr.responseURL }
+        }
+        function getValue (key) {
+          const el = xhr.responseXML.querySelector(key)
+          return el ? el.textContent : ''
+        }
+        return {
+          location: getValue('Location'),
+          bucket: getValue('Bucket'),
+          key: getValue('Key'),
+          etag: getValue('ETag')
+        }
+      },
+      getResponseError (xhr) {
+        // If no response, we don't have a specific error message, use the default.
+        if (!xhr.responseXML) {
+          return
+        }
+        const error = xhr.responseXML.querySelector('Error > Message')
+        return new Error(error.textContent)
+      }
+    })
   }
 
   uninstall () {
-    this.core.removePreProcessor(this.prepareUpload)
+    const uploader = this.uppy.getPlugin('XHRUpload')
+    this.uppy.removePlugin(uploader)
+
+    this.uppy.removePreProcessor(this.prepareUpload)
   }
 }

+ 35 - 20
src/plugins/Dashboard/ActionBrowseTagline.js

@@ -1,23 +1,38 @@
-const html = require('yo-yo')
+const { h, Component } = require('preact')
 
-module.exports = (props) => {
-  const input = html`
-    <input class="UppyDashboard-input" type="file" name="files[]" multiple="true"
-           onchange=${props.handleInputChange} />
-  `
+class ActionBrowseTagline extends Component {
+  constructor (props) {
+    super(props)
+    this.handleClick = this.handleClick.bind(this)
+  }
 
-  return html`
-    <span>
-      ${props.acquirers.length === 0
-        ? props.i18n('dropPaste')
-        : props.i18n('dropPasteImport')
-      }
-      <button type="button"
-              class="UppyDashboard-browse"
-              onclick=${(ev) => {
-                input.click()
-              }}>${props.i18n('browse')}</button>
-      ${input}
-    </span>
-  `
+  handleClick (ev) {
+    this.input.click()
+  }
+
+  render () {
+    return (
+      <span>
+        {this.props.acquirers.length === 0
+          ? this.props.i18n('dropPaste')
+          : this.props.i18n('dropPasteImport')
+        } <button type="button" class="uppy-Dashboard-browse" onclick={this.handleClick}>
+          {this.props.i18n('browse')}
+        </button>
+        <input class="uppy-Dashboard-input"
+          hidden="true"
+          aria-hidden="true"
+          tabindex="-1"
+          type="file"
+          name="files[]"
+          multiple="true"
+          onchange={this.props.handleInputChange}
+          ref={(input) => {
+            this.input = input
+          }} />
+      </span>
+    )
+  }
 }
+
+module.exports = ActionBrowseTagline

+ 68 - 145
src/plugins/Dashboard/Dashboard.js

@@ -1,161 +1,84 @@
-const html = require('yo-yo')
 const FileList = require('./FileList')
 const Tabs = require('./Tabs')
 const FileCard = require('./FileCard')
-const UploadBtn = require('./UploadBtn')
-const { isTouchDevice, toArray } = require('../../core/Utils')
+const classNames = require('classnames')
+const { isTouchDevice } = require('../../core/Utils')
 const { closeIcon } = require('./icons')
+const { h } = require('preact')
 
 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
+// https://github.com/ghosh/micromodal
 
-module.exports = function Dashboard (props) {
-  function handleInputChange (ev) {
-    ev.preventDefault()
-    const files = toArray(ev.target.files)
-
-    files.forEach((file) => {
-      props.addFile({
-        source: props.id,
-        name: file.name,
-        type: file.type,
-        data: file
-      })
-    })
-  }
-
-  // @TODO Exprimental, work in progress
-  // no names, weird API, Chrome-only http://stackoverflow.com/a/22940020
-  function handlePaste (ev) {
-    ev.preventDefault()
-
-    const files = toArray(ev.clipboardData.items)
-    files.forEach((file) => {
-      if (file.kind !== 'file') return
-
-      const blob = file.getAsFile()
-      if (!blob) {
-        props.log('[Dashboard] File pasted, but the file blob is empty')
-        props.info('Error pasting file', 'error')
-        return
-      }
-      props.log('[Dashboard] File pasted')
-      props.addFile({
-        source: props.id,
-        name: file.name,
-        type: file.type,
-        data: blob
-      })
-    })
-  }
-
-  return html`
-    <div class="Uppy UppyTheme--default UppyDashboard
-                          ${isTouchDevice() ? 'Uppy--isTouchDevice' : ''}
-                          ${props.semiTransparent ? 'UppyDashboard--semiTransparent' : ''}
-                          ${!props.inline ? 'UppyDashboard--modal' : ''}
-                          ${props.isWide ? 'UppyDashboard--wide' : ''}"
-          aria-hidden="${props.inline ? 'false' : props.modal.isHidden}"
-          aria-label="${!props.inline
-                       ? props.i18n('dashboardWindowTitle')
-                       : props.i18n('dashboardTitle')}"
-          role="dialog"
-          onpaste=${handlePaste}
-          onload=${() => props.updateDashboardElWidth()}>
-
-    <div class="UppyDashboard-overlay" onclick=${props.handleClickOutside}></div>
-
-    <div class="UppyDashboard-inner"
-         tabindex="0"
-         style="
-          ${props.inline && props.maxWidth ? `max-width: ${props.maxWidth}px;` : ''}
-          ${props.inline && props.maxHeight ? `max-height: ${props.maxHeight}px;` : ''}
-         ">
-      <button class="UppyDashboard-close"
-              type="button"
-              aria-label="${props.i18n('closeModal')}"
-              title="${props.i18n('closeModal')}"
-              onclick=${props.closeModal}>${closeIcon()}</button>
-
-      <div class="UppyDashboard-innerWrap">
-
-        ${Tabs({
-          files: props.files,
-          handleInputChange: handleInputChange,
-          acquirers: props.acquirers,
-          panelSelectorPrefix: props.panelSelectorPrefix,
-          showPanel: props.showPanel,
-          i18n: props.i18n
-        })}
-
-        ${FileCard({
-          files: props.files,
-          fileCardFor: props.fileCardFor,
-          done: props.fileCardDone,
-          metaFields: props.metaFields,
-          log: props.log,
-          i18n: props.i18n
-        })}
-
-        <div class="UppyDashboard-filesContainer">
-
-          ${FileList({
-            acquirers: props.acquirers,
-            files: props.files,
-            handleInputChange: handleInputChange,
-            showFileCard: props.showFileCard,
-            showProgressDetails: props.showProgressDetails,
-            totalProgress: props.totalProgress,
-            totalFileCount: props.totalFileCount,
-            info: props.info,
-            note: props.note,
-            i18n: props.i18n,
-            log: props.log,
-            removeFile: props.removeFile,
-            pauseAll: props.pauseAll,
-            resumeAll: props.resumeAll,
-            pauseUpload: props.pauseUpload,
-            startUpload: props.startUpload,
-            cancelUpload: props.cancelUpload,
-            retryUpload: props.retryUpload,
-            resumableUploads: props.resumableUploads,
-            isWide: props.isWide
-          })}
+const renderInnerPanel = (props) => {
+  return <div style={{ width: '100%', height: '100%' }}>
+    <div class="uppy-DashboardContent-bar">
+      <div class="uppy-DashboardContent-title">
+        {props.i18n('importFrom')} {props.activePanel ? props.activePanel.name : null}
+      </div>
+      <button class="uppy-DashboardContent-back"
+        type="button"
+        onclick={props.hideAllPanels}>{props.i18n('done')}</button>
+    </div>
+    {props.getPlugin(props.activePanel.id).render(props.state)}
+  </div>
+}
 
-          <div class="UppyDashboard-actions">
-            ${!props.hideUploadButton && !props.autoProceed && props.newFiles.length > 0
-              ? UploadBtn({
-                i18n: props.i18n,
-                startUpload: props.startUpload,
-                newFileCount: props.newFiles.length
-              })
-              : null
-            }
+module.exports = function Dashboard (props) {
+  const dashboardClassName = classNames(
+    'uppy',
+    'uppy-Dashboard',
+    { 'Uppy--isTouchDevice': isTouchDevice() },
+    { 'uppy-Dashboard--semiTransparent': props.semiTransparent },
+    { 'uppy-Dashboard--modal': !props.inline },
+    { 'uppy-Dashboard--wide': props.isWide }
+  )
+
+  return (
+    <div class={dashboardClassName}
+      aria-hidden={props.inline ? 'false' : props.modal.isHidden}
+      aria-label={!props.inline ? props.i18n('dashboardWindowTitle') : props.i18n('dashboardTitle')}
+      onpaste={props.handlePaste}>
+
+      <div class="uppy-Dashboard-overlay" tabindex="-1" onclick={props.handleClickOutside} />
+
+      <div class="uppy-Dashboard-inner"
+        aria-modal={!props.inline && 'true'}
+        role={!props.inline && 'dialog'}
+        style={{
+          maxWidth: props.inline && props.maxWidth ? props.maxWidth : '',
+          maxHeight: props.inline && props.maxHeight ? props.maxHeight : ''
+        }}>
+        <button class="uppy-Dashboard-close"
+          type="button"
+          aria-label={props.i18n('closeModal')}
+          title={props.i18n('closeModal')}
+          onclick={props.closeModal}>
+          {closeIcon()}
+        </button>
+
+        <div class="uppy-Dashboard-innerWrap">
+          <Tabs {...props} />
+
+          <FileCard {...props} />
+
+          <div class="uppy-Dashboard-filesContainer">
+            <FileList {...props} />
           </div>
 
-        </div>
-
-        <div class="UppyDashboardContent-panel"
-             role="tabpanel"
-             aria-hidden="${props.activePanel ? 'false' : 'true'}">
-          <div class="UppyDashboardContent-bar">
-            <h2 class="UppyDashboardContent-title">
-              ${props.i18n('importFrom')} ${props.activePanel ? props.activePanel.name : null}
-            </h2>
-            <button class="UppyDashboardContent-back"
-                    type="button"
-                    onclick=${props.hideAllPanels}>${props.i18n('done')}</button>
+          <div class="uppy-DashboardContent-panel"
+            role="tabpanel"
+            id={props.activePanel && `uppy-DashboardContent-panel--${props.activePanel.id}`}
+            aria-hidden={props.activePanel ? 'false' : 'true'}>
+            {props.activePanel && renderInnerPanel(props)}
           </div>
-          ${props.activePanel ? props.getPlugin(props.activePanel.id).render(props.state) : ''}
-        </div>
 
-        <div class="UppyDashboard-progressindicators">
-          ${props.progressindicators.map((target) => {
-            return props.getPlugin(target.id).render(props.state)
-          })}
+          <div class="uppy-Dashboard-progressindicators">
+            {props.progressindicators.map((target) => {
+              return props.getPlugin(target.id).render(props.state)
+            })}
+          </div>
         </div>
-
       </div>
     </div>
-  </div>
-  `
+  )
 }

+ 77 - 47
src/plugins/Dashboard/FileCard.js

@@ -1,68 +1,98 @@
-const html = require('yo-yo')
 const getFileTypeIcon = require('./getFileTypeIcon')
 const { checkIcon } = require('./icons')
+const { h, Component } = require('preact')
 
-module.exports = function fileCard (props) {
-  const file = props.fileCardFor ? props.files[props.fileCardFor] : false
-  const meta = {}
+module.exports = class FileCard extends Component {
+  constructor (props) {
+    super(props)
+
+    this.meta = {}
+
+    this.tempStoreMetaOrSubmit = this.tempStoreMetaOrSubmit.bind(this)
+    this.renderMetaFields = this.renderMetaFields.bind(this)
+    this.handleClick = this.handleClick.bind(this)
+  }
+
+  tempStoreMetaOrSubmit (ev) {
+    const file = this.props.files[this.props.fileCardFor]
 
-  const tempStoreMetaOrSubmit = (ev) => {
     if (ev.keyCode === 13) {
-      props.done(meta, file.id)
+      ev.stopPropagation()
+      ev.preventDefault()
+      this.props.fileCardDone(this.meta, file.id)
+      return
     }
 
     const value = ev.target.value
     const name = ev.target.dataset.name
-    meta[name] = value
+    this.meta[name] = value
   }
 
-  function renderMetaFields (file) {
-    const metaFields = props.metaFields || []
+  renderMetaFields (file) {
+    const metaFields = this.props.metaFields || []
     return metaFields.map((field) => {
-      return html`<fieldset class="UppyDashboardFileCard-fieldset">
-        <label class="UppyDashboardFileCard-label">${field.name}</label>
-        <input class="UppyDashboardFileCard-input"
-               type="text"
-               data-name="${field.id}"
-               value="${file.meta[field.id]}"
-               placeholder="${field.placeholder || ''}"
-               onkeyup=${tempStoreMetaOrSubmit} /></fieldset>`
+      return <fieldset class="uppy-DashboardFileCard-fieldset">
+        <label class="uppy-DashboardFileCard-label">{field.name}</label>
+        <input class="uppy-DashboardFileCard-input"
+          type="text"
+          data-name={field.id}
+          value={file.meta[field.id]}
+          placeholder={field.placeholder}
+          onkeyup={this.tempStoreMetaOrSubmit}
+          onkeydown={this.tempStoreMetaOrSubmit}
+          onkeypress={this.tempStoreMetaOrSubmit} /></fieldset>
     })
   }
 
-  return html`<div class="UppyDashboardFileCard" aria-hidden="${!props.fileCardFor}">
-    <div class="UppyDashboardContent-bar">
-      <h2 class="UppyDashboardContent-title">Editing <span class="UppyDashboardContent-titleFile">${file.meta ? file.meta.name : file.name}</span></h2>
-      <button class="UppyDashboardContent-back" type="button" title="Finish editing file"
-              onclick=${() => props.done(meta, file.id)}>Done</button>
-    </div>
-    ${props.fileCardFor
-      ? html`<div class="UppyDashboardFileCard-inner">
-          <div class="UppyDashboardFileCard-preview" style="background-color: ${getFileTypeIcon(file.type).color}">
-            ${file.preview
-              ? html`<img alt="${file.name}" src="${file.preview}">`
-              : html`<div class="UppyDashboardItem-previewIconWrap">
-                <span class="UppyDashboardItem-previewIcon" style="color: ${getFileTypeIcon(file.type).color}">${getFileTypeIcon(file.type).icon}</span>
-                <svg class="UppyDashboardItem-previewIconBg" width="72" height="93" viewBox="0 0 72 93"><g><path d="M24.08 5h38.922A2.997 2.997 0 0 1 66 8.003v74.994A2.997 2.997 0 0 1 63.004 86H8.996A2.998 2.998 0 0 1 6 83.01V22.234L24.08 5z" fill="#FFF"/><path d="M24 5L6 22.248h15.007A2.995 2.995 0 0 0 24 19.244V5z" fill="#E4E4E4"/></g></svg>
-              </div>`
-            }
+  handleClick (ev) {
+    const file = this.props.files[this.props.fileCardFor]
+    this.props.fileCardDone(this.meta, file.id)
+  }
+
+  render () {
+    const file = this.props.files[this.props.fileCardFor]
+
+    return <div class="uppy-DashboardFileCard" aria-hidden={!this.props.fileCardFor}>
+      {this.props.fileCardFor &&
+        <div style="width: 100%; height: 100%;">
+          <div class="uppy-DashboardContent-bar">
+            <h2 class="uppy-DashboardContent-title">Editing <span class="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span></h2>
+            <button class="uppy-DashboardContent-back" type="button" title="Finish editing file"
+              onclick={this.handleClick}>Done</button>
           </div>
-          <div class="UppyDashboardFileCard-info">
-            <fieldset class="UppyDashboardFileCard-fieldset">
-              <label class="UppyDashboardFileCard-label">Name</label>
-              <input class="UppyDashboardFileCard-input" data-name="name" type="text" value="${file.meta.name}"
-                     onkeyup=${tempStoreMetaOrSubmit} />
-            </fieldset>
-            ${renderMetaFields(file)}
+          <div class="uppy-DashboardFileCard-inner">
+            <div class="uppy-DashboardFileCard-preview" style={{ backgroundColor: getFileTypeIcon(file.type).color }}>
+              {file.preview
+                ? <img alt={file.name} src={file.preview} />
+                : <div class="uppy-DashboardItem-previewIconWrap">
+                  <span class="uppy-DashboardItem-previewIcon" style={{ color: getFileTypeIcon(file.type).color }}>{getFileTypeIcon(file.type).icon}</span>
+                  <svg class="uppy-DashboardItem-previewIconBg" width="72" height="93" viewBox="0 0 72 93"><g><path d="M24.08 5h38.922A2.997 2.997 0 0 1 66 8.003v74.994A2.997 2.997 0 0 1 63.004 86H8.996A2.998 2.998 0 0 1 6 83.01V22.234L24.08 5z" fill="#FFF" /><path d="M24 5L6 22.248h15.007A2.995 2.995 0 0 0 24 19.244V5z" fill="#E4E4E4" /></g></svg>
+                </div>
+              }
+            </div>
+            <div class="uppy-DashboardFileCard-info">
+              <fieldset class="uppy-DashboardFileCard-fieldset">
+                <label class="uppy-DashboardFileCard-label">Name</label>
+                <input class="uppy-DashboardFileCard-input"
+                  type="text"
+                  data-name="name"
+                  value={file.meta.name || ''}
+                  placeholder="name"
+                  onkeyup={this.tempStoreMetaOrSubmit}
+                  onkeydown={this.tempStoreMetaOrSubmit}
+                  onkeypress={this.tempStoreMetaOrSubmit} />
+              </fieldset>
+              {this.renderMetaFields(file)}
+            </div>
           </div>
-        </div>`
-      : null
-    }
-    <div class="UppyDashboard-actions">
-      <button class="UppyButton--circular UppyButton--blue UppyDashboardFileCard-done"
+          <div class="uppy-Dashboard-actions">
+            <button class="UppyButton--circular UppyButton--blue uppy-DashboardFileCard-done"
               type="button"
               title="Finish editing file"
-              onclick=${() => props.done(meta, file.id)}>${checkIcon()}</button>
+              onclick={this.handleClick}>{checkIcon()}</button>
+          </div>
+        </div>
+      }
     </div>
-  </div>`
+  }
 }

+ 101 - 94
src/plugins/Dashboard/FileItem.js

@@ -1,4 +1,3 @@
-const html = require('yo-yo')
 const { getETA,
          getSpeed,
          prettyETA,
@@ -9,6 +8,8 @@ const prettyBytes = require('prettier-bytes')
 const FileItemProgress = require('./FileItemProgress')
 const getFileTypeIcon = require('./getFileTypeIcon')
 const { iconEdit, iconCopy, iconRetry } = require('./icons')
+const classNames = require('classnames')
+const { h } = require('preact')
 
 module.exports = function fileItem (props) {
   const file = props.file
@@ -22,7 +23,7 @@ module.exports = function fileItem (props) {
   const error = file.error || false
 
   const fileName = getFileNameAndExtension(file.meta.name).name
-  const truncatedFileName = props.isWide ? truncateString(fileName, 15) : fileName
+  const truncatedFileName = props.isWide ? truncateString(fileName, 14) : fileName
 
   const onPauseResumeCancelRetry = (ev) => {
     if (isUploaded) return
@@ -37,38 +38,47 @@ module.exports = function fileItem (props) {
     }
   }
 
-  return html`<li class="UppyDashboardItem
-                        ${uploadInProgress ? 'is-inprogress' : ''}
-                        ${isProcessing ? 'is-processing' : ''}
-                        ${isUploaded ? 'is-complete' : ''}
-                        ${isPaused ? 'is-paused' : ''}
-                        ${error ? 'is-error' : ''}
-                        ${props.resumableUploads ? 'is-resumable' : ''}"
-                  id="uppy_${file.id}"
-                  title="${file.meta.name}">
-      <div class="UppyDashboardItem-preview">
-        <div class="UppyDashboardItem-previewInnerWrap" style="background-color: ${getFileTypeIcon(file.type).color}">
-          ${file.preview
-            ? html`<img alt="${file.name}" src="${file.preview}">`
-            : html`<div class="UppyDashboardItem-previewIconWrap">
-                <span class="UppyDashboardItem-previewIcon" style="color: ${getFileTypeIcon(file.type).color}">${getFileTypeIcon(file.type).icon}</span>
-                <svg class="UppyDashboardItem-previewIconBg" width="72" height="93" viewBox="0 0 72 93"><g><path d="M24.08 5h38.922A2.997 2.997 0 0 1 66 8.003v74.994A2.997 2.997 0 0 1 63.004 86H8.996A2.998 2.998 0 0 1 6 83.01V22.234L24.08 5z" fill="#FFF"/><path d="M24 5L6 22.248h15.007A2.995 2.995 0 0 0 24 19.244V5z" fill="#E4E4E4"/></g></svg>
-              </div>`
-          }
-        </div>
-        <div class="UppyDashboardItem-progress">
-          <button class="UppyDashboardItem-progressBtn"
-                  type="button"
-                  title="${isUploaded
-                          ? 'upload complete'
-                          : props.resumableUploads
-                            ? file.isPaused
-                              ? 'resume upload'
-                              : 'pause upload'
-                            : 'cancel upload'
-                        }"
-                  onclick=${onPauseResumeCancelRetry}>
-            ${error
+  const dashboardItemClass = classNames(
+    'uppy-DashboardItem',
+    { 'is-inprogress': uploadInProgress },
+    { 'is-processing': isProcessing },
+    { 'is-complete': isUploaded },
+    { 'is-paused': isPaused },
+    { 'is-error': error },
+    { 'is-resumable': props.resumableUploads }
+  )
+
+  return <li class={dashboardItemClass} id={`uppy_${file.id}`} title={file.meta.name}>
+    <div class="uppy-DashboardItem-preview">
+      <div class="uppy-DashboardItem-previewInnerWrap" style={{ backgroundColor: getFileTypeIcon(file.type).color }}>
+        {file.preview
+          ? <img alt={file.name} src={file.preview} />
+          : <div class="uppy-DashboardItem-previewIconWrap">
+            <span class="uppy-DashboardItem-previewIcon" style={{ color: getFileTypeIcon(file.type).color }}>{getFileTypeIcon(file.type).icon}</span>
+            <svg class="uppy-DashboardItem-previewIconBg" width="72" height="93" viewBox="0 0 72 93"><g><path d="M24.08 5h38.922A2.997 2.997 0 0 1 66 8.003v74.994A2.997 2.997 0 0 1 63.004 86H8.996A2.998 2.998 0 0 1 6 83.01V22.234L24.08 5z" fill="#FFF" /><path d="M24 5L6 22.248h15.007A2.995 2.995 0 0 0 24 19.244V5z" fill="#E4E4E4" /></g></svg>
+          </div>
+        }
+      </div>
+      <div class="uppy-DashboardItem-progress">
+        {isUploaded
+          ? <div class="uppy-DashboardItem-progressIndicator">
+            {FileItemProgress({
+              progress: file.progress.percentage,
+              fileID: file.id
+            })}
+          </div>
+          : <button class="uppy-DashboardItem-progressIndicator"
+            type="button"
+            title={isUploaded
+                    ? 'upload complete'
+                    : props.resumableUploads
+                      ? file.isPaused
+                        ? 'resume upload'
+                        : 'pause upload'
+                      : 'cancel upload'
+                  }
+            onclick={onPauseResumeCancelRetry}>
+            {error
               ? iconRetry()
               : FileItemProgress({
                 progress: file.progress.percentage,
@@ -76,76 +86,73 @@ module.exports = function fileItem (props) {
               })
             }
           </button>
-          ${props.showProgressDetails
-            ? html`<div class="UppyDashboardItem-progressInfo"
-                        title="${props.i18n('fileProgress')}"
-                        aria-label="${props.i18n('fileProgress')}">
-                ${!file.isPaused && !isUploaded
-                  ? html`<span>${prettyETA(getETA(file.progress))} ・ ↑ ${prettyBytes(getSpeed(file.progress))}/s</span>`
-                  : null
-                }
-              </div>`
-            : null
-          }
-        </div>
+        }
+        {props.showProgressDetails &&
+          <div class="uppy-DashboardItem-progressInfo"
+            title={props.i18n('fileProgress')}
+            aria-label={props.i18n('fileProgress')}>
+            {(!file.isPaused && !isUploaded) &&
+              <span>{prettyETA(getETA(file.progress))} ・ ↑ {prettyBytes(getSpeed(file.progress))}/s</span>
+            }
+          </div>
+        }
       </div>
-    <div class="UppyDashboardItem-info">
-      <h4 class="UppyDashboardItem-name" title="${fileName}">
-        ${file.uploadURL
-          ? html`<a href="${file.uploadURL}" target="_blank">
-              ${file.extension ? truncatedFileName + '.' + file.extension : truncatedFileName}
-            </a>`
+    </div>
+    <div class="uppy-DashboardItem-info">
+      <h4 class="uppy-DashboardItem-name" title="{fileName}">
+        {file.uploadURL
+          ? <a href="{file.uploadURL}" target="_blank">
+            {file.extension ? truncatedFileName + '.' + file.extension : truncatedFileName}
+          </a>
           : file.extension ? truncatedFileName + '.' + file.extension : truncatedFileName
         }
       </h4>
-      <div class="UppyDashboardItem-status">
-        ${file.data.size && html`<div class="UppyDashboardItem-statusSize">${prettyBytes(file.data.size)}</div>`}
-        ${file.source && html`<div class="UppyDashboardItem-sourceIcon">
-            ${acquirers.map(acquirer => {
-              if (acquirer.id === file.source) return html`<span title="${props.i18n('fileSource')}: ${acquirer.name}">${acquirer.icon()}</span>`
+      <div class="uppy-DashboardItem-status">
+        {file.data.size && <div class="uppy-DashboardItem-statusSize">{prettyBytes(file.data.size)}</div>}
+        {file.source && <div class="uppy-DashboardItem-sourceIcon">
+            {acquirers.map(acquirer => {
+              if (acquirer.id === file.source) return <span title="{props.i18n('fileSource')}: {acquirer.name}">{acquirer.icon()}</span>
             })}
-          </div>`
+          </div>
         }
       </div>
-      ${!uploadInProgressOrComplete
-        ? html`<button class="UppyDashboardItem-edit"
-                       type="button"
-                       aria-label="Edit file"
-                       title="Edit file"
-                       onclick=${(e) => props.showFileCard(file.id)}>
-                        ${iconEdit()}</button>`
-        : null
+      {!uploadInProgressOrComplete &&
+        <button class="uppy-DashboardItem-edit"
+          type="button"
+          aria-label="Edit file"
+          title="Edit file"
+          onclick={(e) => props.showFileCard(file.id)}>
+          {iconEdit()}
+        </button>
       }
-      ${file.uploadURL
-        ? html`<button class="UppyDashboardItem-copyLink"
-                       type="button"
-                       aria-label="Copy link"
-                       title="Copy link"
-                       onclick=${() => {
-                         copyToClipboard(file.uploadURL, props.i18n('copyLinkToClipboardFallback'))
-                          .then(() => {
-                            props.log('Link copied to clipboard.')
-                            props.info(props.i18n('copyLinkToClipboardSuccess'), 'info', 3000)
-                          })
-                          .catch(props.log)
-                       }}>${iconCopy()}</button>`
-        : null
+      {file.uploadURL &&
+        <button class="uppy-DashboardItem-copyLink"
+          type="button"
+          aria-label="Copy link"
+          title="Copy link"
+          onclick={() => {
+            copyToClipboard(file.uploadURL, props.i18n('copyLinkToClipboardFallback'))
+            .then(() => {
+              props.log('Link copied to clipboard.')
+              props.info(props.i18n('copyLinkToClipboardSuccess'), 'info', 3000)
+            })
+            .catch(props.log)
+          }}>{iconCopy()}</button>
       }
     </div>
-    <div class="UppyDashboardItem-action">
-      ${!isUploaded
-        ? html`<button class="UppyDashboardItem-remove"
-                       type="button"
-                       aria-label="Remove file"
-                       title="Remove file"
-                       onclick=${() => props.removeFile(file.id)}>
-                 <svg class="UppyIcon" width="22" height="21" viewBox="0 0 18 17">
-                   <ellipse cx="8.62" cy="8.383" rx="8.62" ry="8.383"/>
-                   <path stroke="#FFF" fill="#FFF" d="M11 6.147L10.85 6 8.5 8.284 6.15 6 6 6.147 8.35 8.43 6 10.717l.15.146L8.5 8.578l2.35 2.284.15-.146L8.65 8.43z"/>
-                 </svg>
-               </button>`
-        : null
+    <div class="uppy-DashboardItem-action">
+      {!isUploaded &&
+        <button class="uppy-DashboardItem-remove"
+          type="button"
+          aria-label="Remove file"
+          title="Remove file"
+          onclick={() => props.removeFile(file.id)}>
+          <svg aria-hidden="true" class="UppyIcon" width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
+            <path stroke="#FFF" stroke-width="1" fill-rule="nonzero" vector-effect="non-scaling-stroke" d="M30 1C14 1 1 14 1 30s13 29 29 29 29-13 29-29S46 1 30 1z" />
+            <path fill="#FFF" vector-effect="non-scaling-stroke" d="M42 39.667L39.667 42 30 32.333 20.333 42 18 39.667 27.667 30 18 20.333 20.333 18 30 27.667 39.667 18 42 20.333 32.333 30z" />
+          </svg>
+        </button>
       }
     </div>
-  </li>`
+  </li>
 }

+ 10 - 9
src/plugins/Dashboard/FileItemProgress.js

@@ -1,4 +1,4 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 
 // http://codepen.io/Harkko/pen/rVxvNM
 // https://css-tricks.com/svg-line-animation-works/
@@ -10,21 +10,22 @@ const circleLength = 2 * Math.PI * 15
 // stroke-dashoffset is a percentage of the progress from circleLength,
 // substracted from circleLength, because its an offset
 module.exports = (props) => {
-  return html`
+  return (
     <svg width="70" height="70" viewBox="0 0 36 36" class="UppyIcon UppyIcon-progressCircle">
       <g class="progress-group">
-        <circle r="15" cx="18" cy="18" stroke-width="2" fill="none" class="bg"/>
+        <circle r="15" cx="18" cy="18" stroke-width="2" fill="none" class="bg" />
         <circle r="15" cx="18" cy="18" transform="rotate(-90, 18, 18)" stroke-width="2" fill="none" class="progress"
-                stroke-dasharray=${circleLength}
-                stroke-dashoffset=${circleLength - (circleLength / 100 * props.progress)}
+          stroke-dasharray={circleLength}
+          stroke-dashoffset={circleLength - (circleLength / 100 * props.progress)}
         />
       </g>
-      <polygon transform="translate(3, 3)" points="12 20 12 10 20 15" class="play"/>
+      <polygon transform="translate(3, 3)" points="12 20 12 10 20 15" class="play" />
       <g transform="translate(14.5, 13)" class="pause">
         <rect x="0" y="0" width="2" height="10" rx="0" />
         <rect x="5" y="0" width="2" height="10" rx="0" />
       </g>
-      <polygon transform="translate(2, 3)" points="14 22.5 7 15.2457065 8.99985857 13.1732815 14 18.3547104 22.9729883 9 25 11.1005634" class="check"/>
-      <polygon class="cancel" transform="translate(2, 2)" points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12"></polygon>
-  </svg>`
+      <polygon transform="translate(2, 3)" points="14 22.5 7 15.2457065 8.99985857 13.1732815 14 18.3547104 22.9729883 9 25 11.1005634" class="check" />
+      <polygon class="cancel" transform="translate(2, 2)" points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12" />
+    </svg>
+  )
 }

+ 40 - 40
src/plugins/Dashboard/FileList.js

@@ -1,46 +1,46 @@
-const html = require('yo-yo')
 const FileItem = require('./FileItem')
 const ActionBrowseTagline = require('./ActionBrowseTagline')
 const { dashboardBgIcon } = require('./icons')
+const classNames = require('classnames')
+const { h } = require('preact')
 
 module.exports = (props) => {
-  return html`<ul class="UppyDashboard-files
-                         ${props.totalFileCount === 0 ? 'UppyDashboard-files--noFiles' : ''}">
-      ${props.totalFileCount === 0
-       ? html`<div class="UppyDashboard-bgIcon">
-          ${dashboardBgIcon()}
-          <h3 class="UppyDashboard-dropFilesTitle">
-            ${ActionBrowseTagline({
-              acquirers: props.acquirers,
-              handleInputChange: props.handleInputChange,
-              i18n: props.i18n
-            })}
-          </h3>
-          ${props.note
-            ? html`<p class="UppyDashboard-note">${props.note}</p>`
-            : ''
-          }
-          <input class="UppyDashboard-input" type="file" name="files[]" multiple="true"
-                 onchange=${props.handleInputChange} />
-         </div>`
-       : null
-      }
-      ${Object.keys(props.files).map((fileID) => {
-        return FileItem({
-          acquirers: props.acquirers,
-          file: props.files[fileID],
-          showFileCard: props.showFileCard,
-          showProgressDetails: props.showProgressDetails,
-          info: props.info,
-          log: props.log,
-          i18n: props.i18n,
-          removeFile: props.removeFile,
-          pauseUpload: props.pauseUpload,
-          cancelUpload: props.cancelUpload,
-          retryUpload: props.retryUpload,
-          resumableUploads: props.resumableUploads,
-          isWide: props.isWide
-        })
-      })}
-    </ul>`
+  const noFiles = props.totalFileCount === 0
+  const dashboardFilesClass = classNames(
+    'uppy-Dashboard-files',
+    { 'uppy-Dashboard-files--noFiles': noFiles }
+  )
+
+  return <ul class={dashboardFilesClass}>
+    {noFiles &&
+      <div class="uppy-Dashboard-bgIcon">
+        {dashboardBgIcon()}
+        <h3 class="uppy-Dashboard-dropFilesTitle">
+          {h(ActionBrowseTagline, {
+            acquirers: props.acquirers,
+            handleInputChange: props.handleInputChange,
+            i18n: props.i18n
+          })}
+        </h3>
+        { props.note && <p class="uppy-Dashboard-note">{props.note}</p> }
+      </div>
+    }
+    {Object.keys(props.files).map((fileID) => {
+      return FileItem({
+        acquirers: props.acquirers,
+        file: props.files[fileID],
+        showFileCard: props.showFileCard,
+        showProgressDetails: props.showProgressDetails,
+        info: props.info,
+        log: props.log,
+        i18n: props.i18n,
+        removeFile: props.removeFile,
+        pauseUpload: props.pauseUpload,
+        cancelUpload: props.cancelUpload,
+        retryUpload: props.retryUpload,
+        resumableUploads: props.resumableUploads,
+        isWide: props.isWide
+      })
+    })}
+  </ul>
 }

+ 59 - 46
src/plugins/Dashboard/Tabs.js

@@ -1,59 +1,72 @@
-const html = require('yo-yo')
 const ActionBrowseTagline = require('./ActionBrowseTagline')
 const { localIcon } = require('./icons')
+const { h, Component } = require('preact')
 
-module.exports = (props) => {
-  const isHidden = Object.keys(props.files).length === 0
+class Tabs extends Component {
+  constructor (props) {
+    super(props)
+    this.handleClick = this.handleClick.bind(this)
+  }
 
-  if (props.acquirers.length === 0) {
-    return html`
-      <div class="UppyDashboardTabs" aria-hidden="${isHidden}">
-        <h3 class="UppyDashboardTabs-title">
-        ${ActionBrowseTagline({
-          acquirers: props.acquirers,
-          handleInputChange: props.handleInputChange,
-          i18n: props.i18n
-        })}
-        </h3>
-      </div>
-    `
+  handleClick (ev) {
+    this.input.click()
   }
 
-  const input = html`
-    <input class="UppyDashboard-input" type="file" name="files[]" multiple="true"
-           onchange=${props.handleInputChange} />
-  `
+  render () {
+    const isHidden = Object.keys(this.props.files).length === 0
+    const hasAcquirers = this.props.acquirers.length !== 0
+
+    if (!hasAcquirers) {
+      return (
+        <div class="uppy-DashboardTabs" aria-hidden={isHidden}>
+          <div class="uppy-DashboardTabs-title">
+            <ActionBrowseTagline
+              acquirers={this.props.acquirers}
+              handleInputChange={this.props.handleInputChange}
+              i18n={this.props.i18n} />
+          </div>
+        </div>
+      )
+    }
 
-  return html`<div class="UppyDashboardTabs">
-    <nav>
-      <ul class="UppyDashboardTabs-list" role="tablist">
-        <li class="UppyDashboardTab">
-          <button type="button" class="UppyDashboardTab-btn UppyDashboard-focus"
-                  role="tab"
-                  tabindex="0"
-                  onclick=${(ev) => {
-                    input.click()
-                  }}>
-            ${localIcon()}
-            <h5 class="UppyDashboardTab-name">${props.i18n('myDevice')}</h5>
+    return <div class="uppy-DashboardTabs">
+      <ul class="uppy-DashboardTabs-list" role="tablist">
+        <li class="uppy-DashboardTab" role="presentation">
+          <button type="button"
+            class="uppy-DashboardTab-btn"
+            role="tab"
+            tabindex="0"
+            onclick={this.handleClick}>
+            {localIcon()}
+            <div class="uppy-DashboardTab-name">{this.props.i18n('myDevice')}</div>
           </button>
-          ${input}
+          <input class="uppy-Dashboard-input"
+            hidden="true"
+            aria-hidden="true"
+            tabindex="-1"
+            type="file"
+            name="files[]"
+            multiple="true"
+            onchange={this.props.handleInputChange}
+            ref={(input) => { this.input = input }} />
         </li>
-        ${props.acquirers.map((target) => {
-          return html`<li class="UppyDashboardTab">
-            <button class="UppyDashboardTab-btn"
-                    type="button"
-                    role="tab"
-                    tabindex="0"
-                    aria-controls="UppyDashboardContent-panel--${target.id}"
-                    aria-selected="${target.isHidden ? 'false' : 'true'}"
-                    onclick=${() => props.showPanel(target.id)}>
-              ${target.icon()}
-              <h5 class="UppyDashboardTab-name">${target.name}</h5>
+        {this.props.acquirers.map((target) => {
+          return <li class="uppy-DashboardTab" role="presentation">
+            <button class="uppy-DashboardTab-btn"
+              type="button"
+              role="tab"
+              tabindex="0"
+              aria-controls={`uppy-DashboardContent-panel--${target.id}`}
+              aria-selected={this.props.activePanel.id === target.id}
+              onclick={() => this.props.showPanel(target.id)}>
+              {target.icon()}
+              <h5 class="uppy-DashboardTab-name">{target.name}</h5>
             </button>
-          </li>`
+          </li>
         })}
       </ul>
-    </nav>
-  </div>`
+    </div>
+  }
 }
+
+module.exports = Tabs

+ 0 - 21
src/plugins/Dashboard/UploadBtn.js

@@ -1,21 +0,0 @@
-const html = require('yo-yo')
-const { uploadIcon } = require('./icons')
-
-module.exports = (props) => {
-  props = props || {}
-
-  return html`<button class="UppyButton--circular
-                   UppyButton--blue
-                   UppyDashboard-upload"
-                 type="button"
-                 title="${props.i18n('uploadAllNewFiles')}"
-                 aria-label="${props.i18n('uploadAllNewFiles')}"
-                 onclick=${props.startUpload}>
-            ${uploadIcon()}
-            <sup class="UppyDashboard-uploadCount"
-                 title="${props.i18n('numberOfSelectedFiles')}"
-                 aria-label="${props.i18n('numberOfSelectedFiles')}">
-                  ${props.newFileCount}</sup>
-    </button>
-  `
-}

+ 1 - 1
src/plugins/Dashboard/getFileTypeIcon.js

@@ -13,7 +13,7 @@ module.exports = function getIconByMime (fileType) {
 
   if (fileTypeGeneral === 'text') {
     return {
-      color: '#000',
+      color: '#cbcbcb',
       icon: iconText()
     }
   }

+ 53 - 52
src/plugins/Dashboard/icons.js

@@ -1,116 +1,117 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 
 // https://css-tricks.com/creating-svg-icon-system-react/
 
 function defaultTabIcon () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="30" height="30" viewBox="0 0 30 30">
+  return <svg aria-hidden="true" class="UppyIcon" width="30" height="30" viewBox="0 0 30 30">
     <path d="M15 30c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15C6.716 0 0 6.716 0 15c0 8.284 6.716 15 15 15zm4.258-12.676v6.846h-8.426v-6.846H5.204l9.82-12.364 9.82 12.364H19.26z" />
-  </svg>`
+  </svg>
 }
 
 function iconCopy () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="51" height="51" viewBox="0 0 51 51">
-    <path d="M17.21 45.765a5.394 5.394 0 0 1-7.62 0l-4.12-4.122a5.393 5.393 0 0 1 0-7.618l6.774-6.775-2.404-2.404-6.775 6.776c-3.424 3.427-3.424 9 0 12.426l4.12 4.123a8.766 8.766 0 0 0 6.216 2.57c2.25 0 4.5-.858 6.214-2.57l13.55-13.552a8.72 8.72 0 0 0 2.575-6.213 8.73 8.73 0 0 0-2.575-6.213l-4.123-4.12-2.404 2.404 4.123 4.12a5.352 5.352 0 0 1 1.58 3.81c0 1.438-.562 2.79-1.58 3.808l-13.55 13.55z"/>
-    <path d="M44.256 2.858A8.728 8.728 0 0 0 38.043.283h-.002a8.73 8.73 0 0 0-6.212 2.574l-13.55 13.55a8.725 8.725 0 0 0-2.575 6.214 8.73 8.73 0 0 0 2.574 6.216l4.12 4.12 2.405-2.403-4.12-4.12a5.357 5.357 0 0 1-1.58-3.812c0-1.437.562-2.79 1.58-3.808l13.55-13.55a5.348 5.348 0 0 1 3.81-1.58c1.44 0 2.792.562 3.81 1.58l4.12 4.12c2.1 2.1 2.1 5.518 0 7.617L39.2 23.775l2.404 2.404 6.775-6.777c3.426-3.427 3.426-9 0-12.426l-4.12-4.12z"/>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" width="51" height="51" viewBox="0 0 51 51">
+    <path d="M17.21 45.765a5.394 5.394 0 0 1-7.62 0l-4.12-4.122a5.393 5.393 0 0 1 0-7.618l6.774-6.775-2.404-2.404-6.775 6.776c-3.424 3.427-3.424 9 0 12.426l4.12 4.123a8.766 8.766 0 0 0 6.216 2.57c2.25 0 4.5-.858 6.214-2.57l13.55-13.552a8.72 8.72 0 0 0 2.575-6.213 8.73 8.73 0 0 0-2.575-6.213l-4.123-4.12-2.404 2.404 4.123 4.12a5.352 5.352 0 0 1 1.58 3.81c0 1.438-.562 2.79-1.58 3.808l-13.55 13.55z" />
+    <path d="M44.256 2.858A8.728 8.728 0 0 0 38.043.283h-.002a8.73 8.73 0 0 0-6.212 2.574l-13.55 13.55a8.725 8.725 0 0 0-2.575 6.214 8.73 8.73 0 0 0 2.574 6.216l4.12 4.12 2.405-2.403-4.12-4.12a5.357 5.357 0 0 1-1.58-3.812c0-1.437.562-2.79 1.58-3.808l13.55-13.55a5.348 5.348 0 0 1 3.81-1.58c1.44 0 2.792.562 3.81 1.58l4.12 4.12c2.1 2.1 2.1 5.518 0 7.617L39.2 23.775l2.404 2.404 6.775-6.777c3.426-3.427 3.426-9 0-12.426l-4.12-4.12z" />
+  </svg>
 }
 
 function iconResume () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="25" height="25" viewBox="0 0 44 44">
+  return <svg aria-hidden="true" class="UppyIcon" width="25" height="25" viewBox="0 0 44 44">
     <polygon class="play" transform="translate(6, 5.5)" points="13 21.6666667 13 11 21 16.3333333" />
-  </svg>`
+  </svg>
 }
 
 function iconPause () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="25px" height="25px" viewBox="0 0 44 44">
+  return <svg aria-hidden="true" class="UppyIcon" width="25px" height="25px" viewBox="0 0 44 44">
     <g transform="translate(18, 17)" class="pause">
       <rect x="0" y="0" width="2" height="10" rx="0" />
       <rect x="6" y="0" width="2" height="10" rx="0" />
     </g>
-  </svg>`
+  </svg>
 }
 
 function iconEdit () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="28" height="28" viewBox="0 0 28 28">
+  return <svg aria-hidden="true" class="UppyIcon" width="28" height="28" viewBox="0 0 28 28">
     <path d="M25.436 2.566a7.98 7.98 0 0 0-2.078-1.51C22.638.703 21.906.5 21.198.5a3 3 0 0 0-1.023.17 2.436 2.436 0 0 0-.893.562L2.292 18.217.5 27.5l9.28-1.796 16.99-16.99c.255-.254.444-.56.562-.888a3 3 0 0 0 .17-1.023c0-.708-.205-1.44-.555-2.16a8 8 0 0 0-1.51-2.077zM9.01 24.252l-4.313.834c0-.03.008-.06.012-.09.007-.944-.74-1.715-1.67-1.723-.04 0-.078.007-.118.01l.83-4.29L17.72 5.024l5.264 5.264L9.01 24.252zm16.84-16.96a.818.818 0 0 1-.194.31l-1.57 1.57-5.26-5.26 1.57-1.57a.82.82 0 0 1 .31-.194 1.45 1.45 0 0 1 .492-.074c.397 0 .917.126 1.468.397.55.27 1.13.678 1.656 1.21.53.53.94 1.11 1.208 1.655.272.55.397 1.07.393 1.468.004.193-.027.358-.074.488z" />
-  </svg>`
+  </svg>
 }
 
 function localIcon () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="27" height="25" viewBox="0 0 27 25">
-    <path d="M5.586 9.288a.313.313 0 0 0 .282.176h4.84v3.922c0 1.514 1.25 2.24 2.792 2.24 1.54 0 2.79-.726 2.79-2.24V9.464h4.84c.122 0 .23-.068.284-.176a.304.304 0 0 0-.046-.324L13.735.106a.316.316 0 0 0-.472 0l-7.63 8.857a.302.302 0 0 0-.047.325z"/>
-    <path d="M24.3 5.093c-.218-.76-.54-1.187-1.208-1.187h-4.856l1.018 1.18h3.948l2.043 11.038h-7.193v2.728H9.114v-2.725h-7.36l2.66-11.04h3.33l1.018-1.18H3.907c-.668 0-1.06.46-1.21 1.186L0 16.456v7.062C0 24.338.676 25 1.51 25h23.98c.833 0 1.51-.663 1.51-1.482v-7.062L24.3 5.093z"/>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" width="27" height="25" viewBox="0 0 27 25">
+    <path d="M5.586 9.288a.313.313 0 0 0 .282.176h4.84v3.922c0 1.514 1.25 2.24 2.792 2.24 1.54 0 2.79-.726 2.79-2.24V9.464h4.84c.122 0 .23-.068.284-.176a.304.304 0 0 0-.046-.324L13.735.106a.316.316 0 0 0-.472 0l-7.63 8.857a.302.302 0 0 0-.047.325z" />
+    <path d="M24.3 5.093c-.218-.76-.54-1.187-1.208-1.187h-4.856l1.018 1.18h3.948l2.043 11.038h-7.193v2.728H9.114v-2.725h-7.36l2.66-11.04h3.33l1.018-1.18H3.907c-.668 0-1.06.46-1.21 1.186L0 16.456v7.062C0 24.338.676 25 1.51 25h23.98c.833 0 1.51-.663 1.51-1.482v-7.062L24.3 5.093z" />
+  </svg>
 }
 
 function closeIcon () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="14px" height="14px" viewBox="0 0 19 19">
-    <path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z"/>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" width="14px" height="14px" viewBox="0 0 19 19">
+    <path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
+  </svg>
 }
 
 function iconRetry () {
-  return html`<svg class="UppyIcon retry" width="28" height="31" viewBox="0 0 16 19" xmlns="http://www.w3.org/2000/svg">
-    <path d="M16 11a8 8 0 1 1-8-8v2a6 6 0 1 0 6 6h2z"/>
-    <path d="M7.9 3H10v2H7.9z"/><path d="M8.536.5l3.535 3.536-1.414 1.414L7.12 1.914z"/><path d="M10.657 2.621l1.414 1.415L8.536 7.57 7.12 6.157z"/>
-  </svg>`
+  return <svg class="UppyIcon retry" width="28" height="31" viewBox="0 0 16 19" xmlns="http://www.w3.org/2000/svg">
+    <path d="M16 11a8 8 0 1 1-8-8v2a6 6 0 1 0 6 6h2z" />
+    <path d="M7.9 3H10v2H7.9z" />
+    <path d="M8.536.5l3.535 3.536-1.414 1.414L7.12 1.914z" />
+    <path d="M10.657 2.621l1.414 1.415L8.536 7.57 7.12 6.157z" />
+  </svg>
 }
 
 function pluginIcon () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 32 30">
-      <path d="M6.6209894,11.1451162 C6.6823051,11.2751669 6.81374248,11.3572188 6.95463813,11.3572188 L12.6925482,11.3572188 L12.6925482,16.0630427 C12.6925482,17.880509 14.1726048,18.75 16.0000083,18.75 C17.8261072,18.75 19.3074684,17.8801847 19.3074684,16.0630427 L19.3074684,11.3572188 L25.0437478,11.3572188 C25.1875787,11.3572188 25.3164069,11.2751669 25.3790272,11.1451162 C25.4370814,11.0173358 25.4171865,10.8642587 25.3252129,10.7562615 L16.278212,0.127131837 C16.2093949,0.0463771751 16.1069846,0 15.9996822,0 C15.8910751,0 15.7886648,0.0463771751 15.718217,0.127131837 L6.6761083,10.7559371 C6.58250402,10.8642587 6.56293518,11.0173358 6.6209894,11.1451162 L6.6209894,11.1451162 Z"/>
-      <path d="M28.8008722,6.11142645 C28.5417891,5.19831555 28.1583331,4.6875 27.3684848,4.6875 L21.6124454,4.6875 L22.8190234,6.10307874 L27.4986725,6.10307874 L29.9195817,19.3486449 L21.3943891,19.3502502 L21.3943891,22.622552 L10.8023461,22.622552 L10.8023461,19.3524977 L2.07815702,19.3534609 L5.22979699,6.10307874 L9.17871529,6.10307874 L10.3840011,4.6875 L4.6308691,4.6875 C3.83940559,4.6875 3.37421888,5.2390909 3.19815864,6.11142645 L0,19.7470874 L0,28.2212959 C0,29.2043992 0.801477937,30 1.78870751,30 L30.2096773,30 C31.198199,30 32,29.2043992 32,28.2212959 L32,19.7470874 L28.8008722,6.11142645 L28.8008722,6.11142645 Z"/>
-    </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 32 30">
+    <path d="M6.6209894,11.1451162 C6.6823051,11.2751669 6.81374248,11.3572188 6.95463813,11.3572188 L12.6925482,11.3572188 L12.6925482,16.0630427 C12.6925482,17.880509 14.1726048,18.75 16.0000083,18.75 C17.8261072,18.75 19.3074684,17.8801847 19.3074684,16.0630427 L19.3074684,11.3572188 L25.0437478,11.3572188 C25.1875787,11.3572188 25.3164069,11.2751669 25.3790272,11.1451162 C25.4370814,11.0173358 25.4171865,10.8642587 25.3252129,10.7562615 L16.278212,0.127131837 C16.2093949,0.0463771751 16.1069846,0 15.9996822,0 C15.8910751,0 15.7886648,0.0463771751 15.718217,0.127131837 L6.6761083,10.7559371 C6.58250402,10.8642587 6.56293518,11.0173358 6.6209894,11.1451162 L6.6209894,11.1451162 Z" />
+    <path d="M28.8008722,6.11142645 C28.5417891,5.19831555 28.1583331,4.6875 27.3684848,4.6875 L21.6124454,4.6875 L22.8190234,6.10307874 L27.4986725,6.10307874 L29.9195817,19.3486449 L21.3943891,19.3502502 L21.3943891,22.622552 L10.8023461,22.622552 L10.8023461,19.3524977 L2.07815702,19.3534609 L5.22979699,6.10307874 L9.17871529,6.10307874 L10.3840011,4.6875 L4.6308691,4.6875 C3.83940559,4.6875 3.37421888,5.2390909 3.19815864,6.11142645 L0,19.7470874 L0,28.2212959 C0,29.2043992 0.801477937,30 1.78870751,30 L30.2096773,30 C31.198199,30 32,29.2043992 32,28.2212959 L32,19.7470874 L28.8008722,6.11142645 L28.8008722,6.11142645 Z" />
+  </svg>
 }
 
 function checkIcon () {
-  return html`<svg aria-hidden="true" class="UppyIcon UppyIcon-check" width="13px" height="9px" viewBox="0 0 13 9">
-    <polygon points="5 7.293 1.354 3.647 0.646 4.354 5 8.707 12.354 1.354 11.646 0.647"></polygon>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon UppyIcon-check" width="13px" height="9px" viewBox="0 0 13 9">
+    <polygon points="5 7.293 1.354 3.647 0.646 4.354 5 8.707 12.354 1.354 11.646 0.647" />
+  </svg>
 }
 
 function iconAudio () {
-  return html`<svg aria-hidden="true" class="UppyIcon" viewBox="0 0 55 55">
-    <path d="M52.66.25c-.216-.19-.5-.276-.79-.242l-31 4.01a1 1 0 0 0-.87.992V40.622C18.174 38.428 15.273 37 12 37c-5.514 0-10 4.037-10 9s4.486 9 10 9 10-4.037 10-9c0-.232-.02-.46-.04-.687.014-.065.04-.124.04-.192V16.12l29-3.753v18.257C49.174 28.428 46.273 27 43 27c-5.514 0-10 4.037-10 9s4.486 9 10 9c5.464 0 9.913-3.966 9.993-8.867 0-.013.007-.024.007-.037V1a.998.998 0 0 0-.34-.75zM12 53c-4.41 0-8-3.14-8-7s3.59-7 8-7 8 3.14 8 7-3.59 7-8 7zm31-10c-4.41 0-8-3.14-8-7s3.59-7 8-7 8 3.14 8 7-3.59 7-8 7zM22 14.1V5.89l29-3.753v8.21l-29 3.754z"/>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" viewBox="0 0 55 55">
+    <path d="M52.66.25c-.216-.19-.5-.276-.79-.242l-31 4.01a1 1 0 0 0-.87.992V40.622C18.174 38.428 15.273 37 12 37c-5.514 0-10 4.037-10 9s4.486 9 10 9 10-4.037 10-9c0-.232-.02-.46-.04-.687.014-.065.04-.124.04-.192V16.12l29-3.753v18.257C49.174 28.428 46.273 27 43 27c-5.514 0-10 4.037-10 9s4.486 9 10 9c5.464 0 9.913-3.966 9.993-8.867 0-.013.007-.024.007-.037V1a.998.998 0 0 0-.34-.75zM12 53c-4.41 0-8-3.14-8-7s3.59-7 8-7 8 3.14 8 7-3.59 7-8 7zm31-10c-4.41 0-8-3.14-8-7s3.59-7 8-7 8 3.14 8 7-3.59 7-8 7zM22 14.1V5.89l29-3.753v8.21l-29 3.754z" />
+  </svg>
 }
 
 function iconVideo () {
-  return html`<svg aria-hidden="true" class="UppyIcon" viewBox="0 0 58 58">
-    <path d="M36.537 28.156l-11-7a1.005 1.005 0 0 0-1.02-.033C24.2 21.3 24 21.635 24 22v14a1 1 0 0 0 1.537.844l11-7a1.002 1.002 0 0 0 0-1.688zM26 34.18V23.82L34.137 29 26 34.18z"/><path d="M57 6H1a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h56a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1zM10 28H2v-9h8v9zm-8 2h8v9H2v-9zm10 10V8h34v42H12V40zm44-12h-8v-9h8v9zm-8 2h8v9h-8v-9zm8-22v9h-8V8h8zM2 8h8v9H2V8zm0 42v-9h8v9H2zm54 0h-8v-9h8v9z"/>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" viewBox="0 0 58 58">
+    <path d="M36.537 28.156l-11-7a1.005 1.005 0 0 0-1.02-.033C24.2 21.3 24 21.635 24 22v14a1 1 0 0 0 1.537.844l11-7a1.002 1.002 0 0 0 0-1.688zM26 34.18V23.82L34.137 29 26 34.18z" /><path d="M57 6H1a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h56a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1zM10 28H2v-9h8v9zm-8 2h8v9H2v-9zm10 10V8h34v42H12V40zm44-12h-8v-9h8v9zm-8 2h8v9h-8v-9zm8-22v9h-8V8h8zM2 8h8v9H2V8zm0 42v-9h8v9H2zm54 0h-8v-9h8v9z" />
+  </svg>
 }
 
 function iconPDF () {
-  return html`<svg aria-hidden="true" class="UppyIcon" viewBox="0 0 342 335">
+  return <svg aria-hidden="true" class="UppyIcon" viewBox="0 0 342 335">
     <path d="M329.337 227.84c-2.1 1.3-8.1 2.1-11.9 2.1-12.4 0-27.6-5.7-49.1-14.9 8.3-.6 15.8-.9 22.6-.9 12.4 0 16 0 28.2 3.1 12.1 3 12.2 9.3 10.2 10.6zm-215.1 1.9c4.8-8.4 9.7-17.3 14.7-26.8 12.2-23.1 20-41.3 25.7-56.2 11.5 20.9 25.8 38.6 42.5 52.8 2.1 1.8 4.3 3.5 6.7 5.3-34.1 6.8-63.6 15-89.6 24.9zm39.8-218.9c6.8 0 10.7 17.06 11 33.16.3 16-3.4 27.2-8.1 35.6-3.9-12.4-5.7-31.8-5.7-44.5 0 0-.3-24.26 2.8-24.26zm-133.4 307.2c3.9-10.5 19.1-31.3 41.6-49.8 1.4-1.1 4.9-4.4 8.1-7.4-23.5 37.6-39.3 52.5-49.7 57.2zm315.2-112.3c-6.8-6.7-22-10.2-45-10.5-15.6-.2-34.3 1.2-54.1 3.9-8.8-5.1-17.9-10.6-25.1-17.3-19.2-18-35.2-42.9-45.2-70.3.6-2.6 1.2-4.8 1.7-7.1 0 0 10.8-61.5 7.9-82.3-.4-2.9-.6-3.7-1.4-5.9l-.9-2.5c-2.9-6.76-8.7-13.96-17.8-13.57l-5.3-.17h-.1c-10.1 0-18.4 5.17-20.5 12.84-6.6 24.3.2 60.5 12.5 107.4l-3.2 7.7c-8.8 21.4-19.8 43-29.5 62l-1.3 2.5c-10.2 20-19.5 37-27.9 51.4l-8.7 4.6c-.6.4-15.5 8.2-19 10.3-29.6 17.7-49.28 37.8-52.54 53.8-1.04 5-.26 11.5 5.01 14.6l8.4 4.2c3.63 1.8 7.53 2.7 11.43 2.7 21.1 0 45.6-26.2 79.3-85.1 39-12.7 83.4-23.3 122.3-29.1 29.6 16.7 66 28.3 89 28.3 4.1 0 7.6-.4 10.5-1.2 4.4-1.1 8.1-3.6 10.4-7.1 4.4-6.7 5.4-15.9 4.1-25.4-.3-2.8-2.6-6.3-5-8.7z" />
-  </svg>`
+  </svg>
 }
 
 function iconFile () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="44" height="58" viewBox="0 0 44 58">
+  return <svg aria-hidden="true" class="UppyIcon" width="44" height="58" viewBox="0 0 44 58">
     <path d="M27.437.517a1 1 0 0 0-.094.03H4.25C2.037.548.217 2.368.217 4.58v48.405c0 2.212 1.82 4.03 4.03 4.03H39.03c2.21 0 4.03-1.818 4.03-4.03V15.61a1 1 0 0 0-.03-.28 1 1 0 0 0 0-.093 1 1 0 0 0-.03-.032 1 1 0 0 0 0-.03 1 1 0 0 0-.032-.063 1 1 0 0 0-.03-.063 1 1 0 0 0-.032 0 1 1 0 0 0-.03-.063 1 1 0 0 0-.032-.03 1 1 0 0 0-.03-.063 1 1 0 0 0-.063-.062l-14.593-14a1 1 0 0 0-.062-.062A1 1 0 0 0 28 .708a1 1 0 0 0-.374-.157 1 1 0 0 0-.156 0 1 1 0 0 0-.03-.03l-.003-.003zM4.25 2.547h22.218v9.97c0 2.21 1.82 4.03 4.03 4.03h10.564v36.438a2.02 2.02 0 0 1-2.032 2.032H4.25c-1.13 0-2.032-.9-2.032-2.032V4.58c0-1.13.902-2.032 2.03-2.032zm24.218 1.345l10.375 9.937.75.718H30.5c-1.13 0-2.032-.9-2.032-2.03V3.89z" />
-  </svg>`
+  </svg>
 }
 
 function iconText () {
-  return html`<svg aria-hidden="true" class="UppyIcon" viewBox="0 0 64 64">
-    <path d="M8 64h48V0H22.586L8 14.586V64zm46-2H10V16h14V2h30v60zM11.414 14L22 3.414V14H11.414z"/>
-    <path d="M32 13h14v2H32zM18 23h28v2H18zM18 33h28v2H18zM18 43h28v2H18zM18 53h28v2H18z"/>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" width="62" height="62" viewBox="0 0 62 62" xmlns="http://www.w3.org/2000/svg">
+    <path d="M4.309 4.309h24.912v53.382h-6.525v3.559h16.608v-3.559h-6.525V4.309h24.912v10.676h3.559V.75H.75v14.235h3.559z" fill-rule="nonzero" fill="#000" />
+  </svg>
 }
 
 function uploadIcon () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="37" height="33" viewBox="0 0 37 33">
-    <path d="M29.107 24.5c4.07 0 7.393-3.355 7.393-7.442 0-3.994-3.105-7.307-7.012-7.502l.468.415C29.02 4.52 24.34.5 18.886.5c-4.348 0-8.27 2.522-10.138 6.506l.446-.288C4.394 6.782.5 10.758.5 15.608c0 4.924 3.906 8.892 8.76 8.892h4.872c.635 0 1.095-.467 1.095-1.104 0-.636-.46-1.103-1.095-1.103H9.26c-3.644 0-6.63-3.035-6.63-6.744 0-3.71 2.926-6.685 6.57-6.685h.964l.14-.28.177-.362c1.477-3.4 4.744-5.576 8.347-5.576 4.58 0 8.45 3.452 9.01 8.072l.06.536.05.446h1.101c2.87 0 5.204 2.37 5.204 5.295s-2.333 5.296-5.204 5.296h-6.062c-.634 0-1.094.467-1.094 1.103 0 .637.46 1.104 1.094 1.104h6.12z"/>
-    <path d="M23.196 18.92l-4.828-5.258-.366-.4-.368.398-4.828 5.196a1.13 1.13 0 0 0 0 1.546c.428.46 1.11.46 1.537 0l3.45-3.71-.868-.34v15.03c0 .64.445 1.118 1.075 1.118.63 0 1.075-.48 1.075-1.12V16.35l-.867.34 3.45 3.712a1 1 0 0 0 .767.345 1 1 0 0 0 .77-.345c.416-.33.416-1.036 0-1.485v.003z"/>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" width="37" height="33" viewBox="0 0 37 33">
+    <path d="M29.107 24.5c4.07 0 7.393-3.355 7.393-7.442 0-3.994-3.105-7.307-7.012-7.502l.468.415C29.02 4.52 24.34.5 18.886.5c-4.348 0-8.27 2.522-10.138 6.506l.446-.288C4.394 6.782.5 10.758.5 15.608c0 4.924 3.906 8.892 8.76 8.892h4.872c.635 0 1.095-.467 1.095-1.104 0-.636-.46-1.103-1.095-1.103H9.26c-3.644 0-6.63-3.035-6.63-6.744 0-3.71 2.926-6.685 6.57-6.685h.964l.14-.28.177-.362c1.477-3.4 4.744-5.576 8.347-5.576 4.58 0 8.45 3.452 9.01 8.072l.06.536.05.446h1.101c2.87 0 5.204 2.37 5.204 5.295s-2.333 5.296-5.204 5.296h-6.062c-.634 0-1.094.467-1.094 1.103 0 .637.46 1.104 1.094 1.104h6.12z" />
+    <path d="M23.196 18.92l-4.828-5.258-.366-.4-.368.398-4.828 5.196a1.13 1.13 0 0 0 0 1.546c.428.46 1.11.46 1.537 0l3.45-3.71-.868-.34v15.03c0 .64.445 1.118 1.075 1.118.63 0 1.075-.48 1.075-1.12V16.35l-.867.34 3.45 3.712a1 1 0 0 0 .767.345 1 1 0 0 0 .77-.345c.416-.33.416-1.036 0-1.485v.003z" />
+  </svg>
 }
 
 function dashboardBgIcon () {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="48" height="69" viewBox="0 0 48 69">
-    <path d="M.5 1.5h5zM10.5 1.5h5zM20.5 1.5h5zM30.504 1.5h5zM45.5 11.5v5zM45.5 21.5v5zM45.5 31.5v5zM45.5 41.502v5zM45.5 51.502v5zM45.5 61.5v5zM45.5 66.502h-4.998zM35.503 66.502h-5zM25.5 66.502h-5zM15.5 66.502h-5zM5.5 66.502h-5zM.5 66.502v-5zM.5 56.502v-5zM.5 46.503V41.5zM.5 36.5v-5zM.5 26.5v-5zM.5 16.5v-5zM.5 6.5V1.498zM44.807 11H36V2.195z"/>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" width="48" height="69" viewBox="0 0 48 69">
+    <path d="M.5 1.5h5zM10.5 1.5h5zM20.5 1.5h5zM30.504 1.5h5zM45.5 11.5v5zM45.5 21.5v5zM45.5 31.5v5zM45.5 41.502v5zM45.5 51.502v5zM45.5 61.5v5zM45.5 66.502h-4.998zM35.503 66.502h-5zM25.5 66.502h-5zM15.5 66.502h-5zM5.5 66.502h-5zM.5 66.502v-5zM.5 56.502v-5zM.5 46.503V41.5zM.5 36.5v-5zM.5 26.5v-5zM.5 16.5v-5zM.5 6.5V1.498zM44.807 11H36V2.195z" />
+  </svg>
 }
 
 module.exports = {

+ 190 - 111
src/plugins/Dashboard/index.js

@@ -1,20 +1,34 @@
-const Plugin = require('../Plugin')
+const Plugin = require('../../core/Plugin')
 const Translator = require('../../core/Translator')
 const dragDrop = require('drag-drop')
-const Dashboard = require('./Dashboard')
+const DashboardUI = require('./Dashboard')
 const StatusBar = require('../StatusBar')
 const Informer = require('../Informer')
-const { findAllDOMElements } = require('../../core/Utils')
+const { findAllDOMElements, toArray } = require('../../core/Utils')
 const prettyBytes = require('prettier-bytes')
 const { defaultTabIcon } = require('./icons')
 
+const FOCUSABLE_ELEMENTS = [
+  'a[href]',
+  'area[href]',
+  'input:not([disabled]):not([type="hidden"])',
+  'select:not([disabled])',
+  'textarea:not([disabled])',
+  'button:not([disabled])',
+  'iframe',
+  'object',
+  'embed',
+  '[contenteditable]',
+  '[tabindex]:not([tabindex^="-"])'
+]
+
 /**
- * Modal Dialog & Dashboard
+ * Dashboard UI with previews, metadata editing, tabs for various services and more
  */
-module.exports = class DashboardUI extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
-    this.id = 'Dashboard'
+module.exports = class Dashboard extends Plugin {
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = this.opts.id || 'Dashboard'
     this.title = 'Dashboard'
     this.type = 'orchestrator'
 
@@ -37,7 +51,12 @@ module.exports = class DashboardUI extends Plugin {
         browse: 'browse',
         fileProgress: 'File progress: upload speed and ETA',
         numberOfSelectedFiles: 'Number of selected files',
-        uploadAllNewFiles: 'Upload all new files'
+        uploadAllNewFiles: 'Upload all new files',
+        emptyFolderAdded: 'No files were added from empty folder',
+        folderAdded: {
+          0: 'Added %{smart_count} file from %{folder}',
+          1: 'Added %{smart_count} files from %{folder}'
+        }
       }
     }
 
@@ -45,12 +64,13 @@ module.exports = class DashboardUI extends Plugin {
     const defaultOptions = {
       target: 'body',
       getMetaFromForm: true,
+      metaFields: [],
       trigger: '#uppy-select-files',
       inline: false,
       width: 750,
       height: 550,
       semiTransparent: false,
-      defaultTabIcon: defaultTabIcon(),
+      defaultTabIcon: defaultTabIcon,
       showProgressDetails: false,
       hideUploadButton: false,
       note: null,
@@ -66,7 +86,7 @@ module.exports = class DashboardUI extends Plugin {
     this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
 
     this.translator = new Translator({locale: this.locale})
-    this.containerWidth = this.translator.translate.bind(this.translator)
+    this.i18n = this.translator.translate.bind(this.translator)
 
     this.closeModal = this.closeModal.bind(this)
     this.requestCloseModal = this.requestCloseModal.bind(this)
@@ -74,17 +94,19 @@ module.exports = class DashboardUI extends Plugin {
     this.isModalOpen = this.isModalOpen.bind(this)
 
     this.addTarget = this.addTarget.bind(this)
-    this.actions = this.actions.bind(this)
     this.hideAllPanels = this.hideAllPanels.bind(this)
     this.showPanel = this.showPanel.bind(this)
+    this.getFocusableNodes = this.getFocusableNodes.bind(this)
+    this.setFocusToFirstNode = this.setFocusToFirstNode.bind(this)
+    this.maintainFocus = this.maintainFocus.bind(this)
+
     this.initEvents = this.initEvents.bind(this)
-    this.handleEscapeKeyPress = this.handleEscapeKeyPress.bind(this)
+    this.onKeydown = this.onKeydown.bind(this)
     this.handleClickOutside = this.handleClickOutside.bind(this)
     this.handleFileCard = this.handleFileCard.bind(this)
     this.handleDrop = this.handleDrop.bind(this)
-    this.pauseAll = this.pauseAll.bind(this)
-    this.resumeAll = this.resumeAll.bind(this)
-    this.cancelAll = this.cancelAll.bind(this)
+    this.handlePaste = this.handlePaste.bind(this)
+    this.handleInputChange = this.handleInputChange.bind(this)
     this.updateDashboardElWidth = this.updateDashboardElWidth.bind(this)
     this.render = this.render.bind(this)
     this.install = this.install.bind(this)
@@ -93,24 +115,20 @@ module.exports = class DashboardUI extends Plugin {
   addTarget (plugin) {
     const callerPluginId = plugin.id || plugin.constructor.name
     const callerPluginName = plugin.title || callerPluginId
-    // const callerPluginIcon = plugin.icon || this.opts.defaultTabIcon
     const callerPluginType = plugin.type
 
     if (callerPluginType !== 'acquirer' &&
         callerPluginType !== 'progressindicator' &&
         callerPluginType !== 'presenter') {
       let msg = 'Dashboard: Modal can only be used by plugins of types: acquirer, progressindicator, presenter'
-      this.core.log(msg)
+      this.uppy.log(msg)
       return
     }
 
     const target = {
       id: callerPluginId,
       name: callerPluginName,
-      // icon: callerPluginIcon,
-      type: callerPluginType,
-      // render: plugin.render,
-      isHidden: true
+      type: callerPluginType
     }
 
     const state = this.getPluginState()
@@ -121,7 +139,7 @@ module.exports = class DashboardUI extends Plugin {
       targets: newTargets
     })
 
-    return this.target
+    return this.el
   }
 
   hideAllPanels () {
@@ -150,6 +168,33 @@ module.exports = class DashboardUI extends Plugin {
     }
   }
 
+  getFocusableNodes () {
+    const nodes = this.el.querySelectorAll(FOCUSABLE_ELEMENTS)
+    return Object.keys(nodes).map((key) => nodes[key])
+  }
+
+  setFocusToFirstNode () {
+    const focusableNodes = this.getFocusableNodes()
+    // console.log(focusableNodes)
+    // console.log(focusableNodes[0])
+    if (focusableNodes.length) focusableNodes[0].focus()
+  }
+
+  maintainFocus (event) {
+    var focusableNodes = this.getFocusableNodes()
+    var focusedItemIndex = focusableNodes.indexOf(document.activeElement)
+
+    if (event.shiftKey && focusedItemIndex === 0) {
+      focusableNodes[focusableNodes.length - 1].focus()
+      event.preventDefault()
+    }
+
+    if (!event.shiftKey && focusedItemIndex === focusableNodes.length - 1) {
+      focusableNodes[0].focus()
+      event.preventDefault()
+    }
+  }
+
   openModal () {
     this.setPluginState({
       isHidden: false
@@ -160,15 +205,15 @@ module.exports = class DashboardUI extends Plugin {
 
     // add class to body that sets position fixed, move everything back
     // to scroll position
-    document.body.classList.add('is-UppyDashboard-open')
+    document.body.classList.add('uppy-Dashboard-isOpen')
     document.body.style.top = `-${this.savedDocumentScrollPosition}px`
 
-    // focus on modal inner block
-    this.target.querySelector('.UppyDashboard-inner').focus()
+    this.updateDashboardElWidth()
+    this.setFocusToFirstNode()
 
-    // this.updateDashboardElWidth()
-    // to be sure, sometimes when the function runs, container size is still 0
-    setTimeout(this.updateDashboardElWidth, 500)
+    // timeout is needed because yo-yo/morphdom/nanoraf; not needed without nanoraf
+    // setTimeout(this.setFocusToFirstNode, 100)
+    // setTimeout(this.updateDashboardElWidth, 100)
   }
 
   closeModal () {
@@ -176,7 +221,7 @@ module.exports = class DashboardUI extends Plugin {
       isHidden: true
     })
 
-    document.body.classList.remove('is-UppyDashboard-open')
+    document.body.classList.remove('uppy-Dashboard-isOpen')
 
     window.scrollTo(0, this.savedDocumentScrollPosition)
   }
@@ -185,17 +230,52 @@ module.exports = class DashboardUI extends Plugin {
     return !this.getPluginState().isHidden || false
   }
 
-  // Close the Modal on esc key press
-  handleEscapeKeyPress (event) {
-    if (event.keyCode === 27) {
-      this.requestCloseModal()
-    }
+  onKeydown (event) {
+    // close modal on esc key press
+    if (event.keyCode === 27) this.requestCloseModal(event)
+    // maintainFocus on tab key press
+    if (event.keyCode === 9) this.maintainFocus(event)
   }
 
   handleClickOutside () {
     if (this.opts.closeModalOnClickOutside) this.requestCloseModal()
   }
 
+  handlePaste (ev) {
+    const files = toArray(ev.clipboardData.items)
+    files.forEach((file) => {
+      if (file.kind !== 'file') return
+
+      const blob = file.getAsFile()
+      if (!blob) {
+        this.uppy.log('[Dashboard] File pasted, but the file blob is empty')
+        this.uppy.info('Error pasting file', 'error')
+        return
+      }
+      this.uppy.log('[Dashboard] File pasted')
+      this.uppy.addFile({
+        source: this.id,
+        name: file.name,
+        type: file.type,
+        data: blob
+      })
+    })
+  }
+
+  handleInputChange (ev) {
+    ev.preventDefault()
+    const files = toArray(ev.target.files)
+
+    files.forEach((file) => {
+      this.uppy.addFile({
+        source: this.id,
+        name: file.name,
+        type: file.type,
+        data: file
+      })
+    })
+  }
+
   initEvents () {
     // Modal open button
     const showModalTrigger = findAllDOMElements(this.opts.trigger)
@@ -204,15 +284,21 @@ module.exports = class DashboardUI extends Plugin {
     }
 
     if (!this.opts.inline && !showModalTrigger) {
-      this.core.log('Dashboard modal trigger not found, you won’t be able to select files. Make sure `trigger` is set correctly in Dashboard options', 'error')
+      this.uppy.log('Dashboard modal trigger not found, you won’t be able to select files. Make sure `trigger` is set correctly in Dashboard options', 'error')
     }
 
-    document.body.addEventListener('keyup', this.handleEscapeKeyPress)
+    if (!this.opts.inline) {
+      document.addEventListener('keydown', this.onKeydown)
+    }
 
     // Drag Drop
     this.removeDragDropListener = dragDrop(this.el, (files) => {
       this.handleDrop(files)
     })
+
+    this.uppy.on('dashboard:file-card', this.handleFileCard)
+
+    window.addEventListener('resize', this.updateDashboardElWidth)
   }
 
   removeEvents () {
@@ -221,27 +307,20 @@ module.exports = class DashboardUI extends Plugin {
       showModalTrigger.forEach(trigger => trigger.removeEventListener('click', this.openModal))
     }
 
-    this.removeDragDropListener()
-    document.body.removeEventListener('keyup', this.handleEscapeKeyPress)
-  }
+    if (!this.opts.inline) {
+      document.removeEventListener('keydown', this.onKeydown)
+    }
 
-  actions () {
-    this.core.on('core:file-added', this.hideAllPanels)
-    this.core.on('dashboard:file-card', this.handleFileCard)
+    this.removeDragDropListener()
 
-    window.addEventListener('resize', this.updateDashboardElWidth)
-  }
+    this.uppy.off('dashboard:file-card', this.handleFileCard)
 
-  removeActions () {
     window.removeEventListener('resize', this.updateDashboardElWidth)
-
-    this.core.off('core:file-added', this.hideAllPanels)
-    this.core.off('dashboard:file-card', this.handleFileCard)
   }
 
   updateDashboardElWidth () {
-    const dashboardEl = this.target.querySelector('.UppyDashboard-inner')
-    this.core.log(`Dashboard width: ${dashboardEl.offsetWidth}`)
+    const dashboardEl = this.el.querySelector('.uppy-Dashboard-inner')
+    this.uppy.log(`Dashboard width: ${dashboardEl.offsetWidth}`)
 
     this.setPluginState({
       containerWidth: dashboardEl.offsetWidth
@@ -255,10 +334,10 @@ module.exports = class DashboardUI extends Plugin {
   }
 
   handleDrop (files) {
-    this.core.log('[Dashboard] Files were dropped')
+    this.uppy.log('[Dashboard] Files were dropped')
 
     files.forEach((file) => {
-      this.core.addFile({
+      this.uppy.addFile({
         source: this.id,
         name: file.name,
         type: file.type,
@@ -267,18 +346,6 @@ module.exports = class DashboardUI extends Plugin {
     })
   }
 
-  cancelAll () {
-    this.core.emit('core:cancel-all')
-  }
-
-  pauseAll () {
-    this.core.emit('core:pause-all')
-  }
-
-  resumeAll () {
-    this.core.emit('core:resume-all')
-  }
-
   render (state) {
     const pluginState = this.getPluginState()
     const files = state.files
@@ -306,42 +373,53 @@ module.exports = class DashboardUI extends Plugin {
     totalSize = prettyBytes(totalSize)
     totalUploadedSize = prettyBytes(totalUploadedSize)
 
-    const acquirers = pluginState.targets.filter(target => {
-      const plugin = this.core.getPlugin(target.id)
-      target.icon = plugin.icon || this.opts.defaultTabIcon
-      target.render = plugin.render
-      return target.type === 'acquirer'
-    })
+    const attachRenderFunctionToTarget = (target) => {
+      const plugin = this.uppy.getPlugin(target.id)
+      return Object.assign({}, target, {
+        icon: plugin.icon || this.opts.defaultTabIcon,
+        render: plugin.render
+      })
+    }
 
-    const progressindicators = pluginState.targets.filter(target => {
-      const plugin = this.core.getPlugin(target.id)
-      target.icon = plugin.icon || this.opts.defaultTabIcon
-      target.render = plugin.render
-      return target.type === 'progressindicator'
-    })
+    const isSupported = (target) => {
+      const plugin = this.uppy.getPlugin(target.id)
+      // If the plugin does not provide a `supported` check, assume the plugin works everywhere.
+      if (typeof plugin.isSupported !== 'function') {
+        return true
+      }
+      return plugin.isSupported()
+    }
+
+    const acquirers = pluginState.targets
+      .filter(target => target.type === 'acquirer' && isSupported(target))
+      .map(attachRenderFunctionToTarget)
+
+    const progressindicators = pluginState.targets
+      .filter(target => target.type === 'progressindicator')
+      .map(attachRenderFunctionToTarget)
 
     const startUpload = (ev) => {
-      this.core.upload().catch((err) => {
+      this.uppy.upload().catch((err) => {
         // Log error.
-        this.core.log(err.stack || err.message || err)
+        this.uppy.log(err.stack || err.message || err)
       })
     }
 
     const cancelUpload = (fileID) => {
-      this.core.emit('core:upload-cancel', fileID)
-      this.core.emit('core:file-remove', fileID)
+      this.uppy.emit('upload-cancel', fileID)
+      this.uppy.removeFile(fileID)
     }
 
     const showFileCard = (fileID) => {
-      this.core.emit('dashboard:file-card', fileID)
+      this.uppy.emit('dashboard:file-card', fileID)
     }
 
     const fileCardDone = (meta, fileID) => {
-      this.core.emit('core:update-meta', meta, fileID)
-      this.core.emit('dashboard:file-card')
+      this.uppy.setFileMeta(fileID, meta)
+      this.uppy.emit('dashboard:file-card')
     }
 
-    return Dashboard({
+    return DashboardUI({
       state: state,
       modal: pluginState,
       newFiles: newFiles,
@@ -350,31 +428,33 @@ module.exports = class DashboardUI extends Plugin {
       totalProgress: state.totalProgress,
       acquirers: acquirers,
       activePanel: pluginState.activePanel,
-      getPlugin: this.core.getPlugin,
+      getPlugin: this.uppy.getPlugin,
       progressindicators: progressindicators,
-      autoProceed: this.core.opts.autoProceed,
+      autoProceed: this.uppy.opts.autoProceed,
       hideUploadButton: this.opts.hideUploadButton,
       id: this.id,
       closeModal: this.requestCloseModal,
       handleClickOutside: this.handleClickOutside,
+      handleInputChange: this.handleInputChange,
+      handlePaste: this.handlePaste,
       showProgressDetails: this.opts.showProgressDetails,
       inline: this.opts.inline,
       semiTransparent: this.opts.semiTransparent,
       showPanel: this.showPanel,
       hideAllPanels: this.hideAllPanels,
-      log: this.core.log,
-      i18n: this.containerWidth,
-      pauseAll: this.pauseAll,
-      resumeAll: this.resumeAll,
-      addFile: this.core.addFile,
-      removeFile: this.core.removeFile,
-      info: this.core.info,
+      log: this.uppy.log,
+      i18n: this.i18n,
+      // pauseAll: this.pauseAll,
+      // resumeAll: this.resumeAll,
+      addFile: this.uppy.addFile,
+      removeFile: this.uppy.removeFile,
+      info: this.uppy.info,
       note: this.opts.note,
-      metaFields: state.metaFields,
-      resumableUploads: this.core.state.capabilities.resumableUploads || false,
+      metaFields: this.getPluginState().metaFields,
+      resumableUploads: this.uppy.state.capabilities.resumableUploads || false,
       startUpload: startUpload,
-      pauseUpload: this.core.pauseResume,
-      retryUpload: this.core.retryUpload,
+      pauseUpload: this.uppy.pauseResume,
+      retryUpload: this.uppy.retryUpload,
       cancelUpload: cancelUpload,
       fileCardFor: pluginState.fileCardFor,
       showFileCard: showFileCard,
@@ -388,7 +468,7 @@ module.exports = class DashboardUI extends Plugin {
   }
 
   discoverProviderPlugins () {
-    this.core.iteratePlugins((plugin) => {
+    this.uppy.iteratePlugins((plugin) => {
       if (plugin && !plugin.target && plugin.opts && plugin.opts.target === this.constructor) {
         this.addTarget(plugin)
       }
@@ -401,29 +481,30 @@ module.exports = class DashboardUI extends Plugin {
       isHidden: true,
       showFileCard: false,
       activePanel: false,
+      metaFields: this.opts.metaFields,
       targets: []
     })
 
     const target = this.opts.target
-
     if (target) {
       this.mount(target, this)
     }
 
     const plugins = this.opts.plugins || []
     plugins.forEach((pluginID) => {
-      const plugin = this.core.getPlugin(pluginID)
+      const plugin = this.uppy.getPlugin(pluginID)
       if (plugin) plugin.mount(this, plugin)
     })
 
     if (!this.opts.disableStatusBar) {
-      this.core.use(StatusBar, {
-        target: this
+      this.uppy.use(StatusBar, {
+        target: this,
+        hideUploadButton: this.opts.hideUploadButton
       })
     }
 
     if (!this.opts.disableInformer) {
-      this.core.use(Informer, {
+      this.uppy.use(Informer, {
         target: this
       })
     }
@@ -431,30 +512,28 @@ module.exports = class DashboardUI extends Plugin {
     this.discoverProviderPlugins()
 
     this.initEvents()
-    this.actions()
   }
 
   uninstall () {
     if (!this.opts.disableInformer) {
-      const informer = this.core.getPlugin('Informer')
-      if (informer) this.core.removePlugin(informer)
+      const informer = this.uppy.getPlugin('Informer')
+      if (informer) this.uppy.removePlugin(informer)
     }
 
     if (!this.opts.disableStatusBar) {
-      const statusBar = this.core.getPlugin('StatusBar')
+      const statusBar = this.uppy.getPlugin('StatusBar')
       // Checking if this plugin exists, in case it was removed by uppy-core
       // before the Dashboard was.
-      if (statusBar) this.core.removePlugin(statusBar)
+      if (statusBar) this.uppy.removePlugin(statusBar)
     }
 
     const plugins = this.opts.plugins || []
     plugins.forEach((pluginID) => {
-      const plugin = this.core.getPlugin(pluginID)
+      const plugin = this.uppy.getPlugin(pluginID)
       if (plugin) plugin.unmount()
     })
 
     this.unmount()
-    this.removeActions()
     this.removeEvents()
   }
 }

+ 48 - 73
src/plugins/DragDrop/index.js

@@ -1,35 +1,24 @@
-const Plugin = require('./../Plugin')
+const Plugin = require('../../core/Plugin')
 const Translator = require('../../core/Translator')
 const { toArray } = require('../../core/Utils')
 const dragDrop = require('drag-drop')
-const html = require('yo-yo')
+const { h } = require('preact')
 
 /**
  * Drag & Drop plugin
  *
  */
 module.exports = class DragDrop extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'acquirer'
-    this.id = 'DragDrop'
+    this.id = this.opts.id || 'DragDrop'
     this.title = 'Drag & Drop'
-    this.icon = html`
-      <svg aria-hidden="true" class="UppyIcon" width="28" height="28" viewBox="0 0 16 16">
-        <path d="M15.982 2.97c0-.02 0-.02-.018-.037 0-.017-.017-.035-.035-.053 0 0 0-.018-.02-.018-.017-.018-.034-.053-.052-.07L13.19.123c-.017-.017-.034-.035-.07-.053h-.018c-.018-.017-.035-.017-.053-.034h-.02c-.017 0-.034-.018-.052-.018h-6.31a.415.415 0 0 0-.446.426V11.11c0 .25.196.446.445.446h8.89A.44.44 0 0 0 16 11.11V3.023c-.018-.018-.018-.035-.018-.053zm-2.65-1.46l1.157 1.157h-1.157V1.51zm1.78 9.157h-8V.89h5.332v2.22c0 .25.196.446.445.446h2.22v7.11z"/>
-        <path d="M9.778 12.89H4V2.666a.44.44 0 0 0-.444-.445.44.44 0 0 0-.445.445v10.666c0 .25.197.445.446.445h6.222a.44.44 0 0 0 .444-.445.44.44 0 0 0-.444-.444z"/>
-        <path d="M.444 16h6.223a.44.44 0 0 0 .444-.444.44.44 0 0 0-.443-.445H.89V4.89a.44.44 0 0 0-.446-.446A.44.44 0 0 0 0 4.89v10.666c0 .248.196.444.444.444z"/>
-      </svg>
-    `
 
     const defaultLocale = {
       strings: {
         dropHereOr: 'Drop files here or',
         browse: 'browse'
-        // selectedFiles: {
-        //   0: '%{smart_count} file selected',
-        //   1: '%{smart_count} files selected'
-        // }
       }
     }
 
@@ -46,7 +35,7 @@ module.exports = class DragDrop extends Plugin {
     // Merge default options with the ones set by user
     this.opts = Object.assign({}, defaultOpts, opts)
 
-        // Check for browser dragDrop support
+    // Check for browser dragDrop support
     this.isDragDropSupported = this.checkDragDropSupport()
 
     this.locale = Object.assign({}, defaultLocale, this.opts.locale)
@@ -58,15 +47,16 @@ module.exports = class DragDrop extends Plugin {
 
     // Bind `this` to class methods
     this.handleDrop = this.handleDrop.bind(this)
-    this.checkDragDropSupport = this.checkDragDropSupport.bind(this)
+    this.handleBrowseClick = this.handleBrowseClick.bind(this)
     this.handleInputChange = this.handleInputChange.bind(this)
+    this.checkDragDropSupport = this.checkDragDropSupport.bind(this)
     this.render = this.render.bind(this)
   }
 
-/**
- * Checks if the browser supports Drag & Drop (not supported on mobile devices, for example).
- * @return {Boolean} true if supported, false otherwise
- */
+  /**
+   * Checks if the browser supports Drag & Drop (not supported on mobile devices, for example).
+   * @return {Boolean}
+   */
   checkDragDropSupport () {
     const div = document.createElement('div')
 
@@ -86,10 +76,10 @@ module.exports = class DragDrop extends Plugin {
   }
 
   handleDrop (files) {
-    this.core.log('[DragDrop] Files dropped')
+    this.uppy.log('[DragDrop] Files dropped')
 
     files.forEach((file) => {
-      this.core.addFile({
+      this.uppy.addFile({
         source: this.id,
         name: file.name,
         type: file.type,
@@ -99,12 +89,12 @@ module.exports = class DragDrop extends Plugin {
   }
 
   handleInputChange (ev) {
-    this.core.log('[DragDrop] Files selected through input')
+    this.uppy.log('[DragDrop] Files selected through input')
 
     const files = toArray(ev.target.files)
 
     files.forEach((file) => {
-      this.core.addFile({
+      this.uppy.addFile({
         source: this.id,
         name: file.name,
         type: file.type,
@@ -113,68 +103,53 @@ module.exports = class DragDrop extends Plugin {
     })
   }
 
+  handleBrowseClick (ev) {
+    this.input.click()
+  }
+
   render (state) {
-    const onSelect = (ev) => {
-      const input = this.target.querySelector('.uppy-DragDrop-input')
-      input.click()
+    const DragDropClass = `uppy uppy-DragDrop-container ${this.isDragDropSupported ? 'is-dragdrop-supported' : ''}`
+    const DragDropStyle = {
+      width: this.opts.width,
+      height: this.opts.height
     }
-
-    // const selectedFilesCount = Object.keys(state.files).length
-
-    return html`
-      <div class="Uppy UppyTheme--default uppy-DragDrop-container ${this.isDragDropSupported ? 'is-dragdrop-supported' : ''}"
-           style="width: ${this.opts.width}; height: ${this.opts.height};">
-        <form class="uppy-DragDrop-inner" onsubmit=${(ev) => ev.preventDefault()}>
-          <svg class="UppyIcon uppy-DragDrop-arrow" aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
-            <path d="M11 10V0H5v10H2l6 6 6-6h-3zm0 0" fill-rule="evenodd"/>
+    return (
+      <div class={DragDropClass} style={DragDropStyle} onclick={this.handleBrowseClick}>
+        <div class="uppy-DragDrop-inner">
+          <svg aria-hidden="true" class="UppyIcon uppy-DragDrop-arrow" width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
+            <path d="M11 10V0H5v10H2l6 6 6-6h-3zm0 0" fill-rule="evenodd" />
           </svg>
-          <input class="uppy-DragDrop-input uppy-DragDrop-focus"
-                 type="file"
-                 name="files[]"
-                 multiple="true"
-                 value=""
-                 onchange=${this.handleInputChange.bind(this)} />
-          <label class="uppy-DragDrop-label" onclick=${onSelect}>
-            ${this.i18n('dropHereOr')}
-            <span class="uppy-DragDrop-dragText">${this.i18n('browse')}</span>
+          <input class="uppy-DragDrop-input"
+            type="file"
+            name="files[]"
+            multiple="true"
+            value=""
+            ref={(input) => {
+              this.input = input
+            }}
+            onchange={this.handleInputChange} />
+          <label class="uppy-DragDrop-label" onclick={this.handleBrowseClick}>
+            {this.i18n('dropHereOr')} <span class="uppy-DragDrop-dragText">{this.i18n('browse')}</span>
           </label>
-          <span class="uppy-DragDrop-note">${this.opts.note}</span>
-        </form>
+          <span class="uppy-DragDrop-note">{this.opts.note}</span>
+        </div>
       </div>
-    `
+    )
   }
 
-  // ${selectedFilesCount > 0
-  // ? html`<div class="uppy-DragDrop-selectedCount">
-  //     ${this.i18n('selectedFiles', {'smart_count': selectedFilesCount})}
-  //   </div>`
-  // : ''}
-
   install () {
     const target = this.opts.target
-    const plugin = this
     if (target) {
-      this.mount(target, plugin)
+      this.mount(target, this)
     }
-  }
-
-  uninstall () {
-    this.unmount()
-  }
-
-  mount (...args) {
-    super.mount(...args)
-
-    const dndContainer = this.target.querySelector('.uppy-DragDrop-container')
-    this.removeDragDropListener = dragDrop(dndContainer, (files) => {
+    this.removeDragDropListener = dragDrop(this.el, (files) => {
       this.handleDrop(files)
-      this.core.log(files)
+      this.uppy.log(files)
     })
   }
 
-  unmount (...args) {
+  uninstall () {
+    this.unmount()
     this.removeDragDropListener()
-
-    super.unmount(...args)
   }
 }

+ 11 - 9
src/plugins/Dropbox/icons.js

@@ -1,12 +1,14 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 
 module.exports = {
-  folder: () =>
-    html`<svg aria-hidden="true" class="UppyIcon" style="width:16px;margin-right:3px" viewBox="0 0 276.157 276.157">
-      <path d="M273.08 101.378c-3.3-4.65-8.86-7.32-15.254-7.32h-24.34V67.59c0-10.2-8.3-18.5-18.5-18.5h-85.322c-3.63 0-9.295-2.875-11.436-5.805l-6.386-8.735c-4.982-6.814-15.104-11.954-23.546-11.954H58.73c-9.292 0-18.638 6.608-21.737 15.372l-2.033 5.752c-.958 2.71-4.72 5.37-7.596 5.37H18.5C8.3 49.09 0 57.39 0 67.59v167.07c0 .886.16 1.73.443 2.52.152 3.306 1.18 6.424 3.053 9.064 3.3 4.652 8.86 7.32 15.255 7.32h188.487c11.395 0 23.27-8.425 27.035-19.18l40.677-116.188c2.11-6.035 1.43-12.164-1.87-16.816zM18.5 64.088h8.864c9.295 0 18.64-6.607 21.738-15.37l2.032-5.75c.96-2.712 4.722-5.373 7.597-5.373h29.565c3.63 0 9.295 2.876 11.437 5.806l6.386 8.735c4.982 6.815 15.104 11.954 23.546 11.954h85.322c1.898 0 3.5 1.602 3.5 3.5v26.47H69.34c-11.395 0-23.27 8.423-27.035 19.178L15 191.23V67.59c0-1.898 1.603-3.5 3.5-3.5zm242.29 49.15l-40.676 116.188c-1.674 4.78-7.812 9.135-12.877 9.135H18.75c-1.447 0-2.576-.372-3.02-.997-.442-.625-.422-1.814.057-3.18l40.677-116.19c1.674-4.78 7.812-9.134 12.877-9.134h188.487c1.448 0 2.577.372 3.02.997.443.625.423 1.814-.056 3.18z"/>
-  </svg>`,
-  file: () =>
-    html`<svg aria-hidden="true" class="UppyIcon" width="11" height="14.5" viewBox="0 0 44 58">
-    <path d="M27.437.517a1 1 0 0 0-.094.03H4.25C2.037.548.217 2.368.217 4.58v48.405c0 2.212 1.82 4.03 4.03 4.03H39.03c2.21 0 4.03-1.818 4.03-4.03V15.61a1 1 0 0 0-.03-.28 1 1 0 0 0 0-.093 1 1 0 0 0-.03-.032 1 1 0 0 0 0-.03 1 1 0 0 0-.032-.063 1 1 0 0 0-.03-.063 1 1 0 0 0-.032 0 1 1 0 0 0-.03-.063 1 1 0 0 0-.032-.03 1 1 0 0 0-.03-.063 1 1 0 0 0-.063-.062l-14.593-14a1 1 0 0 0-.062-.062A1 1 0 0 0 28 .708a1 1 0 0 0-.374-.157 1 1 0 0 0-.156 0 1 1 0 0 0-.03-.03l-.003-.003zM4.25 2.547h22.218v9.97c0 2.21 1.82 4.03 4.03 4.03h10.564v36.438a2.02 2.02 0 0 1-2.032 2.032H4.25c-1.13 0-2.032-.9-2.032-2.032V4.58c0-1.13.902-2.032 2.03-2.032zm24.218 1.345l10.375 9.937.75.718H30.5c-1.13 0-2.032-.9-2.032-2.03V3.89z" />
-  </svg>`
+  folder: () => {
+    return <svg aria-hidden="true" class="UppyIcon" style="width:16px;margin-right:3px" viewBox="0 0 276.157 276.157">
+      <path d="M273.08 101.378c-3.3-4.65-8.86-7.32-15.254-7.32h-24.34V67.59c0-10.2-8.3-18.5-18.5-18.5h-85.322c-3.63 0-9.295-2.875-11.436-5.805l-6.386-8.735c-4.982-6.814-15.104-11.954-23.546-11.954H58.73c-9.292 0-18.638 6.608-21.737 15.372l-2.033 5.752c-.958 2.71-4.72 5.37-7.596 5.37H18.5C8.3 49.09 0 57.39 0 67.59v167.07c0 .886.16 1.73.443 2.52.152 3.306 1.18 6.424 3.053 9.064 3.3 4.652 8.86 7.32 15.255 7.32h188.487c11.395 0 23.27-8.425 27.035-19.18l40.677-116.188c2.11-6.035 1.43-12.164-1.87-16.816zM18.5 64.088h8.864c9.295 0 18.64-6.607 21.738-15.37l2.032-5.75c.96-2.712 4.722-5.373 7.597-5.373h29.565c3.63 0 9.295 2.876 11.437 5.806l6.386 8.735c4.982 6.815 15.104 11.954 23.546 11.954h85.322c1.898 0 3.5 1.602 3.5 3.5v26.47H69.34c-11.395 0-23.27 8.423-27.035 19.178L15 191.23V67.59c0-1.898 1.603-3.5 3.5-3.5zm242.29 49.15l-40.676 116.188c-1.674 4.78-7.812 9.135-12.877 9.135H18.75c-1.447 0-2.576-.372-3.02-.997-.442-.625-.422-1.814.057-3.18l40.677-116.19c1.674-4.78 7.812-9.134 12.877-9.134h188.487c1.448 0 2.577.372 3.02.997.443.625.423 1.814-.056 3.18z" />
+    </svg>
+  },
+  file: () => {
+    return <svg aria-hidden="true" class="UppyIcon" width="11" height="14.5" viewBox="0 0 44 58">
+      <path d="M27.437.517a1 1 0 0 0-.094.03H4.25C2.037.548.217 2.368.217 4.58v48.405c0 2.212 1.82 4.03 4.03 4.03H39.03c2.21 0 4.03-1.818 4.03-4.03V15.61a1 1 0 0 0-.03-.28 1 1 0 0 0 0-.093 1 1 0 0 0-.03-.032 1 1 0 0 0 0-.03 1 1 0 0 0-.032-.063 1 1 0 0 0-.03-.063 1 1 0 0 0-.032 0 1 1 0 0 0-.03-.063 1 1 0 0 0-.032-.03 1 1 0 0 0-.03-.063 1 1 0 0 0-.063-.062l-14.593-14a1 1 0 0 0-.062-.062A1 1 0 0 0 28 .708a1 1 0 0 0-.374-.157 1 1 0 0 0-.156 0 1 1 0 0 0-.03-.03l-.003-.003zM4.25 2.547h22.218v9.97c0 2.21 1.82 4.03 4.03 4.03h10.564v36.438a2.02 2.02 0 0 1-2.032 2.032H4.25c-1.13 0-2.032-.9-2.032-2.032V4.58c0-1.13.902-2.032 2.03-2.032zm24.218 1.345l10.375 9.937.75.718H30.5c-1.13 0-2.032-.9-2.032-2.03V3.89z" />
+    </svg>
+  }
 }

+ 23 - 30
src/plugins/Dropbox/index.js

@@ -1,29 +1,26 @@
-const html = require('yo-yo')
-const Plugin = require('../Plugin')
-
-const Provider = require('../../Provider')
-
-const View = require('../../generic-provider-views')
+const Plugin = require('../../core/Plugin')
+const Provider = require('../Provider')
+const View = require('../Provider/view')
 const icons = require('./icons')
+const { h } = require('preact')
 
 module.exports = class Dropbox extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'acquirer'
-    this.id = 'Dropbox'
+    this.id = this.opts.id || 'Dropbox'
     this.title = 'Dropbox'
-    this.stateId = 'dropbox'
-    this.icon = () => html`
+    this.icon = () => (
       <svg class="UppyIcon" width="128" height="118" viewBox="0 0 128 118">
-        <path d="M38.145.777L1.108 24.96l25.608 20.507 37.344-23.06z"/>
-        <path d="M1.108 65.975l37.037 24.183L64.06 68.525l-37.343-23.06zM64.06 68.525l25.917 21.633 37.036-24.183-25.61-20.51z"/>
-        <path d="M127.014 24.96L89.977.776 64.06 22.407l37.345 23.06zM64.136 73.18l-25.99 21.567-11.122-7.262v8.142l37.112 22.256 37.114-22.256v-8.142l-11.12 7.262z"/>
+        <path d="M38.145.777L1.108 24.96l25.608 20.507 37.344-23.06z" />
+        <path d="M1.108 65.975l37.037 24.183L64.06 68.525l-37.343-23.06zM64.06 68.525l25.917 21.633 37.036-24.183-25.61-20.51z" />
+        <path d="M127.014 24.96L89.977.776 64.06 22.407l37.345 23.06zM64.136 73.18l-25.99 21.567-11.122-7.262v8.142l37.112 22.256 37.114-22.256v-8.142l-11.12 7.262z" />
       </svg>
-    `
+    )
 
     // writing out the key explicitly for readability the key used to store
     // the provider instance must be equal to this.id.
-    this.Dropbox = new Provider(core, {
+    this[this.id] = new Provider(uppy, {
       host: this.opts.host,
       provider: 'dropbox'
     })
@@ -31,7 +28,6 @@ module.exports = class Dropbox extends Plugin {
     this.files = []
 
     this.onAuth = this.onAuth.bind(this)
-    // Visual
     this.render = this.render.bind(this)
 
     // set default options
@@ -44,18 +40,14 @@ module.exports = class Dropbox extends Plugin {
   install () {
     this.view = new View(this)
     // Set default state
-    this.core.setState({
-      // writing out the key explicitly for readability the key used to store
-      // the plugin state must be equal to this.stateId.
-      dropbox: {
-        authenticated: false,
-        files: [],
-        folders: [],
-        directories: [],
-        activeRow: -1,
-        filterInput: '',
-        isSearchVisible: false
-      }
+    this.setPluginState({
+      authenticated: false,
+      files: [],
+      folders: [],
+      directories: [],
+      activeRow: -1,
+      filterInput: '',
+      isSearchVisible: false
     })
 
     const target = this.opts.target
@@ -65,11 +57,12 @@ module.exports = class Dropbox extends Plugin {
   }
 
   uninstall () {
+    this.view.tearDown()
     this.unmount()
   }
 
   onAuth (authenticated) {
-    this.view.updateState({authenticated})
+    this.setPluginState({ authenticated })
     if (authenticated) {
       this.view.getFolder()
     }

+ 22 - 20
src/plugins/Dummy.js

@@ -1,16 +1,15 @@
-const Plugin = require('./Plugin')
-const html = require('yo-yo')
-// const yo = require('yo-yo')
+const Plugin = require('../core/Plugin')
+const { h } = require('preact')
 
 /**
  * Dummy
  * A test plugin, does nothing useful
  */
 module.exports = class Dummy extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'acquirer'
-    this.id = 'Dummy'
+    this.id = this.opts.id || 'Dummy'
     this.title = 'Mr. Plugin'
 
     // set default options
@@ -19,7 +18,7 @@ module.exports = class Dummy extends Plugin {
     // merge default options with the ones set by user
     this.opts = Object.assign({}, defaultOptions, opts)
 
-    this.strange = html`<h1>this is strange 1</h1>`
+    this.strange = <h1>this is strange 1</h1>
     this.render = this.render.bind(this)
     this.install = this.install.bind(this)
   }
@@ -40,28 +39,31 @@ module.exports = class Dummy extends Plugin {
   }
 
   render (state) {
-    const bla = html`<h2>this is strange 2</h2>`
-    return html`
+    const bla = <h2>this is strange 2</h2>
+    return (
       <div class="wow-this-works">
-        <input class="UppyDummy-firstInput" type="text" value="hello" onload=${(el) => {
-          el.focus()
-        }} />
-        ${this.strange}
-        ${bla}
-        ${state.dummy.text}
+        <input class="UppyDummy-firstInput" type="text" value="hello" />
+        {this.strange}
+        {bla}
+        {state.dummy.text}
       </div>
-    `
+    )
   }
 
   install () {
-    this.core.setState({dummy: {text: '123'}})
+    this.uppy.setState({
+      dummy: { text: '123' }
+    })
 
     const target = this.opts.target
-    const plugin = this
-    this.target = this.mount(target, plugin)
+    if (target) {
+      this.mount(target, this)
+    }
 
     setTimeout(() => {
-      this.core.setState({dummy: {text: '!!!'}})
+      this.uppy.setState({
+        dummy: {text: '!!!'}
+      })
     }, 2000)
   }
 }

+ 42 - 31
src/plugins/FileInput.js

@@ -1,27 +1,27 @@
-const Plugin = require('./Plugin')
+const Plugin = require('../core/Plugin')
 const { toArray } = require('../core/Utils')
 const Translator = require('../core/Translator')
-const html = require('yo-yo')
+const { h } = require('preact')
 
 module.exports = class FileInput extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
-    this.id = 'FileInput'
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = this.opts.id || 'FileInput'
     this.title = 'File Input'
     this.type = 'acquirer'
 
     const defaultLocale = {
       strings: {
-        selectToUpload: 'Select to upload'
+        chooseFiles: 'Choose files'
       }
     }
 
     // Default options
     const defaultOptions = {
-      target: '.UppyForm',
+      target: null,
       getMetaFromForm: true,
-      replaceTargetContent: true,
-      multipleFiles: true,
+      replaceTargetContent: false,
+      allowMultipleFiles: true,
       pretty: true,
       locale: defaultLocale,
       inputName: 'files[]'
@@ -38,15 +38,17 @@ module.exports = class FileInput extends Plugin {
     this.i18n = this.translator.translate.bind(this.translator)
 
     this.render = this.render.bind(this)
+    this.handleInputChange = this.handleInputChange.bind(this)
+    this.handleClick = this.handleClick.bind(this)
   }
 
   handleInputChange (ev) {
-    this.core.log('All right, something selected through input...')
+    this.uppy.log('[FileInput] Something selected through input...')
 
     const files = toArray(ev.target.files)
 
     files.forEach((file) => {
-      this.core.addFile({
+      this.uppy.addFile({
         source: this.id,
         name: file.name,
         type: file.type,
@@ -55,32 +57,41 @@ module.exports = class FileInput extends Plugin {
     })
   }
 
-  render (state) {
-    const hiddenInputStyle = 'width: 0.1px; height: 0.1px; opacity: 0; overflow: hidden; position: absolute; z-index: -1;'
+  handleClick (ev) {
+    this.input.click()
+  }
 
-    const input = html`<input class="uppy-FileInput-input"
-           style="${this.opts.pretty ? hiddenInputStyle : ''}"
-           type="file"
-           name=${this.opts.inputName}
-           onchange=${this.handleInputChange.bind(this)}
-           multiple="${this.opts.multipleFiles ? 'true' : 'false'}"
-           value="">`
+  render (state) {
+    const hiddenInputStyle = {
+      width: '0.1px',
+      height: '0.1px',
+      opacity: 0,
+      overflow: 'hidden',
+      position: 'absolute',
+      zIndex: -1
+    }
 
-    return html`<form class="Uppy uppy-FileInput-form">
-      ${input}
-      ${this.opts.pretty
-        ? html`<button class="uppy-FileInput-btn" type="button" onclick=${() => input.click()}>
-          ${this.i18n('selectToUpload')}
-        </button>`
-       : null
-     }
-    </form>`
+    return <div class="uppy uppy-FileInput-container">
+      <input class="uppy-FileInput-input"
+        style={this.opts.pretty && hiddenInputStyle}
+        type="file"
+        name={this.opts.inputName}
+        onchange={this.handleInputChange}
+        multiple={this.opts.allowMultipleFiles}
+        ref={(input) => { this.input = input }} />
+      {this.opts.pretty &&
+        <button class="uppy-FileInput-btn" type="button" onclick={this.handleClick}>
+          {this.i18n('chooseFiles')}
+        </button>
+      }
+    </div>
   }
 
   install () {
     const target = this.opts.target
-    const plugin = this
-    this.target = this.mount(target, plugin)
+    if (target) {
+      this.mount(target, this)
+    }
   }
 
   uninstall () {

+ 13 - 13
src/plugins/Form.js

@@ -1,4 +1,4 @@
-const Plugin = require('./Plugin')
+const Plugin = require('../core/Plugin')
 const { findDOMElement } = require('../core/Utils')
 const getFormData = require('get-form-data')
 
@@ -6,8 +6,8 @@ const getFormData = require('get-form-data')
  * Form
  */
 module.exports = class Form extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'acquirer'
     this.id = 'Form'
     this.title = 'Form'
@@ -36,7 +36,7 @@ module.exports = class Form extends Plugin {
     if (!this.opts.triggerUploadOnSubmit) return
     console.log('PREVENT DEFAULT')
     ev.preventDefault()
-    this.core.upload()
+    this.uppy.upload()
   }
 
   handleSuccess (data) {
@@ -44,7 +44,7 @@ module.exports = class Form extends Plugin {
 
     data.forEach(fileID => {
       result[fileID] = {
-        url: this.core.state.files[fileID].uploadURL
+        url: this.uppy.state.files[fileID].uploadURL
         // transcoding/postprocessing result here too?
       }
     })
@@ -59,8 +59,8 @@ module.exports = class Form extends Plugin {
   addResultToForm (result) {
     if (!this.opts.addResultToForm) return
 
-    this.core.log('[Form] Adding result to the original form:')
-    this.core.log(result)
+    this.uppy.log('[Form] Adding result to the original form:')
+    this.uppy.log(result)
 
     let resultInput = this.form.querySelector(`[name="${this.opts.resultName}"]`)
     if (resultInput) {
@@ -76,9 +76,9 @@ module.exports = class Form extends Plugin {
   }
 
   handleUploadStart () {
-    if (this.opts.getMetaFromForm) return
+    if (!this.opts.getMetaFromForm) return
     const formMeta = getFormData(this.form)
-    this.core.setMeta(formMeta)
+    this.uppy.setMeta(formMeta)
   }
 
   install () {
@@ -89,13 +89,13 @@ module.exports = class Form extends Plugin {
     }
 
     this.form.addEventListener('submit', this.handleFormSubmit)
-    this.core.on('core:upload', this.handleUploadStart)
-    this.core.on('core:success', this.handleSuccess)
+    this.uppy.on('upload', this.handleUploadStart)
+    this.uppy.on('success', this.handleSuccess)
   }
 
   uninstall () {
     this.form.removeEventListener('submit', this.handleFormSubmit)
-    this.core.off('core:upload', this.handleUploadStart)
-    this.core.off('core:success', this.handleSuccess)
+    this.uppy.off('upload', this.handleUploadStart)
+    this.uppy.off('success', this.handleSuccess)
   }
 }

+ 1 - 1
src/plugins/GoldenRetriever/IndexedDBStore.js

@@ -86,7 +86,7 @@ class IndexedDBStore {
       this.ready = IndexedDBStore.cleanup()
         .then(createConnection, createConnection)
     } else {
-      this.ready = createConnection
+      this.ready = createConnection()
     }
   }
 

+ 63 - 43
src/plugins/GoldenRetriever/index.js

@@ -1,4 +1,4 @@
-const Plugin = require('../Plugin')
+const Plugin = require('../../core/Plugin')
 const ServiceWorkerStore = require('./ServiceWorkerStore')
 const IndexedDBStore = require('./IndexedDBStore')
 const MetaDataStore = require('./MetaDataStore')
@@ -11,11 +11,11 @@ const MetaDataStore = require('./MetaDataStore')
 * https://uppy.io/blog/2017/07/golden-retriever/
 */
 module.exports = class GoldenRetriever extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'debugger'
     this.id = 'GoldenRetriever'
-    this.title = 'Restore Files'
+    this.title = 'Golden Retriever'
 
     const defaultOptions = {
       expires: 24 * 60 * 60 * 1000, // 24 hours
@@ -26,16 +26,16 @@ module.exports = class GoldenRetriever extends Plugin {
 
     this.MetaDataStore = new MetaDataStore({
       expires: this.opts.expires,
-      storeName: core.getID()
+      storeName: uppy.getID()
     })
     this.ServiceWorkerStore = null
     if (this.opts.serviceWorker) {
-      this.ServiceWorkerStore = new ServiceWorkerStore({ storeName: core.getID() })
+      this.ServiceWorkerStore = new ServiceWorkerStore({ storeName: uppy.getID() })
     }
     this.IndexedDBStore = new IndexedDBStore(Object.assign(
       { expires: this.opts.expires },
       opts.indexedDB || {},
-      { storeName: core.getID() }))
+      { storeName: uppy.getID() }))
 
     this.saveFilesStateToLocalStorage = this.saveFilesStateToLocalStorage.bind(this)
     this.loadFilesStateFromLocalStorage = this.loadFilesStateFromLocalStorage.bind(this)
@@ -48,8 +48,13 @@ module.exports = class GoldenRetriever extends Plugin {
     const savedState = this.MetaDataStore.load()
 
     if (savedState) {
-      this.core.log('Recovered some state from Local Storage')
-      this.core.setState(savedState)
+      this.uppy.log('[GoldenRetriever] Recovered some state from Local Storage')
+      this.uppy.setState({
+        currentUploads: savedState.currentUploads || {},
+        files: savedState.files || {}
+      })
+
+      this.savedPluginData = savedState.pluginData
     }
   }
 
@@ -60,9 +65,9 @@ module.exports = class GoldenRetriever extends Plugin {
   getWaitingFiles () {
     const waitingFiles = {}
 
-    const allFiles = this.core.state.files
+    const allFiles = this.uppy.state.files
     Object.keys(allFiles).forEach((fileID) => {
-      const file = this.core.getFile(fileID)
+      const file = this.uppy.getFile(fileID)
       if (!file.progress || !file.progress.uploadStarted) {
         waitingFiles[fileID] = file
       }
@@ -79,13 +84,13 @@ module.exports = class GoldenRetriever extends Plugin {
   getUploadingFiles () {
     const uploadingFiles = {}
 
-    const { currentUploads } = this.core.state
+    const { currentUploads } = this.uppy.state
     if (currentUploads) {
       const uploadIDs = Object.keys(currentUploads)
       uploadIDs.forEach((uploadID) => {
         const filesInUpload = currentUploads[uploadID].fileIDs
         filesInUpload.forEach((fileID) => {
-          uploadingFiles[fileID] = this.core.getFile(fileID)
+          uploadingFiles[fileID] = this.uppy.getFile(fileID)
         })
       })
     }
@@ -99,22 +104,31 @@ module.exports = class GoldenRetriever extends Plugin {
       this.getUploadingFiles()
     )
 
+    const pluginData = {}
+    // TODO Find a better way to do this?
+    // Other plugins can attach a restore:get-data listener that receives this callback.
+    // Plugins can then use this callback (sync) to provide data to be stored.
+    this.uppy.emit('restore:get-data', (data) => {
+      Object.assign(pluginData, data)
+    })
+
     this.MetaDataStore.save({
-      currentUploads: this.core.state.currentUploads,
-      files: filesToSave
+      currentUploads: this.uppy.state.currentUploads,
+      files: filesToSave,
+      pluginData: pluginData
     })
   }
 
   loadFileBlobsFromServiceWorker () {
     this.ServiceWorkerStore.list().then((blobs) => {
       const numberOfFilesRecovered = Object.keys(blobs).length
-      const numberOfFilesTryingToRecover = Object.keys(this.core.state.files).length
+      const numberOfFilesTryingToRecover = Object.keys(this.uppy.state.files).length
       if (numberOfFilesRecovered === numberOfFilesTryingToRecover) {
-        this.core.log(`Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`)
-        this.core.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000)
+        this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`)
+        this.uppy.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000)
         this.onBlobsLoaded(blobs)
       } else {
-        this.core.log('Failed to recover blobs from Service Worker, trying IndexedDB now...')
+        this.uppy.log('[GoldenRetriever] Failed to recover blobs from Service Worker, trying IndexedDB now...')
         this.loadFileBlobsFromIndexedDB()
       }
     })
@@ -125,19 +139,19 @@ module.exports = class GoldenRetriever extends Plugin {
       const numberOfFilesRecovered = Object.keys(blobs).length
 
       if (numberOfFilesRecovered > 0) {
-        this.core.log(`Successfully recovered ${numberOfFilesRecovered} blobs from Indexed DB!`)
-        this.core.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000)
+        this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from Indexed DB!`)
+        this.uppy.info(`Successfully recovered ${numberOfFilesRecovered} files`, 'success', 3000)
         return this.onBlobsLoaded(blobs)
       }
-      this.core.log('Couldn’t recover anything from IndexedDB :(')
+      this.uppy.log('[GoldenRetriever] Couldn’t recover anything from IndexedDB :(')
     })
   }
 
   onBlobsLoaded (blobs) {
     const obsoleteBlobs = []
-    const updatedFiles = Object.assign({}, this.core.state.files)
+    const updatedFiles = Object.assign({}, this.uppy.state.files)
     Object.keys(blobs).forEach((fileID) => {
-      const originalFile = this.core.getFile(fileID)
+      const originalFile = this.uppy.getFile(fileID)
       if (!originalFile) {
         obsoleteBlobs.push(fileID)
         return
@@ -152,16 +166,18 @@ module.exports = class GoldenRetriever extends Plugin {
       const updatedFile = Object.assign({}, originalFile, updatedFileData)
       updatedFiles[fileID] = updatedFile
 
-      this.core.generatePreview(updatedFile)
+      this.uppy.generatePreview(updatedFile)
     })
-    this.core.setState({
+
+    this.uppy.setState({
       files: updatedFiles
     })
-    this.core.emit('core:restored')
+
+    this.uppy.emit('restored', this.savedPluginData)
 
     if (obsoleteBlobs.length) {
       this.deleteBlobs(obsoleteBlobs).then(() => {
-        this.core.log(`[GoldenRetriever] cleaned up ${obsoleteBlobs.length} old files`)
+        this.uppy.log(`[GoldenRetriever] Cleaned up ${obsoleteBlobs.length} old files`)
       })
     }
   }
@@ -182,51 +198,55 @@ module.exports = class GoldenRetriever extends Plugin {
   install () {
     this.loadFilesStateFromLocalStorage()
 
-    if (Object.keys(this.core.state.files).length > 0) {
+    if (Object.keys(this.uppy.state.files).length > 0) {
       if (this.ServiceWorkerStore) {
-        this.core.log('Attempting to load files from Service Worker...')
+        this.uppy.log('[GoldenRetriever] Attempting to load files from Service Worker...')
         this.loadFileBlobsFromServiceWorker()
       } else {
-        this.core.log('Attempting to load files from Indexed DB...')
+        this.uppy.log('[GoldenRetriever] Attempting to load files from Indexed DB...')
         this.loadFileBlobsFromIndexedDB()
       }
+    } else {
+      this.uppy.log('[GoldenRetriever] No files need to be loaded, only restoring processing state...')
+      this.onBlobsLoaded([])
     }
 
-    this.core.on('core:file-added', (file) => {
+    this.uppy.on('file-added', (file) => {
       if (file.isRemote) return
 
       if (this.ServiceWorkerStore) {
         this.ServiceWorkerStore.put(file).catch((err) => {
-          this.core.log('Could not store file', 'error')
-          this.core.log(err)
+          this.uppy.log('[GoldenRetriever] Could not store file', 'error')
+          this.uppy.log(err)
         })
       }
 
       this.IndexedDBStore.put(file).catch((err) => {
-        this.core.log('Could not store file', 'error')
-        this.core.log(err)
+        this.uppy.log('[GoldenRetriever] Could not store file', 'error')
+        this.uppy.log(err)
       })
     })
 
-    this.core.on('core:file-removed', (fileID) => {
+    this.uppy.on('file-removed', (fileID) => {
       if (this.ServiceWorkerStore) this.ServiceWorkerStore.delete(fileID)
       this.IndexedDBStore.delete(fileID)
     })
 
-    this.core.on('core:success', (fileIDs) => {
+    this.uppy.on('complete', ({ successful }) => {
+      const fileIDs = successful.map((file) => file.id)
       this.deleteBlobs(fileIDs).then(() => {
-        this.core.log(`[GoldenRetriever] removed ${fileIDs.length} files that finished uploading`)
+        this.uppy.log(`[GoldenRetriever] Removed ${successful.length} files that finished uploading`)
       })
     })
 
-    this.core.on('core:state-update', this.saveFilesStateToLocalStorage)
+    this.uppy.on('state-update', this.saveFilesStateToLocalStorage)
 
-    this.core.on('core:restored', () => {
+    this.uppy.on('restored', () => {
       // start all uploads again when file blobs are restored
-      const { currentUploads } = this.core.getState()
+      const { currentUploads } = this.uppy.getState()
       if (currentUploads) {
         Object.keys(currentUploads).forEach((uploadId) => {
-          this.core.restore(uploadId, currentUploads[uploadId])
+          this.uppy.restore(uploadId, currentUploads[uploadId])
         })
       }
     })

+ 21 - 31
src/plugins/GoogleDrive/index.js

@@ -1,26 +1,20 @@
-const html = require('yo-yo')
-const Plugin = require('../Plugin')
-
-const Provider = require('../../Provider')
-
-const View = require('../../generic-provider-views')
+const Plugin = require('../../core/Plugin')
+const Provider = require('../Provider')
+const View = require('../Provider/view')
+const { h } = require('preact')
 
 module.exports = class GoogleDrive extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'acquirer'
-    this.id = 'GoogleDrive'
+    this.id = this.opts.id || 'GoogleDrive'
     this.title = 'Google Drive'
-    this.stateId = 'googleDrive'
-    this.icon = () => html`
+    this.icon = () =>
       <svg aria-hidden="true" class="UppyIcon UppyModalTab-icon" width="28" height="28" viewBox="0 0 16 16">
-        <path d="M2.955 14.93l2.667-4.62H16l-2.667 4.62H2.955zm2.378-4.62l-2.666 4.62L0 10.31l5.19-8.99 2.666 4.62-2.523 4.37zm10.523-.25h-5.333l-5.19-8.99h5.334l5.19 8.99z"/>
+        <path d="M2.955 14.93l2.667-4.62H16l-2.667 4.62H2.955zm2.378-4.62l-2.666 4.62L0 10.31l5.19-8.99 2.666 4.62-2.523 4.37zm10.523-.25h-5.333l-5.19-8.99h5.334l5.19 8.99z" />
       </svg>
-    `
 
-    // writing out the key explicitly for readability the key used to store
-    // the provider instance must be equal to this.id.
-    this.GoogleDrive = new Provider(core, {
+    this[this.id] = new Provider(uppy, {
       host: this.opts.host,
       provider: 'drive',
       authProvider: 'google'
@@ -29,7 +23,6 @@ module.exports = class GoogleDrive extends Plugin {
     this.files = []
 
     this.onAuth = this.onAuth.bind(this)
-    // Visual
     this.render = this.render.bind(this)
 
     // set default options
@@ -42,18 +35,14 @@ module.exports = class GoogleDrive extends Plugin {
   install () {
     this.view = new View(this)
     // Set default state for Google Drive
-    this.core.setState({
-      // writing out the key explicitly for readability the key used to store
-      // the plugin state must be equal to this.stateId.
-      googleDrive: {
-        authenticated: false,
-        files: [],
-        folders: [],
-        directories: [],
-        activeRow: -1,
-        filterInput: '',
-        isSearchVisible: false
-      }
+    this.setPluginState({
+      authenticated: false,
+      files: [],
+      folders: [],
+      directories: [],
+      activeRow: -1,
+      filterInput: '',
+      isSearchVisible: false
     })
 
     const target = this.opts.target
@@ -63,11 +52,12 @@ module.exports = class GoogleDrive extends Plugin {
   }
 
   uninstall () {
+    this.view.tearDown()
     this.unmount()
   }
 
   onAuth (authenticated) {
-    this.view.updateState({authenticated})
+    this.setPluginState({ authenticated })
     if (authenticated) {
       this.view.getFolder('root')
     }
@@ -82,7 +72,7 @@ module.exports = class GoogleDrive extends Plugin {
   }
 
   getItemIcon (item) {
-    return html`<img src=${item.iconLink}/>`
+    return <img src={item.iconLink} />
   }
 
   getItemSubList (item) {

+ 28 - 18
src/plugins/Informer.js

@@ -1,5 +1,6 @@
-const Plugin = require('./Plugin')
-const html = require('yo-yo')
+const Plugin = require('../core/Plugin')
+
+const { h } = require('preact')
 
 /**
  * Informer
@@ -9,12 +10,11 @@ const html = require('yo-yo')
  *
  */
 module.exports = class Informer extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'progressindicator'
-    this.id = 'Informer'
+    this.id = this.opts.id || 'Informer'
     this.title = 'Informer'
-    // this.timeoutID = undefined
 
     // set default options
     const defaultOptions = {
@@ -45,22 +45,32 @@ module.exports = class Informer extends Plugin {
   }
 
   render (state) {
-    const {isHidden, type, message, details} = state.info
-    const style = `background-color: ${this.opts.typeColors[type].bg}; color: ${this.opts.typeColors[type].text};`
+    const { isHidden, type, message, details } = state.info
+    const style = {
+      backgroundColor: this.opts.typeColors[type].bg,
+      color: this.opts.typeColors[type].text
+    }
 
-    // @TODO add aria-live for screen-readers
-    // maybe details.length < N to set bubble size
-    return html`<div class="Uppy UppyTheme--default UppyInformer" style="${style}" aria-hidden="${isHidden}">
-      <p>
-        ${message} 
-        ${details ? html`<span style="color: ${this.opts.typeColors[type].bg}" data-balloon="${details}" data-balloon-pos="up" data-balloon-length="large">?</span>` : null}
-      </p>
-    </div>`
+    return (
+      <div class="uppy uppy-Informer"
+        style={style}
+        aria-hidden={isHidden}>
+        <p role="alert">
+          {message}
+          {details && <span style={{ color: this.opts.typeColors[type].bg }}
+            data-balloon={details}
+            data-balloon-pos="up"
+            data-balloon-length="large">?</span>
+          }
+        </p>
+      </div>
+    )
   }
 
   install () {
     const target = this.opts.target
-    const plugin = this
-    this.target = this.mount(target, plugin)
+    if (target) {
+      this.mount(target, this)
+    }
   }
 }

+ 25 - 36
src/plugins/Instagram/index.js

@@ -1,30 +1,23 @@
-const html = require('yo-yo')
-const Plugin = require('../Plugin')
-
-const Provider = require('../../Provider')
-
-const View = require('../../generic-provider-views')
+const Plugin = require('../../core/Plugin')
+const Provider = require('../Provider')
+const View = require('../Provider/view')
+const { h } = require('preact')
 
 module.exports = class Instagram extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'acquirer'
-    this.id = 'Instagram'
+    this.id = this.opts.id || 'Instagram'
     this.title = 'Instagram'
-    this.stateId = 'instagram'
-    this.icon = () => html`
+    this.icon = () => (
       <svg aria-hidden="true" class="UppyIcon UppyModalTab-icon" width="28" height="28" viewBox="0 0 512 512">
-        <path
-          d="M256,49.471c67.266,0,75.233.257,101.8,1.469,24.562,1.121,37.9,5.224,46.778,8.674a78.052,78.052,0,0,1,28.966,18.845,78.052,78.052,0,0,1,18.845,28.966c3.45,8.877,7.554,22.216,8.674,46.778,1.212,26.565,1.469,34.532,1.469,101.8s-0.257,75.233-1.469,101.8c-1.121,24.562-5.225,37.9-8.674,46.778a83.427,83.427,0,0,1-47.811,47.811c-8.877,3.45-22.216,7.554-46.778,8.674-26.56,1.212-34.527,1.469-101.8,1.469s-75.237-.257-101.8-1.469c-24.562-1.121-37.9-5.225-46.778-8.674a78.051,78.051,0,0,1-28.966-18.845,78.053,78.053,0,0,1-18.845-28.966c-3.45-8.877-7.554-22.216-8.674-46.778-1.212-26.564-1.469-34.532-1.469-101.8s0.257-75.233,1.469-101.8c1.121-24.562,5.224-37.9,8.674-46.778A78.052,78.052,0,0,1,78.458,78.458a78.053,78.053,0,0,1,28.966-18.845c8.877-3.45,22.216-7.554,46.778-8.674,26.565-1.212,34.532-1.469,101.8-1.469m0-45.391c-68.418,0-77,.29-103.866,1.516-26.815,1.224-45.127,5.482-61.151,11.71a123.488,123.488,0,0,0-44.62,29.057A123.488,123.488,0,0,0,17.3,90.982C11.077,107.007,6.819,125.319,5.6,152.134,4.369,179,4.079,187.582,4.079,256S4.369,333,5.6,359.866c1.224,26.815,5.482,45.127,11.71,61.151a123.489,123.489,0,0,0,29.057,44.62,123.486,123.486,0,0,0,44.62,29.057c16.025,6.228,34.337,10.486,61.151,11.71,26.87,1.226,35.449,1.516,103.866,1.516s77-.29,103.866-1.516c26.815-1.224,45.127-5.482,61.151-11.71a128.817,128.817,0,0,0,73.677-73.677c6.228-16.025,10.486-34.337,11.71-61.151,1.226-26.87,1.516-35.449,1.516-103.866s-0.29-77-1.516-103.866c-1.224-26.815-5.482-45.127-11.71-61.151a123.486,123.486,0,0,0-29.057-44.62A123.487,123.487,0,0,0,421.018,17.3C404.993,11.077,386.681,6.819,359.866,5.6,333,4.369,324.418,4.079,256,4.079h0Z"/>
-        <path
-          d="M256,126.635A129.365,129.365,0,1,0,385.365,256,129.365,129.365,0,0,0,256,126.635Zm0,213.338A83.973,83.973,0,1,1,339.974,256,83.974,83.974,0,0,1,256,339.973Z"/>
-        <circle cx="390.476" cy="121.524" r="30.23"/>
+        <path d="M256,49.471c67.266,0,75.233.257,101.8,1.469,24.562,1.121,37.9,5.224,46.778,8.674a78.052,78.052,0,0,1,28.966,18.845,78.052,78.052,0,0,1,18.845,28.966c3.45,8.877,7.554,22.216,8.674,46.778,1.212,26.565,1.469,34.532,1.469,101.8s-0.257,75.233-1.469,101.8c-1.121,24.562-5.225,37.9-8.674,46.778a83.427,83.427,0,0,1-47.811,47.811c-8.877,3.45-22.216,7.554-46.778,8.674-26.56,1.212-34.527,1.469-101.8,1.469s-75.237-.257-101.8-1.469c-24.562-1.121-37.9-5.225-46.778-8.674a78.051,78.051,0,0,1-28.966-18.845,78.053,78.053,0,0,1-18.845-28.966c-3.45-8.877-7.554-22.216-8.674-46.778-1.212-26.564-1.469-34.532-1.469-101.8s0.257-75.233,1.469-101.8c1.121-24.562,5.224-37.9,8.674-46.778A78.052,78.052,0,0,1,78.458,78.458a78.053,78.053,0,0,1,28.966-18.845c8.877-3.45,22.216-7.554,46.778-8.674,26.565-1.212,34.532-1.469,101.8-1.469m0-45.391c-68.418,0-77,.29-103.866,1.516-26.815,1.224-45.127,5.482-61.151,11.71a123.488,123.488,0,0,0-44.62,29.057A123.488,123.488,0,0,0,17.3,90.982C11.077,107.007,6.819,125.319,5.6,152.134,4.369,179,4.079,187.582,4.079,256S4.369,333,5.6,359.866c1.224,26.815,5.482,45.127,11.71,61.151a123.489,123.489,0,0,0,29.057,44.62,123.486,123.486,0,0,0,44.62,29.057c16.025,6.228,34.337,10.486,61.151,11.71,26.87,1.226,35.449,1.516,103.866,1.516s77-.29,103.866-1.516c26.815-1.224,45.127-5.482,61.151-11.71a128.817,128.817,0,0,0,73.677-73.677c6.228-16.025,10.486-34.337,11.71-61.151,1.226-26.87,1.516-35.449,1.516-103.866s-0.29-77-1.516-103.866c-1.224-26.815-5.482-45.127-11.71-61.151a123.486,123.486,0,0,0-29.057-44.62A123.487,123.487,0,0,0,421.018,17.3C404.993,11.077,386.681,6.819,359.866,5.6,333,4.369,324.418,4.079,256,4.079h0Z" />
+        <path d="M256,126.635A129.365,129.365,0,1,0,385.365,256,129.365,129.365,0,0,0,256,126.635Zm0,213.338A83.973,83.973,0,1,1,339.974,256,83.974,83.974,0,0,1,256,339.973Z" />
+        <circle cx="390.476" cy="121.524" r="30.23" />
       </svg>
-    `
+    )
 
-    // writing out the key explicitly for readability the key used to store
-    // the provider instance must be equal to this.id.
-    this.Instagram = new Provider(core, {
+    this[this.id] = new Provider(uppy, {
       host: this.opts.host,
       provider: 'instagram',
       authProvider: 'instagram'
@@ -33,7 +26,6 @@ module.exports = class Instagram extends Plugin {
     this.files = []
 
     this.onAuth = this.onAuth.bind(this)
-    // Visual
     this.render = this.render.bind(this)
 
     // set default options
@@ -48,18 +40,14 @@ module.exports = class Instagram extends Plugin {
       viewType: 'grid'
     })
     // Set default state for Google Drive
-    this.core.setState({
-      // writing out the key explicitly for readability the key used to store
-      // the plugin state must be equal to this.stateId.
-      instagram: {
-        authenticated: false,
-        files: [],
-        folders: [],
-        directories: [],
-        activeRow: -1,
-        filterInput: '',
-        isSearchVisible: false
-      }
+    this.setPluginState({
+      authenticated: false,
+      files: [],
+      folders: [],
+      directories: [],
+      activeRow: -1,
+      filterInput: '',
+      isSearchVisible: false
     })
 
     const target = this.opts.target
@@ -69,11 +57,12 @@ module.exports = class Instagram extends Plugin {
   }
 
   uninstall () {
+    this.view.tearDown()
     this.unmount()
   }
 
   onAuth (authenticated) {
-    this.view.updateState({authenticated})
+    this.setPluginState({ authenticated })
     if (authenticated) {
       this.view.getFolder('recent')
     }
@@ -88,7 +77,7 @@ module.exports = class Instagram extends Plugin {
   }
 
   getItemIcon (item) {
-    return html`<img width="100px" src=${item.images.thumbnail.url}/>`
+    return <img width="100" src={item.images.thumbnail.url} />
   }
 
   getItemSubList (item) {
@@ -134,7 +123,7 @@ module.exports = class Instagram extends Plugin {
   }
 
   getNextPagePath () {
-    const { files } = this.core.getState()[this.stateId]
+    const { files } = this.getPluginState()
     return `recent?max_id=${this.getItemId(files[files.length - 1])}`
   }
 

+ 5 - 6
src/plugins/MagicLog.js

@@ -1,4 +1,4 @@
-const Plugin = require('./Plugin')
+const Plugin = require('../core/Plugin')
 // import deepDiff from 'deep-diff'
 
 /**
@@ -8,8 +8,8 @@ const Plugin = require('./Plugin')
  *
  */
 module.exports = class MagicLog extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'debugger'
     this.id = 'MagicLog'
     this.title = 'Magic Log'
@@ -32,11 +32,10 @@ module.exports = class MagicLog extends Plugin {
   }
 
   install () {
-    const uppy = this.core.emitter
-    uppy.on('state-update', this.handleStateUpdate)
+    this.uppy.on('state-update', this.handleStateUpdate)
   }
 
   uninstall () {
-    this.core.emitter.off('state-update', this.handleStateUpdate)
+    this.uppy.off('state-update', this.handleStateUpdate)
   }
 }

+ 0 - 51
src/plugins/MetaData.js

@@ -1,51 +0,0 @@
-const Plugin = require('./Plugin')
-
-/**
- * Meta Data
- * Adds metadata fields to Uppy
- *
- */
-module.exports = class MetaData extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
-    this.type = 'modifier'
-    this.id = 'MetaData'
-    this.title = 'Meta Data'
-
-    // set default options
-    const defaultOptions = {}
-
-    // merge default options with the ones set by user
-    this.opts = Object.assign({}, defaultOptions, opts)
-
-    this.handleFileAdded = this.handleFileAdded.bind(this)
-  }
-
-  handleFileAdded (file) {
-    const metaFields = this.opts.fields
-
-    metaFields.forEach((item) => {
-      const obj = {}
-      obj[item.id] = item.value
-      this.core.updateMeta(obj, file.id)
-    })
-  }
-
-  addInitialMeta () {
-    const metaFields = this.opts.fields
-
-    this.core.setState({
-      metaFields: metaFields
-    })
-
-    this.core.on('core:file-added', this.handleFileAdded)
-  }
-
-  install () {
-    this.addInitialMeta()
-  }
-
-  uninstall () {
-    this.core.off('core:file-added', this.handleFileAdded)
-  }
-}

+ 0 - 376
src/plugins/Plugin.test.js

@@ -1,376 +0,0 @@
-const getFormData = require('get-form-data')
-const nanoraf = require('nanoraf')
-const yo = require('yo-yo')
-
-const { findDOMElement } = require('../core/Utils')
-const Plugin = require('./Plugin')
-
-jest.mock('get-form-data')
-jest.mock('nanoraf')
-jest.mock('../core/Utils', () => ({
-  findDOMElement: jest.fn()
-}))
-
-getFormData.mockImplementation(() => ({ foo: 'bar' }))
-nanoraf.mockImplementation(cb => {
-  cb({ some: 'state' }) // eslint-disable-line standard/no-callback-literal
-  return () => {}
-})
-
-describe('Plugin', () => {
-  let plugin
-
-  afterEach(() => {
-    getFormData.mockClear()
-  })
-
-  it('is a class', () => {
-    expect(typeof Plugin).toBe('function')
-  })
-
-  it('accepts two parameters', () => {
-    expect(Plugin.length).toBe(2)
-  })
-
-  it('defaults options when not passed as an argument', () => {
-    plugin = new Plugin()
-    expect(typeof plugin.opts).toBe('object')
-  })
-
-  describe('plugin state', () => {
-    class MockPlugin extends Plugin {
-      constructor (core, opts) {
-        super(core, opts)
-        this.id = 'MockPlugin'
-      }
-    }
-    it('returns plugin state from `getPluginState()`', () => {
-      const mockState = {}
-      plugin = new MockPlugin({
-        state: {
-          plugins: {
-            MockPlugin: mockState
-          }
-        }
-      })
-
-      expect(plugin.getPluginState()).toBe(mockState)
-    })
-
-    it('merges plugin state using `setPluginState()`', () => {
-      const initialState = {
-        plugins: {
-          MockPlugin: {
-            hello: 'world',
-            asdf: 'quux'
-          }
-        }
-      }
-
-      plugin = new MockPlugin({
-        setState (patch) {
-          this.state = Object.assign({}, this.state, patch)
-        },
-        state: initialState
-      })
-
-      plugin.setPluginState({ hello: 'friends' })
-
-      expect(plugin.core.state).not.toBe(initialState)
-      expect(plugin.getPluginState()).toEqual({
-        hello: 'friends',
-        asdf: 'quux'
-      })
-    })
-  })
-
-  // it('sets `replaceTargetContent` based on options argument', () => {
-  //   plugin = new Plugin(null, { replaceTargetContent: false })
-  //   expect(plugin.opts.replaceTargetContent).toBe(false)
-  // })
-
-  // it('defaults `replaceTargetContent` to true when not passed as an option', () => {
-  //   plugin = new Plugin()
-  //   expect(plugin.opts.replaceTargetContent).toBe(true)
-  // })
-
-  describe('.update', () => {
-    beforeEach(() => {
-      plugin = new Plugin()
-      plugin.render = jest.fn(() => ({ ren: 'der' }))
-    })
-
-    it('is a function', () => {
-      expect(typeof Plugin.prototype.update).toBe('function')
-    })
-
-    it('accepts one parameter', () => {
-      expect(Plugin.prototype.update.length).toBe(1)
-    })
-
-    it('does nothing when plugin has no UI element (`el`)', () => {
-      plugin.updateUI = jest.fn()
-      expect(plugin.update()).toBe(undefined)
-      expect(plugin.updateUI.mock.calls.length).toBe(0)
-    })
-
-    it('calls updateUI method with state when UI element (`el`) exists', () => {
-      plugin.el = {}
-      plugin.updateUI = jest.fn()
-      plugin.update({ foo: 'bar' })
-      expect(plugin.updateUI.mock.calls.length).toBe(1)
-      expect(plugin.updateUI.mock.calls[0][0]).toEqual({ foo: 'bar' })
-    })
-
-    it('does nothing when a UI element exists but and no updateUI method', () => {
-      plugin.el = {}
-      expect(() => plugin.update()).not.toThrow()
-    })
-  })
-
-  describe('.mount', () => {
-    const addTarget = jest.fn(() => 'body')
-    const mockCore = {
-      iteratePlugins: (cb) => {
-        cb(new mockTarget()) // eslint-disable-line new-cap
-      },
-      log: jest.fn(),
-      setMeta: jest.fn(),
-      state: 'default'
-    }
-    const mockPlugin = {
-      id: 'pID'
-    }
-    const mockTarget = function () {
-      this.id = 'tID'
-      this.addTarget = addTarget
-    }
-
-    let yoUpdateSpy
-
-    beforeEach(() => {
-      yoUpdateSpy = jest.spyOn(yo, 'update').mockImplementation(() => ({ yo: 'el' }))
-      plugin = new Plugin(mockCore, { getMetaFromForm: true })
-      plugin.render = jest.fn(() => ({ ren: 'der' }))
-    })
-
-    afterEach(() => {
-      findDOMElement.mockReset()
-      findDOMElement.mockRestore()
-      mockCore.log.mockReset()
-      mockCore.setMeta.mockReset()
-      yoUpdateSpy.mockReset()
-      yoUpdateSpy.mockRestore()
-    })
-
-    it('is a function', () => {
-      expect(typeof Plugin.prototype.mount).toBe('function')
-    })
-
-    it('accepts two parameters', () => {
-      expect(Plugin.prototype.mount.length).toBe(2)
-    })
-
-    it('adds updateUI method', () => {
-      plugin.mount(mockTarget, mockPlugin)
-      expect(typeof plugin.updateUI).toBe('function')
-    })
-
-    it('sets `el` property when state has changed', () => {
-      expect.assertions(4)
-
-      expect(plugin.el).toBe(undefined)
-
-      plugin.mount(mockTarget, mockPlugin)
-
-      expect(plugin.render.mock.calls[0][0]).toEqual({ some: 'state' })
-      expect(yo.update.mock.calls[0]).toEqual([undefined, { ren: 'der' }])
-      expect(plugin.el).toEqual({ yo: 'el' })
-    })
-
-    describe('when target is a DOM element', () => {
-      let mockElement
-      const appendChild = jest.fn()
-
-      beforeEach(() => {
-        mockElement = {
-          nodeName: 'FORM',
-          innerHTML: 'foo',
-          appendChild
-        }
-        mockPlugin.render = jest.fn(() => ({ el: 'lo' }))
-        findDOMElement.mockImplementation(() => mockElement)
-      })
-
-      afterEach(() => {
-        findDOMElement.mockReset()
-        findDOMElement.mockRestore()
-      })
-
-      it('logs installation', () => {
-        plugin.mount(mockTarget, mockPlugin)
-        expect(mockCore.log.mock.calls.length).toBe(1)
-        expect(/DOM element/.test(mockCore.log.mock.calls[0][0])).toBe(true)
-      })
-
-      it('sets form data to core\'s meta data when target is a form', () => {
-        plugin.mount(mockTarget, mockPlugin)
-        expect(getFormData.mock.calls[0][0]).toEqual(mockElement)
-        expect(mockCore.setMeta.mock.calls[0][0]).toEqual({ foo: 'bar' })
-      })
-
-      it('does not set data to core\'s meta data when `getMetaFromForm` isn\'t a Plugin option', () => {
-        plugin = new Plugin(mockCore)
-        plugin.render = () => {}
-        plugin.mount(mockTarget, mockPlugin)
-        expect(mockCore.setMeta.mock.calls.length).toBe(0)
-      })
-
-      it('does not set data to core\'s meta data when target is not a form', () => {
-        mockElement.nodeName = 'FOO'
-
-        plugin.mount(mockTarget, mockPlugin)
-        expect(mockCore.setMeta.mock.calls.length).toBe(0)
-      })
-
-      it('does not remove content from target when `replaceTargetContent` is not set', () => {
-        plugin = new Plugin(mockCore)
-        plugin.render = () => {}
-        plugin.mount(mockTarget, mockPlugin)
-        expect(mockElement.innerHTML).toBe('foo')
-      })
-
-      it('removes content from target when `replaceTargetContent` is set', () => {
-        plugin = new Plugin(mockCore, {replaceTargetContent: true})
-        plugin.render = () => {}
-        plugin.mount(mockTarget, mockPlugin)
-        expect(mockElement.innerHTML).toBe('')
-      })
-
-      it('sets `el` to plugin rendered with state', () => {
-        plugin.mount(mockTarget, mockPlugin)
-        expect(mockPlugin.render.mock.calls[0][0]).toBe('default')
-        expect(plugin.el).toEqual({ el: 'lo' })
-      })
-
-      it('appends plugin\'s element to target', () => {
-        plugin.mount(mockTarget, mockPlugin)
-        expect(mockElement.appendChild.mock.calls[0][0]).toEqual({ el: 'lo' })
-      })
-
-      it('returns the target DOM element', () => {
-        plugin = new Plugin(mockCore, {replaceTargetContent: true})
-        plugin.render = () => {}
-        const target = plugin.mount(mockTarget, mockPlugin)
-        expect(target).toEqual({
-          nodeName: 'FORM',
-          innerHTML: '',
-          appendChild
-        })
-      })
-    })
-
-    describe('when target is a plugin', () => {
-      it('logs installation', () => {
-        plugin.mount(mockTarget, mockPlugin)
-        expect(mockCore.log.mock.calls.length).toBe(1)
-        expect(/tID/.test(mockCore.log.mock.calls[0][0])).toBe(true)
-      })
-
-      it('adds plugin to target', () => {
-        plugin.mount(mockTarget, mockPlugin)
-        expect(addTarget.mock.calls[0][0]).toEqual(mockPlugin)
-      })
-
-      it('returns plugin\'s target', () => {
-        const target = plugin.mount(mockTarget, mockPlugin)
-        expect(target).toBe('body')
-      })
-    })
-  })
-
-  describe('.render', () => {
-    beforeEach(() => {
-      plugin = new Plugin()
-    })
-
-    it('is a function', () => {
-      expect(typeof Plugin.prototype.render).toBe('function')
-    })
-
-    it('accepts one parameter', () => {
-      expect(Plugin.prototype.render.length).toBe(1)
-    })
-
-    it('throws by default', () => {
-      expect(() => plugin.render()).toThrow()
-    })
-  })
-
-  describe('.addTarget', () => {
-    beforeEach(() => {
-      plugin = new Plugin()
-    })
-
-    it('is a function', () => {
-      expect(typeof Plugin.prototype.addTarget).toBe('function')
-    })
-
-    it('accepts one parameter', () => {
-      expect(Plugin.prototype.addTarget.length).toBe(1)
-    })
-
-    it('throws by default', () => {
-      expect(() => plugin.addTarget()).toThrow()
-    })
-  })
-
-  describe('.unmount', () => {
-    beforeEach(() => {
-      plugin = new Plugin()
-    })
-
-    it('is a function', () => {
-      expect(typeof Plugin.prototype.unmount).toBe('function')
-    })
-
-    it('removes plugin\'s UI element', () => {
-      const removeChild = jest.fn()
-      const el = {
-        parentNode: {
-          removeChild
-        }
-      }
-      plugin.el = el
-      plugin.unmount()
-      expect(removeChild.mock.calls.length).toBe(1)
-      expect(removeChild.mock.calls[0][0]).toEqual(el)
-    })
-
-    it('does nothing when no UI element or parent', () => {
-      plugin.el = {}
-      expect(() => plugin.unmount()).not.toThrow()
-    })
-  })
-
-  describe('.install', () => {
-    it('is a function', () => {
-      expect(typeof Plugin.prototype.install).toBe('function')
-    })
-  })
-
-  describe('.uninstall', () => {
-    it('is a function', () => {
-      expect(typeof Plugin.prototype.uninstall).toBe('function')
-    })
-
-    it('calls unmount method', () => {
-      const spy = jest.spyOn(Plugin.prototype, 'unmount')
-      const plugin = new Plugin()
-      plugin.uninstall()
-      expect(spy.mock.calls.length).toBe(1)
-      spy.mockReset()
-      spy.mockRestore()
-    })
-  })
-})

+ 10 - 11
src/plugins/ProgressBar.js

@@ -1,14 +1,14 @@
-const Plugin = require('./Plugin')
-const html = require('yo-yo')
+const Plugin = require('../core/Plugin')
+const { h } = require('preact')
 
 /**
  * Progress bar
  *
  */
 module.exports = class ProgressBar extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
-    this.id = 'ProgressBar'
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = this.opts.id || 'ProgressBar'
     this.title = 'Progress Bar'
     this.type = 'progressindicator'
 
@@ -28,17 +28,16 @@ module.exports = class ProgressBar extends Plugin {
   render (state) {
     const progress = state.totalProgress || 0
 
-    return html`<div class="UppyProgressBar" style="${this.opts.fixed ? 'position: fixed' : 'null'}">
-      <div class="UppyProgressBar-inner" style="width: ${progress}%"></div>
-      <div class="UppyProgressBar-percentage">${progress}</div>
-    </div>`
+    return <div class="uppy uppy-ProgressBar" style={{ position: this.opts.fixed ? 'fixed' : 'initial' }}>
+      <div class="uppy-ProgressBar-inner" style={{ width: progress + '%' }} />
+      <div class="uppy-ProgressBar-percentage">{progress}</div>
+    </div>
   }
 
   install () {
     const target = this.opts.target
-    const plugin = this
     if (target) {
-      this.mount(target, plugin)
+      this.mount(target, this)
     }
   }
 

+ 5 - 5
src/Provider.js → src/plugins/Provider/index.js

@@ -7,8 +7,8 @@ const _getName = (id) => {
 }
 
 module.exports = class Provider {
-  constructor (core, opts) {
-    this.core = core
+  constructor (uppy, opts) {
+    this.uppy = uppy
     this.opts = opts
     this.provider = opts.provider
     this.id = this.provider
@@ -19,18 +19,18 @@ module.exports = class Provider {
   }
 
   get hostname () {
-    const uppyServer = this.core.state.uppyServer || {}
+    const uppyServer = this.uppy.state.uppyServer || {}
     const host = this.opts.host
     return uppyServer[host] || host
   }
 
   onReceiveResponse (response) {
-    const uppyServer = this.core.state.uppyServer || {}
+    const uppyServer = this.uppy.state.uppyServer || {}
     const host = this.opts.host
     const headers = response.headers
     // Store the self-identified domain name for the uppy-server we just hit.
     if (headers.has('i-am') && headers.get('i-am') !== uppyServer[host]) {
-      this.core.setState({
+      this.uppy.setState({
         uppyServer: Object.assign({}, uppyServer, {
           [host]: headers.get('i-am')
         })

+ 31 - 0
src/plugins/Provider/view/AuthView.js

@@ -0,0 +1,31 @@
+const LoaderView = require('./Loader')
+const { h, Component } = require('preact')
+
+class AuthView extends Component {
+  componentDidMount () {
+    this.props.checkAuth()
+  }
+
+  render () {
+    const AuthBlock = () => {
+      return <div class="uppy-Provider-auth">
+        <h1 class="uppy-Provider-authTitle">Please authenticate with <span class="uppy-Provider-authTitleName">{this.props.pluginName}</span><br /> to select files</h1>
+        <button type="button" class="uppy-Provider-authBtn" onclick={this.props.handleAuth}>Connect to {this.props.pluginName}</button>
+        {this.props.demo &&
+          <button class="uppy-Provider-authBtnDemo" onclick={this.props.handleDemoAuth}>Proceed with Demo Account</button>
+        }
+      </div>
+    }
+
+    return (
+      <div style="height: 100%;">
+        {this.props.checkAuthInProgress
+          ? LoaderView()
+          : AuthBlock()
+        }
+      </div>
+    )
+  }
+}
+
+module.exports = AuthView

+ 5 - 0
src/plugins/Provider/view/Breadcrumb.js

@@ -0,0 +1,5 @@
+const { h } = require('preact')
+
+module.exports = (props) => {
+  return <li><button type="button" onclick={props.getFolder}>{props.title}</button></li>
+}

+ 5 - 5
src/generic-provider-views/Breadcrumbs.js → src/plugins/Provider/view/Breadcrumbs.js

@@ -1,10 +1,10 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 const Breadcrumb = require('./Breadcrumb')
 
 module.exports = (props) => {
-  return html`
-    <ul class="UppyProvider-breadcrumbs">
-      ${
+  return (
+    <ul class="uppy-Provider-breadcrumbs">
+      {
         props.directories.map((directory, i) => {
           return Breadcrumb({
             getFolder: () => props.getFolder(directory.id),
@@ -13,5 +13,5 @@ module.exports = (props) => {
         })
       }
     </ul>
-  `
+  )
 }

+ 29 - 23
src/generic-provider-views/Browser.js → src/plugins/Provider/view/Browser.js

@@ -1,6 +1,7 @@
-const html = require('yo-yo')
 const Breadcrumbs = require('./Breadcrumbs')
+const Filter = require('./Filter')
 const Table = require('./Table')
+const { h } = require('preact')
 
 module.exports = (props) => {
   let filteredFolders = props.folders
@@ -11,36 +12,29 @@ module.exports = (props) => {
     filteredFiles = props.filterItems(props.files)
   }
 
-  return html`
-    <div class="Browser Browser-viewType--${props.viewType}">
-      <header class="Browser-header">
-        <div class="Browser-search" aria-hidden="${!props.isSearchVisible}">
-          <input type="text" class="Browser-searchInput" placeholder="Search"
-                 onkeyup=${props.filterQuery} value="${props.filterInput}"/>
-          <button type="button" class="Browser-searchClose" 
-                  onclick=${props.toggleSearch}>
-            <svg class="UppyIcon" viewBox="0 0 19 19">
-              <path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z"/>
-            </svg>
-          </button>
+  return (
+    <div class={`uppy uppy-ProviderBrowser uppy-ProviderBrowser-viewType--${props.viewType}`}>
+      <header class="uppy-ProviderBrowser-header">
+        <div class="uppy-ProviderBrowser-search" aria-hidden={!props.isSearchVisible}>
+          { props.isSearchVisible && <Filter {...props} /> }
         </div>
-        <div class="Browser-headerBar">
-          <button type="button" class="Browser-searchToggle"
-                  onclick=${props.toggleSearch}>
+        <div class="uppy-ProviderBrowser-headerBar">
+          <button type="button" class="uppy-ProviderBrowser-searchToggle"
+            onclick={props.toggleSearch}>
             <svg class="UppyIcon" viewBox="0 0 100 100">
-              <path d="M87.533 80.03L62.942 55.439c3.324-4.587 5.312-10.207 5.312-16.295 0-.312-.043-.611-.092-.908.05-.301.093-.605.093-.922 0-15.36-12.497-27.857-27.857-27.857-.273 0-.536.043-.799.08-.265-.037-.526-.08-.799-.08-15.361 0-27.858 12.497-27.858 27.857 0 .312.042.611.092.909a5.466 5.466 0 0 0-.093.921c0 15.36 12.496 27.858 27.857 27.858.273 0 .535-.043.8-.081.263.038.524.081.798.081 5.208 0 10.071-1.464 14.245-3.963L79.582 87.98a5.603 5.603 0 0 0 3.976 1.647 5.621 5.621 0 0 0 3.975-9.597zM39.598 55.838c-.265-.038-.526-.081-.8-.081-9.16 0-16.612-7.452-16.612-16.612 0-.312-.042-.611-.092-.908.051-.301.093-.605.093-.922 0-9.16 7.453-16.612 16.613-16.612.272 0 .534-.042.799-.079.263.037.525.079.799.079 9.16 0 16.612 7.452 16.612 16.612 0 .312.043.611.092.909-.05.301-.094.604-.094.921 0 9.16-7.452 16.612-16.612 16.612-.274 0-.536.043-.798.081z"/>
+              <path d="M87.533 80.03L62.942 55.439c3.324-4.587 5.312-10.207 5.312-16.295 0-.312-.043-.611-.092-.908.05-.301.093-.605.093-.922 0-15.36-12.497-27.857-27.857-27.857-.273 0-.536.043-.799.08-.265-.037-.526-.08-.799-.08-15.361 0-27.858 12.497-27.858 27.857 0 .312.042.611.092.909a5.466 5.466 0 0 0-.093.921c0 15.36 12.496 27.858 27.857 27.858.273 0 .535-.043.8-.081.263.038.524.081.798.081 5.208 0 10.071-1.464 14.245-3.963L79.582 87.98a5.603 5.603 0 0 0 3.976 1.647 5.621 5.621 0 0 0 3.975-9.597zM39.598 55.838c-.265-.038-.526-.081-.8-.081-9.16 0-16.612-7.452-16.612-16.612 0-.312-.042-.611-.092-.908.051-.301.093-.605.093-.922 0-9.16 7.453-16.612 16.613-16.612.272 0 .534-.042.799-.079.263.037.525.079.799.079 9.16 0 16.612 7.452 16.612 16.612 0 .312.043.611.092.909-.05.301-.094.604-.094.921 0 9.16-7.452 16.612-16.612 16.612-.274 0-.536.043-.798.081z" />
             </svg>
           </button>
-          ${Breadcrumbs({
+          {Breadcrumbs({
             getFolder: props.getFolder,
             directories: props.directories,
             title: props.title
           })}
-          <button type="button" onclick=${props.logout} class="Browser-userLogout">Log out</button>
+          <button type="button" onclick={props.logout} class="uppy-ProviderBrowser-userLogout">Log out</button>
         </div>
       </header>
-      <div class="Browser-body">
-        ${Table({
+      <div class="uppy-ProviderBrowser-body">
+        {Table({
           columns: [{
             name: 'Name',
             key: 'title'
@@ -52,11 +46,23 @@ module.exports = (props) => {
           sortByDate: props.sortByDate,
           handleFileClick: props.addFile,
           handleFolderClick: props.getNextFolder,
+          isChecked: props.isChecked,
+          toggleCheckbox: props.toggleCheckbox,
           getItemName: props.getItemName,
           getItemIcon: props.getItemIcon,
-          handleScroll: props.handleScroll
+          handleScroll: props.handleScroll,
+          title: props.title
         })}
       </div>
+      <button class="UppyButton--circular UppyButton--blue uppy-ProviderBrowser-doneBtn"
+        type="button"
+        aria-label="Done picking files"
+        title="Done picking files"
+        onclick={props.done}>
+        <svg aria-hidden="true" class="UppyIcon" width="13px" height="9px" viewBox="0 0 13 9">
+          <polygon points="5 7.293 1.354 3.647 0.646 4.354 5 8.707 12.354 1.354 11.646 0.647" />
+        </svg>
+      </button>
     </div>
-  `
+  )
 }

+ 44 - 0
src/plugins/Provider/view/Filter.js

@@ -0,0 +1,44 @@
+const { h, Component } = require('preact')
+
+module.exports = class Filter extends Component {
+  constructor (props) {
+    super(props)
+
+    this.handleKeyPress = this.handleKeyPress.bind(this)
+  }
+
+  componentDidMount () {
+    this.input.focus()
+  }
+
+  handleKeyPress (ev) {
+    if (ev.keyCode === 13) {
+      ev.stopPropagation()
+      ev.preventDefault()
+      return
+    }
+    this.props.filterQuery(ev)
+  }
+
+  render () {
+    return <div style={{ display: 'flex', width: '100%' }}>
+      <input
+        class="uppy-ProviderBrowser-searchInput"
+        type="text"
+        placeholder="Search"
+        onkeyup={this.handleKeyPress}
+        onkeydown={this.handleKeyPress}
+        onkeypress={this.handleKeyPress}
+        value={this.props.filterInput}
+        ref={(input) => { this.input = input }} />
+      <button
+        class="uppy-ProviderBrowser-searchClose"
+        type="button"
+        onclick={this.props.toggleSearch}>
+        <svg class="UppyIcon" viewBox="0 0 19 19">
+          <path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
+        </svg>
+      </button>
+    </div>
+  }
+}

+ 7 - 0
src/plugins/Provider/view/Loader.js

@@ -0,0 +1,7 @@
+const { h } = require('preact')
+
+module.exports = (props) => {
+  return <div class="uppy-Provider-loading">
+    <span>Loading...</span>
+  </div>
+}

+ 54 - 0
src/plugins/Provider/view/Table.js

@@ -0,0 +1,54 @@
+const Row = require('./TableRow')
+const { h } = require('preact')
+
+module.exports = (props) => {
+  // const headers = props.columns.map((column) => {
+  //   return html`
+  //     <th class="uppy-ProviderBrowserTable-headerColumn uppy-ProviderBrowserTable-column" onclick=${props.sortByTitle}>
+  //       ${column.name}
+  //     </th>
+  //   `
+  // })
+
+  // <thead class="uppy-ProviderBrowserTable-header">
+  //   <tr>${headers}</tr>
+  // </thead>
+
+  return (
+    <table class="uppy-ProviderBrowserTable" onscroll={props.handleScroll}>
+      <tbody role="listbox" aria-label={`List of files from ${props.title}`}>
+        {props.folders.map(folder => {
+          let isDisabled = false
+          let isChecked = props.isChecked(folder)
+          if (isChecked) {
+            isDisabled = isChecked.loading
+          }
+          return Row({
+            title: props.getItemName(folder),
+            type: 'folder',
+            // active: props.activeRow(folder),
+            getItemIcon: () => props.getItemIcon(folder),
+            handleClick: () => props.handleFolderClick(folder),
+            isDisabled: isDisabled,
+            isChecked: isChecked,
+            handleCheckboxClick: (e) => props.toggleCheckbox(e, folder),
+            columns: props.columns
+          })
+        })}
+        {props.files.map(file => {
+          return Row({
+            title: props.getItemName(file),
+            type: 'file',
+            // active: props.activeRow(file),
+            getItemIcon: () => props.getItemIcon(file),
+            handleClick: () => props.handleFileClick(file),
+            isDisabled: false,
+            isChecked: props.isChecked(file),
+            handleCheckboxClick: (e) => props.toggleCheckbox(e, file),
+            columns: props.columns
+          })
+        })}
+      </tbody>
+    </table>
+  )
+}

+ 46 - 0
src/plugins/Provider/view/TableRow.js

@@ -0,0 +1,46 @@
+const cuid = require('cuid')
+const { h } = require('preact')
+
+module.exports = (props) => {
+  const uniqueId = cuid()
+
+  const stop = (ev) => {
+    if (ev.keyCode === 13) {
+      ev.stopPropagation()
+      ev.preventDefault()
+    }
+  }
+
+  const handleItemClick = (ev) => {
+    ev.preventDefault()
+    // when file is clicked, select it, but when folder is clicked, open it
+    if (props.type === 'folder') {
+      return props.handleClick(ev)
+    }
+    props.handleCheckboxClick(ev)
+  }
+
+  return (
+    <tr class="uppy-ProviderBrowserTable-row">
+      <td class="uppy-ProviderBrowserTable-column">
+        <div class="uppy-ProviderBrowserTable-checkbox">
+          <input type="checkbox"
+            role="option"
+            tabindex="0"
+            aria-label={`Select ${props.title}`}
+            id={uniqueId}
+            checked={props.isChecked}
+            disabled={props.isDisabled}
+            onchange={props.handleCheckboxClick}
+            onkeyup={stop}
+            onkeydown={stop}
+            onkeypress={stop} />
+          <label for={uniqueId} onclick={handleItemClick} />
+        </div>
+        <button type="button" class="uppy-ProviderBrowserTable-item" aria-label={`Select ${props.title}`} tabindex="0" onclick={handleItemClick}>
+          {props.getItemIcon()} {props.title}
+        </button>
+      </td>
+    </tr>
+  )
+}

+ 230 - 55
src/generic-provider-views/index.js → src/plugins/Provider/view/index.js

@@ -1,16 +1,14 @@
 const AuthView = require('./AuthView')
 const Browser = require('./Browser')
 const LoaderView = require('./Loader')
-const Utils = require('../core/Utils')
+const Utils = require('../../../core/Utils')
+const { h } = require('preact')
 
 /**
  * Class to easily generate generic views for plugins
  *
- * This class expects the plugin using to have the following attributes
  *
- * stateId {String} object key of which the plugin state is stored
- *
- * This class also expects the plugin instance using it to have the following
+ * This class expects the plugin instance using it to have the following
  * accessor methods.
  * Each method takes the item whose property is to be accessed
  * as a param
@@ -53,6 +51,7 @@ module.exports = class View {
     this.opts = Object.assign({}, defaultOptions, opts)
 
     // Logic
+    this.updateFolderState = this.updateFolderState.bind(this)
     this.addFile = this.addFile.bind(this)
     this.filterItems = this.filterItems.bind(this)
     this.filterQuery = this.filterQuery.bind(this)
@@ -66,21 +65,20 @@ module.exports = class View {
     this.sortByTitle = this.sortByTitle.bind(this)
     this.sortByDate = this.sortByDate.bind(this)
     this.isActiveRow = this.isActiveRow.bind(this)
+    this.isChecked = this.isChecked.bind(this)
+    this.toggleCheckbox = this.toggleCheckbox.bind(this)
     this.handleError = this.handleError.bind(this)
     this.handleScroll = this.handleScroll.bind(this)
+    this.donePicking = this.donePicking.bind(this)
+
+    this.plugin.uppy.on('file-removed', this.updateFolderState)
 
     // Visual
     this.render = this.render.bind(this)
   }
 
-  /**
-   * Little shorthand to update the state with the plugin's state
-   */
-  updateState (newState) {
-    let stateId = this.plugin.stateId
-    const {state} = this.plugin.core
-
-    this.plugin.core.setState({[stateId]: Object.assign({}, state[stateId], newState)})
+  tearDown () {
+    this.plugin.uppy.off('file-removed', this.updateFolderState)
   }
 
   _updateFilesAndFolders (res, files, folders) {
@@ -92,18 +90,18 @@ module.exports = class View {
       }
     })
 
-    this.updateState({ folders, files })
+    this.plugin.setPluginState({ folders, files })
   }
 
   checkAuth () {
-    this.updateState({ checkAuthInProgress: true })
+    this.plugin.setPluginState({ checkAuthInProgress: true })
     this.Provider.checkAuth()
       .then((authenticated) => {
-        this.updateState({ checkAuthInProgress: false })
+        this.plugin.setPluginState({ checkAuthInProgress: false })
         this.plugin.onAuth(authenticated)
       })
       .catch((err) => {
-        this.updateState({ checkAuthInProgress: false })
+        this.plugin.setPluginState({ checkAuthInProgress: false })
         this.handleError(err)
       })
   }
@@ -121,7 +119,7 @@ module.exports = class View {
         let files = []
         let updatedDirectories
 
-        const state = this.plugin.core.getState()[this.plugin.stateId]
+        const state = this.plugin.getPluginState()
         const index = state.directories.findIndex((dir) => id === dir.id)
 
         if (index !== -1) {
@@ -131,7 +129,7 @@ module.exports = class View {
         }
 
         this._updateFilesAndFolders(res, files, folders)
-        this.updateState({ directories: updatedDirectories })
+        this.plugin.setPluginState({ directories: updatedDirectories })
       },
       this.handleError)
   }
@@ -144,9 +142,10 @@ module.exports = class View {
   getNextFolder (folder) {
     let id = this.plugin.getItemRequestPath(folder)
     this.getFolder(id, this.plugin.getItemName(folder))
+    this.lastCheckbox = undefined
   }
 
-  addFile (file) {
+  addFile (file, isCheckbox = false) {
     const tagFile = {
       source: this.plugin.id,
       data: this.plugin.getItemData(file),
@@ -166,11 +165,14 @@ module.exports = class View {
     }
 
     Utils.getFileType(tagFile).then(fileType => {
-      if (Utils.isPreviewSupported(fileType[1])) {
+      if (fileType && Utils.isPreviewSupported(fileType)) {
         tagFile.preview = this.plugin.getItemThumbnailUrl(file)
       }
-      this.plugin.core.log('Adding remote file')
-      this.plugin.core.addFile(tagFile)
+      this.plugin.uppy.log('Adding remote file')
+      this.plugin.uppy.addFile(tagFile)
+      if (!isCheckbox) {
+        this.donePicking()
+      }
     })
   }
 
@@ -188,42 +190,42 @@ module.exports = class View {
             folders: [],
             directories: []
           }
-          this.updateState(newState)
+          this.plugin.setPluginState(newState)
         }
       }).catch(this.handleError)
   }
 
   filterQuery (e) {
-    const state = this.plugin.core.getState()[this.plugin.stateId]
-    this.updateState(Object.assign({}, state, {
+    const state = this.plugin.getPluginState()
+    this.plugin.setPluginState(Object.assign({}, state, {
       filterInput: e.target.value
     }))
   }
 
-  toggleSearch () {
-    const state = this.plugin.core.getState()[this.plugin.stateId]
-    const searchInputEl = document.querySelector('.Browser-searchInput')
+  toggleSearch (inputEl) {
+    const state = this.plugin.getPluginState()
+    // const searchInputEl = document.querySelector('.Browser-searchInput')
 
-    this.updateState(Object.assign({}, state, {
+    this.plugin.setPluginState({
       isSearchVisible: !state.isSearchVisible,
       filterInput: ''
-    }))
+    })
 
-    searchInputEl.value = ''
-    if (!state.isSearchVisible) {
-      searchInputEl.focus()
-    }
+    // searchInputEl.value = ''
+    // if (!state.isSearchVisible) {
+    //   searchInputEl.focus()
+    // }
   }
 
   filterItems (items) {
-    const state = this.plugin.core.getState()[this.plugin.stateId]
+    const state = this.plugin.getPluginState()
     return items.filter((folder) => {
       return this.plugin.getItemName(folder).toLowerCase().indexOf(state.filterInput.toLowerCase()) !== -1
     })
   }
 
   sortByTitle () {
-    const state = Object.assign({}, this.plugin.core.getState()[this.plugin.stateId])
+    const state = Object.assign({}, this.plugin.getPluginState())
     const {files, folders, sorting} = state
 
     let sortedFiles = files.sort((fileA, fileB) => {
@@ -240,7 +242,7 @@ module.exports = class View {
       return this.plugin.getItemName(folderA).localeCompare(this.plugin.getItemName(folderB))
     })
 
-    this.updateState(Object.assign({}, state, {
+    this.plugin.setPluginState(Object.assign({}, state, {
       files: sortedFiles,
       folders: sortedFolders,
       sorting: (sorting === 'titleDescending') ? 'titleAscending' : 'titleDescending'
@@ -248,7 +250,7 @@ module.exports = class View {
   }
 
   sortByDate () {
-    const state = Object.assign({}, this.plugin.core.getState()[this.plugin.stateId])
+    const state = Object.assign({}, this.plugin.getPluginState())
     const {files, folders, sorting} = state
 
     let sortedFiles = files.sort((fileA, fileB) => {
@@ -272,7 +274,7 @@ module.exports = class View {
       return a > b ? 1 : a < b ? -1 : 0
     })
 
-    this.updateState(Object.assign({}, state, {
+    this.plugin.setPluginState(Object.assign({}, state, {
       files: sortedFiles,
       folders: sortedFolders,
       sorting: (sorting === 'dateDescending') ? 'dateAscending' : 'dateDescending'
@@ -280,7 +282,7 @@ module.exports = class View {
   }
 
   sortBySize () {
-    const state = Object.assign({}, this.plugin.core.getState()[this.plugin.stateId])
+    const state = Object.assign({}, this.plugin.getPluginState())
     const {files, sorting} = state
 
     // check that plugin supports file sizes
@@ -298,19 +300,184 @@ module.exports = class View {
       return a > b ? 1 : a < b ? -1 : 0
     })
 
-    this.updateState(Object.assign({}, state, {
+    this.plugin.setPluginState(Object.assign({}, state, {
       files: sortedFiles,
       sorting: (sorting === 'sizeDescending') ? 'sizeAscending' : 'sizeDescending'
     }))
   }
 
   isActiveRow (file) {
-    return this.plugin.core.getState()[this.plugin.stateId].activeRow === this.plugin.getItemId(file)
+    return this.plugin.getPluginState().activeRow === this.plugin.getItemId(file)
+  }
+
+  isChecked (item) {
+    const itemId = this.providerFileToId(item)
+    if (this.plugin.isFolder(item)) {
+      const state = this.plugin.getPluginState()
+      const folders = state.selectedFolders || {}
+      if (itemId in folders) {
+        return folders[itemId]
+      }
+      return false
+    }
+    return (itemId in this.plugin.uppy.getState().files)
+  }
+
+  /**
+   * Adds all files found inside of specified folder.
+   *
+   * Uses separated state while folder contents are being fetched and
+   * mantains list of selected folders, which are separated from files.
+   */
+  addFolder (folder) {
+    const folderId = this.providerFileToId(folder)
+    let state = this.plugin.getPluginState()
+    let folders = state.selectedFolders || {}
+    if (folderId in folders && folders[folderId].loading) {
+      return
+    }
+    folders[folderId] = {loading: true, files: []}
+    this.plugin.setPluginState({selectedFolders: folders})
+    this.Provider.list(this.plugin.getItemRequestPath(folder)).then((res) => {
+      let files = []
+      this.plugin.getItemSubList(res).forEach((item) => {
+        if (!this.plugin.isFolder(item)) {
+          this.addFile(item, true)
+          files.push(this.providerFileToId(item))
+        }
+      })
+      state = this.plugin.getPluginState()
+      state.selectedFolders[folderId] = {loading: false, files: files}
+      this.plugin.setPluginState({selectedFolders: folders})
+      const dashboard = this.plugin.uppy.getPlugin('Dashboard')
+      let message
+      if (files.length) {
+        message = dashboard.i18n('folderAdded', {
+          smart_count: files.length, folder: this.plugin.getItemName(folder)
+        })
+      } else {
+        message = dashboard.i18n('emptyFolderAdded')
+      }
+      this.plugin.uppy.info(message)
+    }).catch((e) => {
+      state = this.plugin.getPluginState()
+      delete state.selectedFolders[folderId]
+      this.plugin.setPluginState({selectedFolders: state.selectedFolders})
+      this.handleError(e)
+    })
+  }
+
+  removeFolder (folderId) {
+    let state = this.plugin.getPluginState()
+    let folders = state.selectedFolders || {}
+    if (!(folderId in folders)) {
+      return
+    }
+    let folder = folders[folderId]
+    if (folder.loading) {
+      return
+    }
+    // deepcopy the files before iteration because the
+    // original array constantly gets mutated during
+    // the iteration by updateFolderState as each file
+    // is removed and 'core:file-removed' is emitted.
+    const files = folder.files.concat([])
+    for (const fileId of files) {
+      if (fileId in this.plugin.uppy.getState().files) {
+        this.plugin.uppy.removeFile(fileId)
+      }
+    }
+    delete folders[folderId]
+    this.plugin.setPluginState({selectedFolders: folders})
+  }
+
+  /**
+   * Updates selected folders state everytime file is being removed.
+   *
+   * Note that this is only important when files are getting removed from the
+   * main screen, and will do nothing when you uncheck folder directly, since
+   * it's already been done in removeFolder method.
+   */
+  updateFolderState (fileId) {
+    let state = this.plugin.getPluginState()
+    let folders = state.selectedFolders || {}
+    for (let folderId in folders) {
+      let folder = folders[folderId]
+      if (folder.loading) {
+        continue
+      }
+      let i = folder.files.indexOf(fileId)
+      if (i > -1) {
+        folder.files.splice(i, 1)
+      }
+      if (!folder.files.length) {
+        delete folders[folderId]
+      }
+    }
+    this.plugin.setPluginState({selectedFolders: folders})
+  }
+
+  /**
+   * Toggles file/folder checkbox to on/off state while updating files list.
+   *
+   * Note that some extra complexity comes from supporting shift+click to
+   * toggle multiple checkboxes at once, which is done by getting all files
+   * in between last checked file and current one, and applying an on/off state
+   * for all of them, depending on current file state.
+   */
+  toggleCheckbox (e, file) {
+    console.log(e, e.shiftKey)
+    e.stopPropagation()
+    e.preventDefault()
+    let { folders, files, filterInput } = this.plugin.getPluginState()
+    let items = folders.concat(files)
+    if (filterInput !== '') {
+      items = this.filterItems(items)
+    }
+    let itemsToToggle = [file]
+    if (this.lastCheckbox && e.shiftKey) {
+      let prevIndex = items.indexOf(this.lastCheckbox)
+      let currentIndex = items.indexOf(file)
+      if (prevIndex < currentIndex) {
+        itemsToToggle = items.slice(prevIndex, currentIndex + 1)
+      } else {
+        itemsToToggle = items.slice(currentIndex, prevIndex + 1)
+      }
+    }
+    this.lastCheckbox = file
+    if (this.isChecked(file)) {
+      for (let item of itemsToToggle) {
+        const itemId = this.providerFileToId(item)
+        if (this.plugin.isFolder(item)) {
+          this.removeFolder(itemId)
+        } else {
+          if (itemId in this.plugin.uppy.getState().files) {
+            this.plugin.uppy.removeFile(itemId)
+          }
+        }
+      }
+    } else {
+      for (let item of itemsToToggle) {
+        if (this.plugin.isFolder(item)) {
+          this.addFolder(item)
+        } else {
+          this.addFile(item, true)
+        }
+      }
+    }
+  }
+
+  providerFileToId (file) {
+    return Utils.generateFileID({
+      data: this.plugin.getItemData(file),
+      name: this.plugin.getItemName(file) || this.plugin.getItemId(file),
+      type: this.plugin.getMimeType(file)
+    })
   }
 
   handleDemoAuth () {
-    const state = this.plugin.core.getState()[this.plugin.stateId]
-    this.updateState({}, state, {
+    const state = this.plugin.getPluginState()
+    this.plugin.setPluginState({}, state, {
       authenticated: true
     })
   }
@@ -347,10 +514,10 @@ module.exports = class View {
   }
 
   handleError (error) {
-    const core = this.plugin.core
-    const message = core.i18n('uppyServerError')
-    core.log(error.toString())
-    core.info({message: message, details: error.toString()}, 'error', 5000)
+    const uppy = this.plugin.uppy
+    const message = uppy.i18n('uppyServerError')
+    uppy.log(error.toString())
+    uppy.info({message: message, details: error.toString()}, 'error', 5000)
   }
 
   handleScroll (e) {
@@ -360,7 +527,7 @@ module.exports = class View {
     if (scrollPos < 50 && path && !this._isHandlingScroll) {
       this.Provider.list(path)
         .then((res) => {
-          const { files, folders } = this.plugin.core.getState()[this.plugin.stateId]
+          const { files, folders } = this.plugin.getPluginState()
           this._updateFilesAndFolders(res, files, folders)
         }).catch(this.handleError)
         .then(() => { this._isHandlingScroll = false }) // always called
@@ -369,23 +536,28 @@ module.exports = class View {
     }
   }
 
+  donePicking () {
+    const dashboard = this.plugin.uppy.getPlugin('Dashboard')
+    if (dashboard) dashboard.hideAllPanels()
+  }
+
   // displays loader view while asynchronous request is being made.
   _loaderWrapper (promise, then, catch_) {
     promise
       .then(then).catch(catch_)
-      .then(() => this.updateState({ loading: false })) // always called.
-    this.updateState({ loading: true })
+      .then(() => this.plugin.setPluginState({ loading: false })) // always called.
+    this.plugin.setPluginState({ loading: true })
   }
 
   render (state) {
-    const { authenticated, checkAuthInProgress, loading } = state[this.plugin.stateId]
+    const { authenticated, checkAuthInProgress, loading } = this.plugin.getPluginState()
 
     if (loading) {
       return LoaderView()
     }
 
     if (!authenticated) {
-      return AuthView({
+      return h(AuthView, {
         pluginName: this.plugin.title,
         demo: this.plugin.opts.demo,
         checkAuth: this.checkAuth,
@@ -395,7 +567,7 @@ module.exports = class View {
       })
     }
 
-    const browserProps = Object.assign({}, state[this.plugin.stateId], {
+    const browserProps = Object.assign({}, this.plugin.getPluginState(), {
       getNextFolder: this.getNextFolder,
       getFolder: this.getFolder,
       addFile: this.addFile,
@@ -407,9 +579,12 @@ module.exports = class View {
       logout: this.logout,
       demo: this.plugin.opts.demo,
       isActiveRow: this.isActiveRow,
+      isChecked: this.isChecked,
+      toggleCheckbox: this.toggleCheckbox,
       getItemName: this.plugin.getItemName,
       getItemIcon: this.plugin.getItemIcon,
       handleScroll: this.handleScroll,
+      done: this.donePicking,
       title: this.plugin.title,
       viewType: this.opts.viewType
     })

+ 6 - 6
src/plugins/Redux.js

@@ -1,8 +1,8 @@
-const Plugin = require('./Plugin')
+const Plugin = require('../core/Plugin')
 
 module.exports = class Redux extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'state-sync'
     this.id = 'Redux'
     this.title = 'Redux Emitter'
@@ -23,11 +23,11 @@ module.exports = class Redux extends Plugin {
   }
 
   install () {
-    this.core.emitter.on('core:state-update', this.handleStateUpdate)
-    this.handleStateUpdate({}, this.core.state, this.core.state) // set the initial redux state
+    this.uppy.on('state-update', this.handleStateUpdate)
+    this.handleStateUpdate({}, this.uppy.state, this.uppy.state) // set the initial redux state
   }
 
   uninstall () {
-    this.core.emitter.off('core:state-update', this.handleStateUpdate)
+    this.uppy.off('state-update', this.handleStateUpdate)
   }
 }

+ 10 - 16
src/plugins/Redux.test.js

@@ -1,5 +1,5 @@
 import ReduxPlugin from './Redux'
-import Plugin from './Plugin'
+import Plugin from '../core/Plugin'
 
 describe('uploader/reduxPlugin', () => {
   it('should initialise successfully', () => {
@@ -33,9 +33,7 @@ describe('uploader/reduxPlugin', () => {
   describe('install', () => {
     it('should subscribe to uppy events', () => {
       const core = {
-        emitter: {
-          on: jest.fn()
-        }
+        on: jest.fn()
       }
 
       const redux = new ReduxPlugin(core, {
@@ -45,18 +43,16 @@ describe('uploader/reduxPlugin', () => {
       redux.handleStateUpdate = jest.fn()
       redux.install()
 
-      expect(core.emitter.on.mock.calls.length).toEqual(1)
-      expect(core.emitter.on.mock.calls[0]).toEqual([
-        'core:state-update',
+      expect(core.on.mock.calls.length).toEqual(1)
+      expect(core.on.mock.calls[0]).toEqual([
+        'state-update',
         redux.handleStateUpdate
       ])
     })
 
     it('should call this.handleStateUpdate with the current state on install', () => {
       const core = {
-        emitter: {
-          on: jest.fn()
-        }
+        on: jest.fn()
       }
 
       const redux = new ReduxPlugin(core, {
@@ -78,9 +74,7 @@ describe('uploader/reduxPlugin', () => {
   describe('uninstall', () => {
     it('should should unsubscribe from uppy events on uninstall', () => {
       const core = {
-        emitter: {
-          off: jest.fn()
-        }
+        off: jest.fn()
       }
 
       const redux = new ReduxPlugin(core, {
@@ -89,9 +83,9 @@ describe('uploader/reduxPlugin', () => {
       })
       redux.uninstall()
 
-      expect(core.emitter.off.mock.calls.length).toEqual(1)
-      expect(core.emitter.off.mock.calls[0]).toEqual([
-        'core:state-update',
+      expect(core.off.mock.calls.length).toEqual(1)
+      expect(core.off.mock.calls[0]).toEqual([
+        'state-update',
         redux.handleStateUpdate
       ])
     })

+ 11 - 10
src/plugins/ReduxDevTools.js

@@ -1,4 +1,4 @@
-const Plugin = require('./Plugin')
+const Plugin = require('../core/Plugin')
 
 /**
  * Add Redux DevTools support to Uppy
@@ -7,8 +7,8 @@ const Plugin = require('./Plugin')
  * and https://github.com/zalmoxisus/mobx-remotedev/blob/master/src/monitorActions.js
  */
 module.exports = class ReduxDevTools extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'debugger'
     this.id = 'ReduxDevTools'
     this.title = 'Redux DevTools'
@@ -36,18 +36,18 @@ module.exports = class ReduxDevTools extends Plugin {
         // Implement monitors actions
         switch (message.payload.type) {
           case 'RESET':
-            this.core.reset()
+            this.uppy.reset()
             return
           case 'IMPORT_STATE':
             const computedStates = message.payload.nextLiftedState.computedStates
-            this.core.state = Object.assign({}, this.core.state, computedStates[computedStates.length - 1].state)
-            this.core.updateAll(this.core.state)
+            this.uppy.state = Object.assign({}, this.uppy.state, computedStates[computedStates.length - 1].state)
+            this.uppy.updateAll(this.uppy.state)
             return
           case 'JUMP_TO_STATE':
           case 'JUMP_TO_ACTION':
             // this.setState(state)
-            this.core.state = Object.assign({}, this.core.state, JSON.parse(message.state))
-            this.core.updateAll(this.core.state)
+            this.uppy.state = Object.assign({}, this.uppy.state, JSON.parse(message.state))
+            this.uppy.updateAll(this.uppy.state)
         }
       }
     })
@@ -57,13 +57,14 @@ module.exports = class ReduxDevTools extends Plugin {
     this.withDevTools = typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__
     if (this.withDevTools) {
       this.initDevTools()
-      this.core.on('core:state-update', this.handleStateChange)
+      this.uppy.on('state-update', this.handleStateChange)
     }
   }
 
   uninstall () {
     if (this.withDevTools) {
-      this.core.emitter.off('core:state-update', this.handleStateUpdate)
+      this.devToolsUnsubscribe()
+      this.uppy.off('state-update', this.handleStateUpdate)
     }
   }
 }

+ 0 - 5
src/plugins/RestoreFiles/index.js

@@ -1,5 +0,0 @@
-const GoldenRetriever = require('../GoldenRetriever')
-
-console.warn('Using `uppy/lib/plugins/RestoreFiles` is deprecated and will be removed in v0.22. Please use `uppy/lib/plugins/GoldenRetriever` instead.')
-
-module.exports = GoldenRetriever

+ 95 - 85
src/plugins/StatusBar/StatusBar.js

@@ -1,11 +1,11 @@
-const html = require('yo-yo')
 const throttle = require('lodash.throttle')
+const { h } = require('preact')
 
 function progressDetails (props) {
-  return html`<span>${props.totalProgress || 0}%・${props.complete} / ${props.inProgress}・${props.totalUploadedSize} / ${props.totalSize}・↑ ${props.totalSpeed}/s・${props.totalETA}</span>`
+  return <span>{props.totalProgress || 0}%・{props.complete} / {props.inProgress}・{props.totalUploadedSize} / {props.totalSize}・↑ {props.totalSpeed}/s・{props.totalETA}</span>
 }
 
-const throttledProgressDetails = throttle(progressDetails, 1000, {leading: true, trailing: true})
+const ThrottledProgressDetails = throttle(progressDetails, 500, {leading: true, trailing: true})
 
 const STATE_ERROR = 'error'
 const STATE_WAITING = 'waiting'
@@ -15,10 +15,6 @@ const STATE_POSTPROCESSING = 'postprocessing'
 const STATE_COMPLETE = 'complete'
 
 function getUploadingState (props, files) {
-  // if (props.error) {
-  //   return STATE_ERROR
-  // }
-
   if (props.isAllErrored) {
     return STATE_ERROR
   }
@@ -80,6 +76,20 @@ function calculateProcessingProgress (files) {
   }
 }
 
+function togglePauseResume (props) {
+  if (props.isAllComplete) return
+
+  if (!props.resumableUploads) {
+    return props.cancelAll()
+  }
+
+  if (props.isAllPaused) {
+    return props.resumeAll()
+  }
+
+  return props.pauseAll()
+}
+
 module.exports = (props) => {
   props = props || {}
 
@@ -106,82 +116,96 @@ module.exports = (props) => {
   }
 
   const width = typeof progressValue === 'number' ? progressValue : 100
-
-  return html`
-    <div class="UppyStatusBar is-${uploadState}"
-                aria-hidden="${uploadState === STATE_WAITING}"
-                title="">
-      <progress style="display: none;" min="0" max="100" value=${progressValue}></progress>
-      <div class="UppyStatusBar-progress ${progressMode ? `is-${progressMode}` : ''}"
-           style="width: ${width}%"></div>
-      ${progressBarContent}
+  const isHidden = (uploadState === STATE_WAITING && props.hideUploadButton) ||
+    (uploadState === STATE_WAITING && !props.newFiles > 0)
+
+  const progressClasses = `uppy-StatusBar-progress 
+                           ${progressMode ? 'is-' + progressMode : ''}`
+
+  return (
+    <div class={`uppy uppy-StatusBar is-${uploadState}`} aria-hidden={isHidden}>
+      <div class={progressClasses}
+        style={{ width: width + '%' }}
+        role="progressbar"
+        aria-valuemin="0"
+        aria-valuemax="100"
+        aria-valuenow={progressValue} />
+      {progressBarContent}
+      <div class="uppy-StatusBar-actions">
+        { props.newFiles && !props.hideUploadButton ? <UploadBtn {...props} /> : null }
+        { props.error ? <RetryBtn {...props} /> : null }
+      </div>
     </div>
-  `
+  )
+}
+
+const UploadBtn = (props) => {
+  return <button type="button"
+    class="uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--upload"
+    aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
+    onclick={props.startUpload}>
+    {props.inProgress
+      ? props.i18n('uploadXNewFiles', { smart_count: props.newFiles })
+      : props.i18n('uploadXFiles', { smart_count: props.newFiles })
+    }
+  </button>
+}
+
+const RetryBtn = (props) => {
+  return <button type="button"
+    class="uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
+    aria-label={props.i18n('retryUpload')}
+    onclick={props.retryAll}>{props.i18n('retry')}</button>
 }
 
 const ProgressBarProcessing = (props) => {
-  return html`
-    <div class="UppyStatusBar-content">
-      ${props.mode === 'determinate' ? `${Math.round(props.value * 100)}%・` : ''}
-      ${props.message}
-    </div>
-  `
+  const value = Math.round(props.value * 100)
+
+  return <div class="uppy-StatusBar-content">
+    {props.mode === 'determinate' ? `${value}%・` : ''}
+    {props.message}
+  </div>
 }
 
 const ProgressBarUploading = (props) => {
-  return html`
-    <div class="UppyStatusBar-content">
-      ${props.isUploadStarted && !props.isAllComplete
+  return (
+    <div class="uppy-StatusBar-content">
+      {props.isUploadStarted && !props.isAllComplete
         ? !props.isAllPaused
-          ? html`<div title="Uploading">${pauseResumeButtons(props)} Uploading... ${throttledProgressDetails(props)}</div>`
-          : html`<div title="Paused">${pauseResumeButtons(props)} Paused・${props.totalProgress}%</div>`
+          ? <div title="Uploading">{ <PauseResumeButtons {...props} /> } Uploading... { <ThrottledProgressDetails {...props} /> }</div>
+          : <div title="Paused">{ <PauseResumeButtons {...props} /> } Paused・{props.totalProgress}%</div>
         : null
-        }
+      }
     </div>
-  `
+  )
 }
 
 const ProgressBarComplete = ({ totalProgress, i18n }) => {
-  return html`
-    <div class="UppyStatusBar-content">
+  return (
+    <div class="uppy-StatusBar-content" role="status">
       <span title="Complete">
-        <svg aria-hidden="true" class="UppyStatusBar-action UppyIcon" width="18" height="17" viewBox="0 0 23 17">
+        <svg aria-hidden="true" class="uppy-StatusBar-statusIndicator UppyIcon" width="18" height="17" viewBox="0 0 23 17">
           <path d="M8.944 17L0 7.865l2.555-2.61 6.39 6.525L20.41 0 23 2.645z" />
         </svg>
-        ${i18n('uploadComplete')}・${totalProgress}%
+        {i18n('uploadComplete')}・{totalProgress}%
       </span>
     </div>
-  `
+  )
 }
 
 const ProgressBarError = ({ error, retryAll, i18n }) => {
-  return html`
-    <div class="UppyStatusBar-content">
-        <button class="UppyStatusBar-action" 
-                title="${i18n('retryUpload')}" 
-                aria-label="${i18n('retryUpload')}" 
-                type="button" 
-                onclick=${retryAll}>
-            <svg class="UppyIcon" width="28" height="31" viewBox="0 0 16 19" xmlns="http://www.w3.org/2000/svg">
-              <path d="M16 11a8 8 0 1 1-8-8v2a6 6 0 1 0 6 6h2z"/>
-              <path d="M7.9 3H10v2H7.9z"/><path d="M8.536.5l3.535 3.536-1.414 1.414L7.12 1.914z"/><path d="M10.657 2.621l1.414 1.415L8.536 7.57 7.12 6.157z"/>
-            </svg></button>
-        ${i18n('uploadFailed')}. 
-        <button class="UppyStatusBar-retryBtn" 
-            title="${i18n('retryUpload')}" 
-            aria-label="${i18n('retryUpload')}" 
-            type="button" 
-            onclick=${retryAll}>
-          ${i18n('retry')}</button>
-        <span class="UppyStatusBar-details" 
-              data-balloon="${error}" 
-              data-balloon-pos="up" 
-              data-balloon-length="large">?</span>
-      </div>
-  `
+  return (
+    <div class="uppy-StatusBar-content" role="alert">
+      <strong>{i18n('uploadFailed')}.</strong> <span>{i18n('pleasePressRetry')}</span>
+      <span class="uppy-StatusBar-details"
+        data-balloon={error}
+        data-balloon-pos="up"
+        data-balloon-length="large">?</span>
+    </div>
+  )
 }
 
-const pauseResumeButtons = (props) => {
+const PauseResumeButtons = (props) => {
   const { resumableUploads, isAllPaused, i18n } = props
   const title = resumableUploads
                 ? isAllPaused
@@ -189,32 +213,18 @@ const pauseResumeButtons = (props) => {
                   : i18n('pauseUpload')
                 : i18n('cancelUpload')
 
-  return html`<button title="${title}" class="UppyStatusBar-action" type="button" onclick=${() => togglePauseResume(props)}>
-    ${resumableUploads
+  return <button title={title} class="uppy-StatusBar-statusIndicator" type="button" onclick={() => togglePauseResume(props)}>
+    {resumableUploads
       ? isAllPaused
-        ? html`<svg aria-hidden="true" class="UppyIcon" width="15" height="17" viewBox="0 0 11 13">
+        ? <svg aria-hidden="true" class="UppyIcon" width="15" height="17" viewBox="0 0 11 13">
           <path d="M1.26 12.534a.67.67 0 0 1-.674.012.67.67 0 0 1-.336-.583v-11C.25.724.38.5.586.382a.658.658 0 0 1 .673.012l9.165 5.5a.66.66 0 0 1 .325.57.66.66 0 0 1-.325.573l-9.166 5.5z" />
-        </svg>`
-        : html`<svg aria-hidden="true" class="UppyIcon" width="16" height="17" viewBox="0 0 12 13">
-          <path d="M4.888.81v11.38c0 .446-.324.81-.722.81H2.722C2.324 13 2 12.636 2 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81zM9.888.81v11.38c0 .446-.324.81-.722.81H7.722C7.324 13 7 12.636 7 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81z"/>
-        </svg>`
-      : html`<svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 19 19">
-        <path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z"/>
-      </svg>`
+        </svg>
+        : <svg aria-hidden="true" class="UppyIcon" width="16" height="17" viewBox="0 0 12 13">
+          <path d="M4.888.81v11.38c0 .446-.324.81-.722.81H2.722C2.324 13 2 12.636 2 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81zM9.888.81v11.38c0 .446-.324.81-.722.81H7.722C7.324 13 7 12.636 7 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81z" />
+        </svg>
+      : <svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 19 19">
+        <path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
+      </svg>
     }
-  </button>`
-}
-
-const togglePauseResume = (props) => {
-  if (props.isAllComplete) return
-
-  if (!props.resumableUploads) {
-    return props.cancelAll()
-  }
-
-  if (props.isAllPaused) {
-    return props.resumeAll()
-  }
-
-  return props.pauseAll()
+  </button>
 }

+ 31 - 16
src/plugins/StatusBar/index.js

@@ -1,6 +1,6 @@
-const Plugin = require('../Plugin')
+const Plugin = require('../../core/Plugin')
 const Translator = require('../../core/Translator')
-const StatusBar = require('./StatusBar')
+const StatusBarUI = require('./StatusBar')
 const { getSpeed } = require('../../core/Utils')
 const { getBytesRemaining } = require('../../core/Utils')
 const { prettyETA } = require('../../core/Utils')
@@ -9,10 +9,10 @@ const prettyBytes = require('prettier-bytes')
 /**
  * A status bar.
  */
-module.exports = class StatusBarUI extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
-    this.id = 'StatusBar'
+module.exports = class StatusBar extends Plugin {
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = this.opts.id || 'StatusBar'
     this.title = 'StatusBar'
     this.type = 'progressindicator'
 
@@ -21,6 +21,7 @@ module.exports = class StatusBarUI extends Plugin {
         uploading: 'Uploading',
         uploadComplete: 'Upload complete',
         uploadFailed: 'Upload failed',
+        pleasePressRetry: 'Please press Retry to upload again',
         paused: 'Paused',
         error: 'Error',
         retry: 'Retry',
@@ -28,13 +29,22 @@ module.exports = class StatusBarUI extends Plugin {
         retryUpload: 'Retry upload',
         resumeUpload: 'Resume upload',
         cancelUpload: 'Cancel upload',
-        pauseUpload: 'Pause upload'
+        pauseUpload: 'Pause upload',
+        uploadXFiles: {
+          0: 'Upload %{smart_count} file',
+          1: 'Upload %{smart_count} files'
+        },
+        uploadXNewFiles: {
+          0: 'Upload +%{smart_count} file',
+          1: 'Upload +%{smart_count} files'
+        }
       }
     }
 
     // set default options
     const defaultOptions = {
       target: 'body',
+      hideUploadButton: false,
       showProgressDetails: false,
       locale: defaultLocale
     }
@@ -79,6 +89,9 @@ module.exports = class StatusBarUI extends Plugin {
     const uploadStartedFiles = Object.keys(files).filter((file) => {
       return files[file].progress.uploadStarted
     })
+    const newFiles = Object.keys(files).filter((file) => {
+      return !files[file].progress.uploadStarted
+    })
     const completeFiles = Object.keys(files).filter((file) => {
       return files[file].progress.uploadComplete
     })
@@ -126,9 +139,9 @@ module.exports = class StatusBarUI extends Plugin {
       !isAllErrored &&
       uploadStartedFiles.length > 0
 
-    const resumableUploads = this.core.getState().capabilities.resumableUploads || false
+    const resumableUploads = this.uppy.getState().capabilities.resumableUploads || false
 
-    return StatusBar({
+    return StatusBarUI({
       error: state.error,
       totalProgress: state.totalProgress,
       totalSize: totalSize,
@@ -139,24 +152,26 @@ module.exports = class StatusBarUI extends Plugin {
       isAllErrored: isAllErrored,
       isUploadStarted: isUploadStarted,
       i18n: this.i18n,
-      pauseAll: this.core.pauseAll,
-      resumeAll: this.core.resumeAll,
-      retryAll: this.core.retryAll,
-      cancelAll: this.core.cancelAll,
+      pauseAll: this.uppy.pauseAll,
+      resumeAll: this.uppy.resumeAll,
+      retryAll: this.uppy.retryAll,
+      cancelAll: this.uppy.cancelAll,
+      startUpload: this.uppy.upload,
       complete: completeFiles.length,
+      newFiles: newFiles.length,
       inProgress: uploadStartedFiles.length,
       totalSpeed: totalSpeed,
       totalETA: totalETA,
       files: state.files,
-      resumableUploads: resumableUploads
+      resumableUploads: resumableUploads,
+      hideUploadButton: this.opts.hideUploadButton
     })
   }
 
   install () {
     const target = this.opts.target
-    const plugin = this
     if (target) {
-      this.mount(target, plugin)
+      this.mount(target, this)
     }
   }
 

+ 306 - 66
src/plugins/Transloadit/index.js

@@ -1,14 +1,22 @@
 const Translator = require('../../core/Translator')
-const Plugin = require('../Plugin')
+const Plugin = require('../../core/Plugin')
 const Client = require('./Client')
 const StatusSocket = require('./Socket')
 
+function defaultGetAssemblyOptions (file, options) {
+  return {
+    params: options.params,
+    signature: options.signature,
+    fields: options.fields
+  }
+}
+
 /**
  * Upload files to Transloadit using Tus.
  */
 module.exports = class Transloadit extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'uploader'
     this.id = 'Transloadit'
     this.title = 'Transloadit'
@@ -24,18 +32,12 @@ module.exports = class Transloadit extends Plugin {
     const defaultOptions = {
       waitForEncoding: false,
       waitForMetadata: false,
-      alwaysRunAssembly: false, // TODO name
+      alwaysRunAssembly: false,
       importFromUploadURLs: false,
       signature: null,
       params: null,
       fields: {},
-      getAssemblyOptions (file, options) {
-        return {
-          params: options.params,
-          signature: options.signature,
-          fields: options.fields
-        }
-      },
+      getAssemblyOptions: defaultGetAssemblyOptions,
       locale: defaultLocale
     }
 
@@ -50,6 +52,8 @@ module.exports = class Transloadit extends Plugin {
     this.prepareUpload = this.prepareUpload.bind(this)
     this.afterUpload = this.afterUpload.bind(this)
     this.onFileUploadURLAvailable = this.onFileUploadURLAvailable.bind(this)
+    this.onRestored = this.onRestored.bind(this)
+    this.getPersistentData = this.getPersistentData.bind(this)
 
     if (this.opts.params) {
       this.validateParams(this.opts.params)
@@ -85,7 +89,7 @@ module.exports = class Transloadit extends Plugin {
     const options = this.opts
     return Promise.all(
       fileIDs.map((fileID) => {
-        const file = this.core.getFile(fileID)
+        const file = this.uppy.getFile(fileID)
         const promise = Promise.resolve()
           .then(() => options.getAssemblyOptions(file, options))
         return promise.then((assemblyOptions) => {
@@ -120,7 +124,7 @@ module.exports = class Transloadit extends Plugin {
   createAssembly (fileIDs, uploadID, options) {
     const pluginOptions = this.opts
 
-    this.core.log('Transloadit: create assembly')
+    this.uppy.log('[Transloadit] create assembly')
 
     return this.client.createAssembly({
       params: options.params,
@@ -158,7 +162,10 @@ module.exports = class Transloadit extends Plugin {
           // Only send assembly metadata to the tus endpoint.
           metaFields: Object.keys(tlMeta),
           // Make sure tus doesn't resume a previous upload.
-          uploadUrl: null
+          uploadUrl: null,
+          // Disable tus-js-client fingerprinting, otherwise uploading the same file at different times
+          // will upload to the same assembly.
+          resume: false
         })
         const transloadit = {
           assembly: assembly.assembly_id
@@ -172,22 +179,22 @@ module.exports = class Transloadit extends Plugin {
         return newFile
       }
 
-      const files = Object.assign({}, this.core.state.files)
+      const files = Object.assign({}, this.uppy.state.files)
       fileIDs.forEach((id) => {
         files[id] = attachAssemblyMetadata(files[id], assembly)
       })
 
-      this.core.setState({ files })
+      this.uppy.setState({ files })
 
-      this.core.emit('transloadit:assembly-created', assembly, fileIDs)
+      this.uppy.emit('transloadit:assembly-created', assembly, fileIDs)
 
       return this.connectSocket(assembly)
         .then(() => assembly)
     }).then((assembly) => {
-      this.core.log('Transloadit: Created assembly')
+      this.uppy.log('[Transloadit] Created assembly')
       return assembly
     }).catch((err) => {
-      this.core.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
+      this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
 
       // Reject the promise.
       throw err
@@ -204,7 +211,7 @@ module.exports = class Transloadit extends Plugin {
    */
   reserveFiles (assembly, fileIDs) {
     return Promise.all(fileIDs.map((fileID) => {
-      const file = this.core.getFile(fileID)
+      const file = this.uppy.getFile(fileID)
       return this.client.reserveFile(assembly, file)
     }))
   }
@@ -214,7 +221,7 @@ module.exports = class Transloadit extends Plugin {
    * once they have been fully uploaded.
    */
   onFileUploadURLAvailable (fileID) {
-    const file = this.core.getFile(fileID)
+    const file = this.uppy.getFile(fileID)
     if (!file || !file.transloadit || !file.transloadit.assembly) {
       return
     }
@@ -223,20 +230,31 @@ module.exports = class Transloadit extends Plugin {
     const assembly = state.assemblies[file.transloadit.assembly]
 
     this.client.addFile(assembly, file).catch((err) => {
-      this.core.log(err)
-      this.core.emit('transloadit:import-error', assembly, file.id, err)
+      this.uppy.log(err)
+      this.uppy.emit('transloadit:import-error', assembly, file.id, err)
     })
   }
 
   findFile (uploadedFile) {
-    const files = this.core.state.files
+    const files = this.uppy.state.files
     for (const id in files) {
       if (!files.hasOwnProperty(id)) {
         continue
       }
+      // Completed file upload.
       if (files[id].uploadURL === uploadedFile.tus_upload_url) {
         return files[id]
       }
+      // In-progress file upload.
+      if (files[id].tus && files[id].tus.uploadUrl === uploadedFile.tus_upload_url) {
+        return files[id]
+      }
+      if (!uploadedFile.is_tus_file) {
+        // Fingers-crossed check for non-tus uploads, eg imported from S3.
+        if (files[id].name === uploadedFile.name && files[id].size === uploadedFile.size) {
+          return files[id]
+        }
+      }
     }
   }
 
@@ -246,12 +264,13 @@ module.exports = class Transloadit extends Plugin {
     this.setPluginState({
       files: Object.assign({}, state.files, {
         [uploadedFile.id]: {
+          assembly: assemblyId,
           id: file.id,
           uploadedFile
         }
       })
     })
-    this.core.emit('transloadit:upload', uploadedFile, this.getAssembly(assemblyId))
+    this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assemblyId))
   }
 
   onResult (assemblyId, stepName, result) {
@@ -260,10 +279,17 @@ module.exports = class Transloadit extends Plugin {
     // The `file` may not exist if an import robot was used instead of a file upload.
     result.localId = file ? file.id : null
 
+    const entry = {
+      result,
+      stepName,
+      id: result.id,
+      assembly: assemblyId
+    }
+
     this.setPluginState({
-      results: state.results.concat(result)
+      results: [...state.results, entry]
     })
-    this.core.emit('transloadit:result', stepName, result, this.getAssembly(assemblyId))
+    this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assemblyId))
   }
 
   onAssemblyFinished (url) {
@@ -274,7 +300,201 @@ module.exports = class Transloadit extends Plugin {
           [assembly.assembly_id]: assembly
         })
       })
-      this.core.emit('transloadit:complete', assembly)
+      this.uppy.emit('transloadit:complete', assembly)
+    })
+  }
+
+  getPersistentData (setData) {
+    const state = this.getPluginState()
+    const assemblies = state.assemblies
+    const uploadsAssemblies = state.uploadsAssemblies
+    const uploads = Object.keys(state.files)
+    const results = state.results.map((result) => result.id)
+
+    setData({
+      [this.id]: {
+        assemblies,
+        uploadsAssemblies,
+        uploads,
+        results
+      }
+    })
+  }
+
+  /**
+   * Emit the necessary events that must have occured to get from the `prevState`,
+   * to the current state.
+   * For completed uploads, `transloadit:upload` is emitted.
+   * For new results, `transloadit:result` is emitted.
+   * For completed or errored assemblies, `transloadit:complete` or `transloadit:assembly-error` is emitted.
+   */
+  emitEventsDiff (prevState) {
+    const opts = this.opts
+    const state = this.getPluginState()
+
+    const emitMissedEvents = () => {
+      // Emit events for completed uploads and completed results
+      // that we've missed while we were away.
+      const newUploads = Object.keys(state.files).filter((fileID) => {
+        return !prevState.files.hasOwnProperty(fileID)
+      }).map((fileID) => state.files[fileID])
+      const newResults = state.results.filter((result) => {
+        return !prevState.results.some((prev) => prev.id === result.id)
+      })
+
+      this.uppy.log('[Transloadit] New fully uploaded files since restore:')
+      this.uppy.log(newUploads)
+      newUploads.forEach(({ assembly, uploadedFile }) => {
+        this.uppy.log(`[Transloadit]  emitting transloadit:upload ${uploadedFile.id}`)
+        this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assembly))
+      })
+      this.uppy.log('[Transloadit] New results since restore:')
+      this.uppy.log(newResults)
+      newResults.forEach(({ assembly, stepName, result, id }) => {
+        this.uppy.log(`[Transloadit]  emitting transloadit:result ${stepName}, ${id}`)
+        this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assembly))
+      })
+
+      const newAssemblies = state.assemblies
+      const previousAssemblies = prevState.assemblies
+      this.uppy.log('[Transloadit] Current assembly status after restore')
+      this.uppy.log(newAssemblies)
+      this.uppy.log('[Transloadit] Assembly status before restore')
+      this.uppy.log(previousAssemblies)
+      Object.keys(newAssemblies).forEach((assemblyId) => {
+        const oldAssembly = previousAssemblies[assemblyId]
+        diffAssemblyStatus(oldAssembly, newAssemblies[assemblyId])
+      })
+    }
+
+    // Emit events for assemblies that have completed or errored while we were away.
+    const diffAssemblyStatus = (prev, next) => {
+      this.uppy.log('[Transloadit] Diff assemblies')
+      this.uppy.log(prev)
+      this.uppy.log(next)
+
+      if (opts.waitForEncoding && next.ok === 'ASSEMBLY_COMPLETED' && prev.ok !== 'ASSEMBLY_COMPLETED') {
+        this.uppy.log(`[Transloadit]  Emitting transloadit:complete for ${next.assembly_id}`)
+        this.uppy.log(next)
+        this.uppy.emit('transloadit:complete', next)
+      } else if (opts.waitForMetadata && next.upload_meta_data_extracted && !prev.upload_meta_data_extracted) {
+        this.uppy.log(`[Transloadit]  Emitting transloadit:complete after metadata extraction for ${next.assembly_id}`)
+        this.uppy.log(next)
+        this.uppy.emit('transloadit:complete', next)
+      }
+
+      if (next.error && !prev.error) {
+        this.uppy.log(`[Transloadit]  !!! Emitting transloadit:assembly-error for ${next.assembly_id}`)
+        this.uppy.log(next)
+        this.uppy.emit('transloadit:assembly-error', next, new Error(next.message))
+      }
+    }
+
+    emitMissedEvents()
+  }
+
+  onRestored (pluginData) {
+    const savedState = pluginData && pluginData[this.id] ? pluginData[this.id] : {}
+    const knownUploads = savedState.files || []
+    const knownResults = savedState.results || []
+    const previousAssemblies = savedState.assemblies || {}
+    const uploadsAssemblies = savedState.uploadsAssemblies || {}
+
+    if (Object.keys(uploadsAssemblies).length === 0) {
+      // Nothing to restore.
+      return
+    }
+
+    // Fetch up-to-date assembly statuses.
+    const loadAssemblies = () => {
+      const assemblyIDs = []
+      Object.keys(uploadsAssemblies).forEach((uploadID) => {
+        assemblyIDs.push(...uploadsAssemblies[uploadID])
+      })
+
+      return Promise.all(
+        assemblyIDs.map((assemblyID) => {
+          const url = `https://api2.transloadit.com/assemblies/${assemblyID}`
+          return this.client.getAssemblyStatus(url)
+        })
+      )
+    }
+
+    const reconnectSockets = (assemblies) => {
+      return Promise.all(assemblies.map((assembly) => {
+        // No need to connect to the socket if the assembly has completed by now.
+        if (assembly.ok === 'ASSEMBLY_COMPLETE') {
+          return null
+        }
+        return this.connectSocket(assembly)
+      }))
+    }
+
+    // Convert loaded assembly statuses to a Transloadit plugin state object.
+    const restoreState = (assemblies) => {
+      const assembliesById = {}
+      const files = {}
+      const results = []
+      assemblies.forEach((assembly) => {
+        assembliesById[assembly.assembly_id] = assembly
+
+        assembly.uploads.forEach((uploadedFile) => {
+          const file = this.findFile(uploadedFile)
+          files[uploadedFile.id] = {
+            id: file.id,
+            assembly: assembly.assembly_id,
+            uploadedFile
+          }
+        })
+
+        const state = this.getPluginState()
+        Object.keys(assembly.results).forEach((stepName) => {
+          assembly.results[stepName].forEach((result) => {
+            const file = state.files[result.original_id]
+            result.localId = file ? file.id : null
+            results.push({
+              id: result.id,
+              result,
+              stepName,
+              assembly: assembly.assembly_id
+            })
+          })
+        })
+      })
+
+      this.setPluginState({
+        assemblies: assembliesById,
+        files: files,
+        results: results,
+        uploadsAssemblies: uploadsAssemblies
+      })
+    }
+
+    // Restore all assembly state.
+    this.restored = Promise.resolve()
+      .then(loadAssemblies)
+      .then((assemblies) => {
+        restoreState(assemblies)
+        return reconnectSockets(assemblies)
+      })
+      .then(() => {
+        // Return a callback that will be called by `afterUpload`
+        // once it has attached event listeners etc.
+        const newState = this.getPluginState()
+        const previousFiles = {}
+        knownUploads.forEach((id) => {
+          previousFiles[id] = newState.files[id]
+        })
+        return () => this.emitEventsDiff({
+          assemblies: previousAssemblies,
+          files: previousFiles,
+          results: newState.results.filter(({ id }) => knownResults.indexOf(id) !== -1),
+          uploadsAssemblies
+        })
+      })
+
+    this.restored.then(() => {
+      this.restored = null
     })
   }
 
@@ -287,7 +507,7 @@ module.exports = class Transloadit extends Plugin {
 
     socket.on('upload', this.onFileUploadComplete.bind(this, assembly.assembly_id))
     socket.on('error', (error) => {
-      this.core.emit('transloadit:assembly-error', assembly, error)
+      this.uppy.emit('transloadit:assembly-error', assembly, error)
     })
 
     if (this.opts.waitForEncoding) {
@@ -301,7 +521,6 @@ module.exports = class Transloadit extends Plugin {
     } else if (this.opts.waitForMetadata) {
       socket.on('metadata', () => {
         this.onAssemblyFinished(assembly.assembly_ssl_url)
-        this.core.emit('transloadit:complete', assembly)
       })
     }
 
@@ -309,13 +528,16 @@ module.exports = class Transloadit extends Plugin {
       socket.on('connect', resolve)
       socket.on('error', reject)
     }).then(() => {
-      this.core.log('Transloadit: Socket is ready')
+      this.uppy.log('[Transloadit] Socket is ready')
     })
   }
 
   prepareUpload (fileIDs, uploadID) {
+    // Only use files without errors
+    fileIDs = fileIDs.filter((file) => !file.error)
+
     fileIDs.forEach((fileID) => {
-      this.core.emit('core:preprocess-progress', fileID, {
+      this.uppy.emit('preprocess-progress', fileID, {
         mode: 'indeterminate',
         message: this.i18n('creatingAssembly')
       })
@@ -328,7 +550,7 @@ module.exports = class Transloadit extends Plugin {
         }
       }).then(() => {
         fileIDs.forEach((fileID) => {
-          this.core.emit('core:preprocess-complete', fileID)
+          this.uppy.emit('preprocess-complete', fileID)
         })
       })
     }
@@ -364,7 +586,20 @@ module.exports = class Transloadit extends Plugin {
   }
 
   afterUpload (fileIDs, uploadID) {
+    // Only use files without errors
+    fileIDs = fileIDs.filter((file) => !file.error)
+
     const state = this.getPluginState()
+
+    // If we're still restoring state, wait for that to be done.
+    if (this.restored) {
+      return this.restored.then((emitMissedEvents) => {
+        const promise = this.afterUpload(fileIDs, uploadID)
+        emitMissedEvents()
+        return promise
+      })
+    }
+
     const assemblyIDs = state.uploadsAssemblies[uploadID]
 
     // If we don't have to wait for encoding metadata or results, we can close
@@ -387,7 +622,7 @@ module.exports = class Transloadit extends Plugin {
 
     return new Promise((resolve, reject) => {
       fileIDs.forEach((fileID) => {
-        this.core.emit('core:postprocess-progress', fileID, {
+        this.uppy.emit('postprocess-progress', fileID, {
           mode: 'indeterminate',
           message: this.i18n('encoding')
         })
@@ -396,8 +631,10 @@ module.exports = class Transloadit extends Plugin {
       const onAssemblyFinished = (assembly) => {
         // An assembly for a different upload just finished. We can ignore it.
         if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
+          this.uppy.log(`[Transloadit] afterUpload(): Ignoring finished assembly ${assembly.assembly_id}`)
           return
         }
+        this.uppy.log(`[Transloadit] afterUpload(): Got assembly finish ${assembly.assembly_id}`)
 
         // TODO set the `file.uploadURL` to a result?
         // We will probably need an option here so the plugin user can tell us
@@ -405,40 +642,31 @@ module.exports = class Transloadit extends Plugin {
 
         const files = this.getAssemblyFiles(assembly.assembly_id)
         files.forEach((file) => {
-          this.core.emit('core:postprocess-complete', file.id)
+          this.uppy.emit('postprocess-complete', file.id)
         })
 
-        finishedAssemblies += 1
-        if (finishedAssemblies === assemblyIDs.length) {
-          // We're done, these listeners can be removed
-          removeListeners()
-          resolve()
-        }
+        checkAllComplete()
       }
 
       const onAssemblyError = (assembly, error) => {
-        // An assembly for a different upload just finished. We can ignore it.
+        // An assembly for a different upload just errored. We can ignore it.
         if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
+          this.uppy.log(`[Transloadit] afterUpload(): Ignoring errored assembly ${assembly.assembly_id}`)
           return
         }
+        this.uppy.log(`[Transloadit] afterUpload(): Got assembly error ${assembly.assembly_id}`)
+        this.uppy.log(error)
 
         // Clear postprocessing state for all our files.
         const files = this.getAssemblyFiles(assembly.assembly_id)
         files.forEach((file) => {
           // TODO Maybe make a postprocess-error event here?
-          this.core.emit('core:upload-error', file.id, error)
+          this.uppy.emit('upload-error', file.id, error)
 
-          this.core.emit('core:postprocess-complete', file.id)
+          this.uppy.emit('postprocess-complete', file.id)
         })
 
-        // Should we remove the listeners here or should we keep handling finished
-        // assemblies?
-        // Doing this for now so that it's not possible to receive more postprocessing
-        // events once the upload has failed.
-        removeListeners()
-
-        // Reject the `afterUpload()` promise.
-        reject(error)
+        checkAllComplete()
       }
 
       const onImportError = (assembly, fileID, error) => {
@@ -454,15 +682,24 @@ module.exports = class Transloadit extends Plugin {
         onAssemblyError(assembly, error)
       }
 
+      const checkAllComplete = () => {
+        finishedAssemblies += 1
+        if (finishedAssemblies === assemblyIDs.length) {
+          // We're done, these listeners can be removed
+          removeListeners()
+          resolve()
+        }
+      }
+
       const removeListeners = () => {
-        this.core.off('transloadit:complete', onAssemblyFinished)
-        this.core.off('transloadit:assembly-error', onAssemblyError)
-        this.core.off('transloadit:import-error', onImportError)
+        this.uppy.off('transloadit:complete', onAssemblyFinished)
+        this.uppy.off('transloadit:assembly-error', onAssemblyError)
+        this.uppy.off('transloadit:import-error', onImportError)
       }
 
-      this.core.on('transloadit:complete', onAssemblyFinished)
-      this.core.on('transloadit:assembly-error', onAssemblyError)
-      this.core.on('transloadit:import-error', onImportError)
+      this.uppy.on('transloadit:complete', onAssemblyFinished)
+      this.uppy.on('transloadit:assembly-error', onAssemblyError)
+      this.uppy.on('transloadit:import-error', onImportError)
     }).then(() => {
       // Clean up uploadID → assemblyIDs, they're no longer going to be used anywhere.
       const state = this.getPluginState()
@@ -473,13 +710,16 @@ module.exports = class Transloadit extends Plugin {
   }
 
   install () {
-    this.core.addPreProcessor(this.prepareUpload)
-    this.core.addPostProcessor(this.afterUpload)
+    this.uppy.addPreProcessor(this.prepareUpload)
+    this.uppy.addPostProcessor(this.afterUpload)
 
     if (this.opts.importFromUploadURLs) {
-      this.core.on('core:upload-success', this.onFileUploadURLAvailable)
+      this.uppy.on('upload-success', this.onFileUploadURLAvailable)
     }
 
+    this.uppy.on('restore:get-data', this.getPersistentData)
+    this.uppy.on('restored', this.onRestored)
+
     this.setPluginState({
       // Contains assembly status objects, indexed by their ID.
       assemblies: {},
@@ -493,11 +733,11 @@ module.exports = class Transloadit extends Plugin {
   }
 
   uninstall () {
-    this.core.removePreProcessor(this.prepareUpload)
-    this.core.removePostProcessor(this.afterUpload)
+    this.uppy.removePreProcessor(this.prepareUpload)
+    this.uppy.removePostProcessor(this.afterUpload)
 
     if (this.opts.importFromUploadURLs) {
-      this.core.off('core:upload-success', this.onFileUploadURLAvailable)
+      this.uppy.off('upload-success', this.onFileUploadURLAvailable)
     }
   }
 
@@ -507,9 +747,9 @@ module.exports = class Transloadit extends Plugin {
   }
 
   getAssemblyFiles (assemblyID) {
-    const fileIDs = Object.keys(this.core.state.files)
+    const fileIDs = Object.keys(this.uppy.state.files)
     return fileIDs.map((fileID) => {
-      return this.core.getFile(fileID)
+      return this.uppy.getFile(fileID)
     }).filter((file) => {
       return file && file.transloadit && file.transloadit.assembly === assemblyID
     })

+ 173 - 0
src/plugins/Transloadit/index.test.js

@@ -0,0 +1,173 @@
+const Core = require('../../core')
+const Transloadit = require('./')
+
+describe('Transloadit', () => {
+  it('Throws errors if options are missing', () => {
+    const uppy = new Core()
+
+    expect(() => {
+      uppy.use(Transloadit, { params: {} })
+    }).toThrowError(/The `params\.auth\.key` option is required/)
+  })
+
+  it('Accepts a JSON string as `params` for signature authentication', () => {
+    const uppy = new Core()
+
+    expect(() => {
+      uppy.use(Transloadit, {
+        params: 'not json'
+      })
+    }).toThrowError(/The `params` option is a malformed JSON string/)
+
+    expect(() => {
+      uppy.use(Transloadit, {
+        params: '{"template_id":"some template id string"}'
+      })
+    }).toThrowError(/The `params\.auth\.key` option is required/)
+    expect(() => {
+      uppy.use(Transloadit, {
+        params: '{"auth":{"key":"some auth key string"},"template_id":"some template id string"}'
+      })
+    }).not.toThrowError(/The `params\.auth\.key` option is required/)
+  })
+
+  it('Validates response from getAssemblyOptions()', () => {
+    const uppy = new Core({ autoProceed: false })
+
+    uppy.use(Transloadit, {
+      getAssemblyOptions: (file) => {
+        expect(file.name).toBe('testfile')
+        return {
+          params: '{"some":"json"}'
+        }
+      }
+    })
+
+    const data = Buffer.alloc(4000)
+    data.size = data.byteLength
+    return uppy.addFile({
+      name: 'testfile',
+      data
+    }).then(() => {
+      return uppy.upload().then(() => {
+        throw new Error('should have rejected')
+      }, (err) => {
+        expect(err.message).toMatch(/The `params\.auth\.key` option is required/)
+      })
+    })
+  })
+
+  it('Uses different assemblies for different params', () => {
+    const uppy = new Core({ autoProceed: false })
+
+    uppy.use(Transloadit, {
+      getAssemblyOptions: (file) => ({
+        params: {
+          auth: { key: 'fake key' },
+          steps: {
+            fake_step: { data: file.name }
+          }
+        }
+      })
+    })
+
+    const tl = uppy.getPlugin('Transloadit')
+    const files = ['a.png', 'b.png', 'c.png', 'd.png']
+    let i = 0
+    tl.client.createAssembly = (opts) => {
+      expect(opts.params.steps.fake_step.data).toEqual(files[i])
+      i++
+      // Short-circuit upload
+      return Promise.reject('short-circuit') // eslint-disable-line prefer-promise-reject-errors
+    }
+
+    const data = Buffer.alloc(10)
+    data.size = data.byteLength
+
+    return Promise.all([
+      uppy.addFile({ name: 'a.png', data }),
+      uppy.addFile({ name: 'b.png', data }),
+      uppy.addFile({ name: 'c.png', data }),
+      uppy.addFile({ name: 'd.png', data })
+    ]).then(() => {
+      return uppy.upload().then(() => {
+        throw new Error('upload should have been rejected')
+      }, () => {
+        expect(i).toBe(4)
+      })
+    })
+  })
+
+  it('Should merge files with same parameters into one assembly', () => {
+    const uppy = new Core({ autoProceed: false })
+
+    uppy.use(Transloadit, {
+      getAssemblyOptions: (file) => ({
+        params: {
+          auth: { key: 'fake key' },
+          steps: {
+            fake_step: { data: file.size }
+          }
+        }
+      })
+    })
+
+    const tl = uppy.getPlugin('Transloadit')
+    const assemblies = [
+      { data: 10, files: ['a.png', 'b.png', 'c.png'] },
+      { data: 20, files: ['d.png'] }
+    ]
+    let i = 0
+    tl.client.createAssembly = (opts) => {
+      const assembly = assemblies[i]
+      expect(opts.params.steps.fake_step.data).toBe(assembly.data)
+      i++
+      // Short-circuit upload
+      return Promise.reject('short-circuit') // eslint-disable-line prefer-promise-reject-errors
+    }
+
+    const data = Buffer.alloc(10)
+    data.size = data.byteLength
+    const data2 = Buffer.alloc(20)
+    data2.size = data2.byteLength
+
+    return Promise.all([
+      uppy.addFile({ name: 'a.png', data }),
+      uppy.addFile({ name: 'b.png', data }),
+      uppy.addFile({ name: 'c.png', data }),
+      uppy.addFile({ name: 'd.png', data: data2 })
+    ]).then(() => {
+      return uppy.upload().then(() => {
+        throw new Error('Upload should have been rejected')
+      }, () => {
+        expect(i).toBe(2)
+      })
+    })
+  })
+
+  it('Does not create an assembly if no files are being uploaded', () => {
+    const uppy = new Core()
+    uppy.use(Transloadit, {
+      getAssemblyOptions () {
+        throw new Error('should not create assembly')
+      }
+    })
+    uppy.run()
+
+    return uppy.upload()
+  })
+
+  it('Creates an assembly if no files are being uploaded but `alwaysRunAssembly` is enabled', () => {
+    const uppy = new Core()
+    uppy.use(Transloadit, {
+      alwaysRunAssembly: true,
+      getAssemblyOptions (file) {
+        // should call getAssemblyOptions with `null`
+        expect(file).toBe(null)
+        return Promise.reject('short-circuited') // eslint-disable-line prefer-promise-reject-errors
+      }
+    })
+
+    return expect(uppy.upload()).rejects.toBe('short-circuited')
+  })
+})

+ 65 - 48
src/plugins/Tus.js

@@ -1,4 +1,4 @@
-const Plugin = require('./Plugin')
+const Plugin = require('../core/Plugin')
 const tus = require('tus-js-client')
 const UppySocket = require('../core/UppySocket')
 const {
@@ -50,8 +50,8 @@ function createEventTracker (emitter) {
  *
  */
 module.exports = class Tus extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'uploader'
     this.id = 'Tus'
     this.title = 'Tus'
@@ -75,7 +75,7 @@ module.exports = class Tus extends Plugin {
   }
 
   handleResetProgress () {
-    const files = Object.assign({}, this.core.state.files)
+    const files = Object.assign({}, this.uppy.state.files)
     Object.keys(files).forEach((fileID) => {
       // Only clone the file object if it has a Tus `uploadUrl` attached.
       if (files[fileID].tus && files[fileID].tus.uploadUrl) {
@@ -85,7 +85,7 @@ module.exports = class Tus extends Plugin {
       }
     })
 
-    this.core.setState({ files })
+    this.uppy.setState({ files })
   }
 
   /**
@@ -116,7 +116,7 @@ module.exports = class Tus extends Plugin {
    * @returns {Promise}
    */
   upload (file, current, total) {
-    this.core.log(`uploading ${current} of ${total}`)
+    this.uppy.log(`uploading ${current} of ${total}`)
 
     this.resetUploaderReferences(file.id)
 
@@ -131,8 +131,8 @@ module.exports = class Tus extends Plugin {
       )
 
       optsTus.onError = (err) => {
-        this.core.log(err)
-        this.core.emit('core:upload-error', file.id, err)
+        this.uppy.log(err)
+        this.uppy.emit('upload-error', file.id, err)
         err.message = `Failed because: ${err.message}`
 
         this.resetUploaderReferences(file.id)
@@ -141,7 +141,7 @@ module.exports = class Tus extends Plugin {
 
       optsTus.onProgress = (bytesUploaded, bytesTotal) => {
         this.onReceiveUploadUrl(file, upload.url)
-        this.core.emit('core:upload-progress', {
+        this.uppy.emit('upload-progress', {
           uploader: this,
           id: file.id,
           bytesUploaded: bytesUploaded,
@@ -150,10 +150,10 @@ module.exports = class Tus extends Plugin {
       }
 
       optsTus.onSuccess = () => {
-        this.core.emit('core:upload-success', file.id, upload, upload.url)
+        this.uppy.emit('upload-success', file.id, upload, upload.url)
 
         if (upload.url) {
-          this.core.log('Download ' + upload.file.name + ' from ' + upload.url)
+          this.uppy.log('Download ' + upload.file.name + ' from ' + upload.url)
         }
 
         this.resetUploaderReferences(file.id)
@@ -163,7 +163,7 @@ module.exports = class Tus extends Plugin {
 
       const upload = new tus.Upload(file.data, optsTus)
       this.uploaders[file.id] = upload
-      this.uploaderEvents[file.id] = createEventTracker(this.core)
+      this.uploaderEvents[file.id] = createEventTracker(this.uppy)
 
       this.onFileRemove(file.id, (targetFileID) => {
         this.resetUploaderReferences(file.id)
@@ -171,7 +171,11 @@ module.exports = class Tus extends Plugin {
       })
 
       this.onPause(file.id, (isPaused) => {
-        isPaused ? upload.abort() : upload.start()
+        if (isPaused) {
+          upload.abort()
+        } else {
+          upload.start()
+        }
       })
 
       this.onPauseAll(file.id, () => {
@@ -189,8 +193,12 @@ module.exports = class Tus extends Plugin {
         upload.start()
       })
 
-      upload.start()
-      this.core.emit('core:upload-started', file.id, upload)
+      if (!file.isPaused) {
+        upload.start()
+      }
+      if (!file.isRestored) {
+        this.uppy.emit('upload-started', file.id, upload)
+      }
     })
   }
 
@@ -198,7 +206,7 @@ module.exports = class Tus extends Plugin {
     this.resetUploaderReferences(file.id)
 
     return new Promise((resolve, reject) => {
-      this.core.log(file.remote.url)
+      this.uppy.log(file.remote.url)
       if (file.serverToken) {
         this.connectToServerSocket(file)
       } else {
@@ -207,7 +215,7 @@ module.exports = class Tus extends Plugin {
           endpoint = file.tus.endpoint
         }
 
-        this.core.emitter.emit('core:upload-started', file.id)
+        this.uppy.emit('upload-started', file.id)
 
         fetch(file.remote.url, {
           method: 'post',
@@ -224,15 +232,14 @@ module.exports = class Tus extends Plugin {
           }))
         })
         .then((res) => {
-          if (res.status < 200 && res.status > 300) {
+          if (res.status < 200 || res.status > 300) {
             return reject(res.statusText)
           }
 
           res.json().then((data) => {
             const token = data.token
+            this.uppy.setFileState(file.id, { serverToken: token })
             file = this.getFile(file.id)
-            file.serverToken = token
-            this.updateFile(file)
             this.connectToServerSocket(file)
             resolve()
           })
@@ -246,7 +253,7 @@ module.exports = class Tus extends Plugin {
     const host = getSocketHost(file.remote.host)
     const socket = new UppySocket({ target: `${host}/api/${token}` })
     this.uploaderSockets[file.id] = socket
-    this.uploaderEvents[file.id] = createEventTracker(this.core)
+    this.uploaderEvents[file.id] = createEventTracker(this.uppy)
 
     this.onFileRemove(file.id, () => socket.send('pause', {}))
 
@@ -275,23 +282,31 @@ module.exports = class Tus extends Plugin {
       socket.send('resume', {})
     })
 
+    if (file.isPaused) {
+      socket.send('pause', {})
+    }
+
     socket.on('progress', (progressData) => emitSocketProgress(this, progressData, file))
 
+    socket.on('error', (errData) => {
+      this.uppy.emit('core:upload-error', file.id, new Error(errData.error))
+    })
+
     socket.on('success', (data) => {
-      this.core.emitter.emit('core:upload-success', file.id, data, data.url)
+      this.uppy.emit('upload-success', file.id, data, data.url)
       this.resetUploaderReferences(file.id)
     })
   }
 
   getFile (fileID) {
-    return this.core.state.files[fileID]
+    return this.uppy.state.files[fileID]
   }
 
   updateFile (file) {
-    const files = Object.assign({}, this.core.state.files, {
+    const files = Object.assign({}, this.uppy.state.files, {
       [file.id]: file
     })
-    this.core.setState({ files })
+    this.uppy.setState({ files })
   }
 
   onReceiveUploadUrl (file, uploadURL) {
@@ -309,22 +324,22 @@ module.exports = class Tus extends Plugin {
   }
 
   onFileRemove (fileID, cb) {
-    this.uploaderEvents[fileID].on('core:file-removed', (targetFileID) => {
+    this.uploaderEvents[fileID].on('file-removed', (targetFileID) => {
       if (fileID === targetFileID) cb(targetFileID)
     })
   }
 
   onPause (fileID, cb) {
-    this.uploaderEvents[fileID].on('core:upload-pause', (targetFileID, isPaused) => {
+    this.uploaderEvents[fileID].on('upload-pause', (targetFileID, isPaused) => {
       if (fileID === targetFileID) {
-        // const isPaused = this.core.pauseResume(fileID)
+        // const isPaused = this.uppy.pauseResume(fileID)
         cb(isPaused)
       }
     })
   }
 
   onRetry (fileID, cb) {
-    this.uploaderEvents[fileID].on('core:upload-retry', (targetFileID) => {
+    this.uploaderEvents[fileID].on('upload-retry', (targetFileID) => {
       if (fileID === targetFileID) {
         cb()
       }
@@ -332,29 +347,29 @@ module.exports = class Tus extends Plugin {
   }
 
   onRetryAll (fileID, cb) {
-    this.uploaderEvents[fileID].on('core:retry-all', (filesToRetry) => {
-      if (!this.core.getFile(fileID)) return
+    this.uploaderEvents[fileID].on('retry-all', (filesToRetry) => {
+      if (!this.uppy.getFile(fileID)) return
       cb()
     })
   }
 
   onPauseAll (fileID, cb) {
-    this.uploaderEvents[fileID].on('core:pause-all', () => {
-      if (!this.core.getFile(fileID)) return
+    this.uploaderEvents[fileID].on('pause-all', () => {
+      if (!this.uppy.getFile(fileID)) return
       cb()
     })
   }
 
   onCancelAll (fileID, cb) {
-    this.uploaderEvents[fileID].on('core:cancel-all', () => {
-      if (!this.core.getFile(fileID)) return
+    this.uploaderEvents[fileID].on('cancel-all', () => {
+      if (!this.uppy.getFile(fileID)) return
       cb()
     })
   }
 
   onResumeAll (fileID, cb) {
-    this.uploaderEvents[fileID].on('core:resume-all', () => {
-      if (!this.core.getFile(fileID)) return
+    this.uploaderEvents[fileID].on('resume-all', () => {
+      if (!this.uppy.getFile(fileID)) return
       cb()
     })
   }
@@ -364,7 +379,9 @@ module.exports = class Tus extends Plugin {
       const current = parseInt(index, 10) + 1
       const total = files.length
 
-      if (!file.isRemote) {
+      if (file.error) {
+        return Promise.reject(new Error(file.error))
+      } else if (!file.isRemote) {
         return this.upload(file, current, total)
       } else {
         return this.uploadRemote(file, current, total)
@@ -376,40 +393,40 @@ module.exports = class Tus extends Plugin {
 
   handleUpload (fileIDs) {
     if (fileIDs.length === 0) {
-      this.core.log('Tus: no files to upload!')
+      this.uppy.log('Tus: no files to upload!')
       return Promise.resolve()
     }
 
-    this.core.log('Tus is uploading...')
-    const filesToUpload = fileIDs.map((fileID) => this.core.getFile(fileID))
+    this.uppy.log('Tus is uploading...')
+    const filesToUpload = fileIDs.map((fileID) => this.uppy.getFile(fileID))
 
     return this.uploadFiles(filesToUpload)
   }
 
   addResumableUploadsCapabilityFlag () {
-    const newCapabilities = Object.assign({}, this.core.getState().capabilities)
+    const newCapabilities = Object.assign({}, this.uppy.getState().capabilities)
     newCapabilities.resumableUploads = true
-    this.core.setState({
+    this.uppy.setState({
       capabilities: newCapabilities
     })
   }
 
   install () {
     this.addResumableUploadsCapabilityFlag()
-    this.core.addUploader(this.handleUpload)
+    this.uppy.addUploader(this.handleUpload)
 
-    this.core.on('core:reset-progress', this.handleResetProgress)
+    this.uppy.on('reset-progress', this.handleResetProgress)
 
     if (this.opts.autoRetry) {
-      this.core.on('back-online', this.core.retryAll)
+      this.uppy.on('back-online', this.uppy.retryAll)
     }
   }
 
   uninstall () {
-    this.core.removeUploader(this.handleUpload)
+    this.uppy.removeUploader(this.handleUpload)
 
     if (this.opts.autoRetry) {
-      this.core.off('back-online', this.core.retryAll)
+      this.uppy.off('back-online', this.uppy.retryAll)
     }
   }
 }

+ 0 - 5
src/plugins/Tus10.js

@@ -1,5 +0,0 @@
-const Tus = require('./Tus')
-
-console.warn('Using `uppy/lib/plugins/Tus10` is deprecated and will be removed in v0.22. Please use `uppy/lib/plugins/Tus` instead.')
-
-module.exports = Tus

+ 5 - 5
src/plugins/Webcam/CameraIcon.js

@@ -1,8 +1,8 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 
 module.exports = (props) => {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="100" height="77" viewBox="0 0 100 77">
-    <path d="M50 32c-7.168 0-13 5.832-13 13s5.832 13 13 13 13-5.832 13-13-5.832-13-13-13z"/>
-    <path d="M87 13H72c0-7.18-5.82-13-13-13H41c-7.18 0-13 5.82-13 13H13C5.82 13 0 18.82 0 26v38c0 7.18 5.82 13 13 13h74c7.18 0 13-5.82 13-13V26c0-7.18-5.82-13-13-13zM50 68c-12.683 0-23-10.318-23-23s10.317-23 23-23 23 10.318 23 23-10.317 23-23 23z"/>
-  </svg>`
+  return <svg aria-hidden="true" class="UppyIcon" width="100" height="77" viewBox="0 0 100 77">
+    <path d="M50 32c-7.168 0-13 5.832-13 13s5.832 13 13 13 13-5.832 13-13-5.832-13-13-13z" />
+    <path d="M87 13H72c0-7.18-5.82-13-13-13H41c-7.18 0-13 5.82-13 13H13C5.82 13 0 18.82 0 26v38c0 7.18 5.82 13 13 13h74c7.18 0 13-5.82 13-13V26c0-7.18-5.82-13-13-13zM50 68c-12.683 0-23-10.318-23-23s10.317-23 23-23 23 10.318 23 23-10.317 23-23 23z" />
+  </svg>
 }

+ 30 - 32
src/plugins/Webcam/CameraScreen.js

@@ -1,4 +1,4 @@
-const html = require('yo-yo')
+const { h, Component } = require('preact')
 const SnapshotButton = require('./SnapshotButton')
 const RecordButton = require('./RecordButton')
 
@@ -6,40 +6,38 @@ function isModeAvailable (modes, mode) {
   return modes.indexOf(mode) !== -1
 }
 
-module.exports = (props) => {
-  const src = props.src || ''
-  let video
-
-  if (props.useTheFlash) {
-    video = props.getSWFHTML()
-  } else {
-    video = html`<video class="UppyWebcam-video" autoplay muted src="${src}"></video>`
+class CameraScreen extends Component {
+  componentDidMount () {
+    this.props.onFocus()
+    this.btnContainer.firstChild.focus()
   }
 
-  const shouldShowRecordButton = props.supportsRecording && (
-    isModeAvailable(props.modes, 'video-only') ||
-    isModeAvailable(props.modes, 'audio-only') ||
-    isModeAvailable(props.modes, 'video-audio')
-  )
+  componentWillUnmount () {
+    this.props.onStop()
+  }
 
-  const shouldShowSnapshotButton = isModeAvailable(props.modes, 'picture')
+  render () {
+    const shouldShowRecordButton = this.props.supportsRecording && (
+      isModeAvailable(this.props.modes, 'video-only') ||
+      isModeAvailable(this.props.modes, 'audio-only') ||
+      isModeAvailable(this.props.modes, 'video-audio')
+    )
+    const shouldShowSnapshotButton = isModeAvailable(this.props.modes, 'picture')
 
-  return html`
-    <div class="UppyWebcam-container" onload=${(el) => {
-      props.onFocus()
-      const recordButton = el.querySelector('.UppyWebcam-recordButton')
-      if (recordButton) recordButton.focus()
-    }} onunload=${(el) => {
-      props.onStop()
-    }}>
-      <div class='UppyWebcam-videoContainer'>
-        ${video}
-      </div>
-      <div class='UppyWebcam-buttonContainer'>
-        ${shouldShowRecordButton ? RecordButton(props) : null}
-        ${shouldShowSnapshotButton ? SnapshotButton(props) : null}
+    return (
+      <div class="uppy uppy-Webcam-container">
+        <div class="uppy-Webcam-videoContainer">
+          <video class="uppy-Webcam-video" autoplay muted src={this.props.src || ''} />
+        </div>
+        <div class="uppy-Webcam-buttonContainer" ref={(el) => { this.btnContainer = el }}>
+          {shouldShowSnapshotButton ? SnapshotButton(this.props) : null}
+          {' '}
+          {shouldShowRecordButton ? RecordButton(this.props) : null}
+        </div>
+        <canvas class="uppy-Webcam-canvas" style="display: none;" />
       </div>
-      <canvas class="UppyWebcam-canvas" style="display: none;"></canvas>
-    </div>
-  `
+    )
+  }
 }
+
+module.exports = CameraScreen

+ 4 - 4
src/plugins/Webcam/PermissionsScreen.js

@@ -1,11 +1,11 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 
 module.exports = (props) => {
-  return html`
+  return (
     <div class="uppy-Webcam-permissons">
       <h1>Please allow access to your camera</h1>
-      <p>You have been prompted to allow camera access from this site.<br>
+      <p>You have been prompted to allow camera access from this site.<br />
       In order to take pictures with your camera you must approve this request.</p>
     </div>
-  `
+  )
 }

+ 11 - 11
src/plugins/Webcam/RecordButton.js

@@ -1,27 +1,27 @@
-const html = require('yo-yo')
 const RecordStartIcon = require('./RecordStartIcon')
 const RecordStopIcon = require('./RecordStopIcon')
+const { h } = require('preact')
 
 module.exports = function RecordButton ({ recording, onStartRecording, onStopRecording }) {
   if (recording) {
-    return html`
-      <button class="UppyButton--circular UppyButton--red UppyButton--sizeM UppyWebcam-recordButton"
+    return (
+      <button class="UppyButton--circular UppyButton--red UppyButton--sizeM uppy-Webcam-recordButton"
         type="button"
         title="Stop Recording"
         aria-label="Stop Recording"
-        onclick=${onStopRecording}>
-        ${RecordStopIcon()}
+        onclick={onStopRecording}>
+        {RecordStopIcon()}
       </button>
-    `
+    )
   }
 
-  return html`
-    <button class="UppyButton--circular UppyButton--red UppyButton--sizeM UppyWebcam-recordButton"
+  return (
+    <button class="UppyButton--circular UppyButton--red UppyButton--sizeM uppy-Webcam-recordButton"
       type="button"
       title="Begin Recording"
       aria-label="Begin Recording"
-      onclick=${onStartRecording}>
-      ${RecordStartIcon()}
+      onclick={onStartRecording}>
+      {RecordStartIcon()}
     </button>
-  `
+  )
 }

+ 3 - 3
src/plugins/Webcam/RecordStartIcon.js

@@ -1,7 +1,7 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 
 module.exports = (props) => {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="100" height="100" viewBox="0 0 100 100">
+  return <svg aria-hidden="true" class="UppyIcon" width="100" height="100" viewBox="0 0 100 100">
     <circle cx="50" cy="50" r="40" />
-  </svg>`
+  </svg>
 }

+ 3 - 3
src/plugins/Webcam/RecordStopIcon.js

@@ -1,7 +1,7 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 
 module.exports = (props) => {
-  return html`<svg aria-hidden="true" class="UppyIcon" width="100" height="100" viewBox="0 0 100 100">
+  return <svg aria-hidden="true" class="UppyIcon" width="100" height="100" viewBox="0 0 100 100">
     <rect x="15" y="15" width="70" height="70" />
-  </svg>`
+  </svg>
 }

+ 7 - 7
src/plugins/Webcam/SnapshotButton.js

@@ -1,14 +1,14 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 const CameraIcon = require('./CameraIcon')
 
-module.exports = function SnapshotButton ({ onSnapshot }) {
-  return html`
-    <button class="UppyButton--circular UppyButton--red UppyButton--sizeM UppyWebcam-recordButton"
+module.exports = ({ onSnapshot }) => {
+  return (
+    <button class="UppyButton--circular UppyButton--red UppyButton--sizeM uppy-Webcam-recordButton"
       type="button"
       title="Take a snapshot"
       aria-label="Take a snapshot"
-      onclick=${onSnapshot}>
-      ${CameraIcon()}
+      onclick={onSnapshot}>
+      {CameraIcon()}
     </button>
-  `
+  )
 }

+ 5 - 5
src/plugins/Webcam/WebcamIcon.js

@@ -1,10 +1,10 @@
-const html = require('yo-yo')
+const { h } = require('preact')
 
 module.exports = (props) => {
-  return html`
+  return (
     <svg aria-hidden="true" class="UppyIcon" width="18" height="21" viewBox="0 0 18 21">
-      <path d="M14.8 16.9c1.9-1.7 3.2-4.1 3.2-6.9 0-5-4-9-9-9s-9 4-9 9c0 2.8 1.2 5.2 3.2 6.9C1.9 17.9.5 19.4 0 21h3c1-1.9 11-1.9 12 0h3c-.5-1.6-1.9-3.1-3.2-4.1zM9 4c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6z"/>
-      <path d="M9 14c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zM8 8c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1c0-.5.4-1 1-1z"/>
+      <path d="M14.8 16.9c1.9-1.7 3.2-4.1 3.2-6.9 0-5-4-9-9-9s-9 4-9 9c0 2.8 1.2 5.2 3.2 6.9C1.9 17.9.5 19.4 0 21h3c1-1.9 11-1.9 12 0h3c-.5-1.6-1.9-3.1-3.2-4.1zM9 4c3.3 0 6 2.7 6 6s-2.7 6-6 6-6-2.7-6-6 2.7-6 6-6z" />
+      <path d="M9 14c2.2 0 4-1.8 4-4s-1.8-4-4-4-4 1.8-4 4 1.8 4 4 4zM8 8c.6 0 1 .4 1 1s-.4 1-1 1-1-.4-1-1c0-.5.4-1 1-1z" />
     </svg>
-  `
+  )
 }

+ 46 - 30
src/plugins/Webcam/index.js

@@ -1,4 +1,5 @@
-const Plugin = require('../Plugin')
+const { h } = require('preact')
+const Plugin = require('../../core/Plugin')
 const Translator = require('../../core/Translator')
 const {
   getFileTypeExtension,
@@ -16,7 +17,7 @@ function getMediaDevices () {
     return navigator.mediaDevices
   }
 
-  let getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia
+  const getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia
   if (!getUserMedia) {
     return null
   }
@@ -34,16 +35,15 @@ function getMediaDevices () {
  * Webcam
  */
 module.exports = class Webcam extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.mediaDevices = getMediaDevices()
     this.supportsUserMedia = !!this.mediaDevices
     this.protocol = location.protocol.match(/https/i) ? 'https' : 'http'
-    this.type = 'acquirer'
-    this.id = 'Webcam'
+    this.id = this.opts.id || 'Webcam'
     this.title = 'Webcam'
+    this.type = 'acquirer'
     this.icon = WebcamIcon
-    this.focus = this.focus.bind(this)
 
     const defaultLocale = {
       strings: {
@@ -86,6 +86,7 @@ module.exports = class Webcam extends Plugin {
     this.startRecording = this.startRecording.bind(this)
     this.stopRecording = this.stopRecording.bind(this)
     this.oneTwoThreeSmile = this.oneTwoThreeSmile.bind(this)
+    this.focus = this.focus.bind(this)
 
     this.webcamActive = false
 
@@ -94,27 +95,37 @@ module.exports = class Webcam extends Plugin {
     }
   }
 
-  start () {
-    if (!this.mediaDevices) {
-      return Promise.reject(new Error('Webcam access not supported'))
-    }
-
-    this.webcamActive = true
+  isSupported () {
+    return !!this.mediaDevices
+  }
 
+  getConstraints () {
     const acceptsAudio = this.opts.modes.indexOf('video-audio') !== -1 ||
       this.opts.modes.indexOf('audio-only') !== -1
     const acceptsVideo = this.opts.modes.indexOf('video-audio') !== -1 ||
       this.opts.modes.indexOf('video-only') !== -1 ||
       this.opts.modes.indexOf('picture') !== -1
 
+    return {
+      audio: acceptsAudio,
+      video: acceptsVideo
+    }
+  }
+
+  start () {
+    if (!this.isSupported()) {
+      return Promise.reject(new Error('Webcam access not supported'))
+    }
+
+    this.webcamActive = true
+
+    const constraints = this.getConstraints()
+
     // ask user for access to their camera
-    return this.mediaDevices
-      .getUserMedia({
-        audio: acceptsAudio,
-        video: acceptsVideo
-      })
+    return this.mediaDevices.getUserMedia(constraints)
       .then((stream) => {
         this.stream = stream
+        console.log(stream)
         this.streamSrc = URL.createObjectURL(this.stream)
         this.setPluginState({
           cameraReady: true
@@ -157,11 +168,13 @@ module.exports = class Webcam extends Plugin {
         isRecording: false
       })
       return this.getVideo()
-    }).then((file) => {
-      return this.core.addFile(file)
-    }).then(() => {
+    })
+    .then(this.uppy.addFile)
+    .then(() => {
       this.recordingChunks = null
       this.recorder = null
+      const dashboard = this.uppy.getPlugin('Dashboard')
+      if (dashboard) dashboard.hideAllPanels()
     }, (error) => {
       this.recordingChunks = null
       this.recorder = null
@@ -182,7 +195,7 @@ module.exports = class Webcam extends Plugin {
   }
 
   getVideoElement () {
-    return this.target.querySelector('.UppyWebcam-video')
+    return this.el.querySelector('.uppy-Webcam-video')
   }
 
   oneTwoThreeSmile () {
@@ -197,11 +210,11 @@ module.exports = class Webcam extends Plugin {
         }
 
         if (count > 0) {
-          this.core.info(`${count}...`, 'warning', 800)
+          this.uppy.info(`${count}...`, 'warning', 800)
           count--
         } else {
           clearInterval(countDown)
-          this.core.info(this.i18n('smile'), 'success', 1500)
+          this.uppy.info(this.i18n('smile'), 'success', 1500)
           setTimeout(() => resolve(), 1500)
         }
       }, 1000)
@@ -214,13 +227,15 @@ module.exports = class Webcam extends Plugin {
 
     this.opts.onBeforeSnapshot().catch((err) => {
       const message = typeof err === 'object' ? err.message : err
-      this.core.info(message, 'error', 5000)
+      this.uppy.info(message, 'error', 5000)
       return Promise.reject(new Error(`onBeforeSnapshot: ${message}`))
     }).then(() => {
       return this.getImage()
     }).then((tagFile) => {
       this.captureInProgress = false
-      this.core.addFile(tagFile)
+      this.uppy.addFile(tagFile)
+      const dashboard = this.uppy.getPlugin('Dashboard')
+      if (dashboard) dashboard.hideAllPanels()
     }, (error) => {
       this.captureInProgress = false
       throw error
@@ -274,7 +289,7 @@ module.exports = class Webcam extends Plugin {
   focus () {
     if (this.opts.countdown) return
     setTimeout(() => {
-      this.core.info(this.i18n('smile'), 'success', 1500)
+      this.uppy.info(this.i18n('smile'), 'success', 1500)
     }, 1000)
   }
 
@@ -289,7 +304,7 @@ module.exports = class Webcam extends Plugin {
       return PermissionsScreen(webcamState)
     }
 
-    return CameraScreen(Object.assign({}, webcamState, {
+    return h(CameraScreen, Object.assign({}, webcamState, {
       onSnapshot: this.takeSnapshot,
       onStartRecording: this.startRecording,
       onStopRecording: this.stopRecording,
@@ -308,8 +323,9 @@ module.exports = class Webcam extends Plugin {
     })
 
     const target = this.opts.target
-    const plugin = this
-    this.target = this.mount(target, plugin)
+    if (target) {
+      this.mount(target, this)
+    }
   }
 
   uninstall () {

+ 51 - 35
src/plugins/XHRUpload.js

@@ -1,16 +1,17 @@
-const Plugin = require('./Plugin')
+const Plugin = require('../core/Plugin')
 const cuid = require('cuid')
 const Translator = require('../core/Translator')
 const UppySocket = require('../core/UppySocket')
 const {
   emitSocketProgress,
   getSocketHost,
-  settle
+  settle,
+  limitPromises
 } = require('../core/Utils')
 
 module.exports = class XHRUpload extends Plugin {
-  constructor (core, opts) {
-    super(core, opts)
+  constructor (uppy, opts) {
+    super(uppy, opts)
     this.type = 'uploader'
     this.id = 'XHRUpload'
     this.title = 'XHRUpload'
@@ -32,6 +33,7 @@ module.exports = class XHRUpload extends Plugin {
       headers: {},
       locale: defaultLocale,
       timeout: 30 * 1000,
+      limit: 0,
       getResponseData (xhr) {
         return JSON.parse(xhr.response)
       },
@@ -50,18 +52,25 @@ module.exports = class XHRUpload extends Plugin {
     this.i18n = this.translator.translate.bind(this.translator)
 
     this.handleUpload = this.handleUpload.bind(this)
+
+    // Simultaneous upload limiting is shared across all uploads with this plugin.
+    if (typeof this.opts.limit === 'number' && this.opts.limit !== 0) {
+      this.limitUploads = limitPromises(this.opts.limit)
+    } else {
+      this.limitUploads = (fn) => fn
+    }
   }
 
   getOptions (file) {
     const opts = Object.assign({},
       this.opts,
-      this.core.state.xhrUpload || {},
+      this.uppy.state.xhrUpload || {},
       file.xhrUpload || {}
     )
     opts.headers = {}
     Object.assign(opts.headers, this.opts.headers)
-    if (this.core.state.xhrUpload) {
-      Object.assign(opts.headers, this.core.state.xhrUpload.headers)
+    if (this.uppy.state.xhrUpload) {
+      Object.assign(opts.headers, this.uppy.state.xhrUpload.headers)
     }
     if (file.xhrUpload) {
       Object.assign(opts.headers, file.xhrUpload.headers)
@@ -93,7 +102,7 @@ module.exports = class XHRUpload extends Plugin {
   upload (file, current, total) {
     const opts = this.getOptions(file)
 
-    this.core.log(`uploading ${current} of ${total}`)
+    this.uppy.log(`uploading ${current} of ${total}`)
     return new Promise((resolve, reject) => {
       const data = opts.formData
         ? this.createFormDataUpload(file, opts)
@@ -101,9 +110,9 @@ module.exports = class XHRUpload extends Plugin {
 
       const onTimedOut = () => {
         xhr.abort()
-        this.core.log(`[XHRUpload] ${id} timed out`)
+        this.uppy.log(`[XHRUpload] ${id} timed out`)
         const error = new Error(this.i18n('timedOut', { seconds: Math.ceil(opts.timeout / 1000) }))
-        this.core.emit('core:upload-error', file.id, error)
+        this.uppy.emit('upload-error', file.id, error)
         reject(error)
       }
       let aliveTimer
@@ -116,7 +125,7 @@ module.exports = class XHRUpload extends Plugin {
       const id = cuid()
 
       xhr.upload.addEventListener('loadstart', (ev) => {
-        this.core.log(`[XHRUpload] ${id} started`)
+        this.uppy.log(`[XHRUpload] ${id} started`)
         if (opts.timeout > 0) {
           // Begin checking for timeouts when loading starts.
           isAlive()
@@ -124,13 +133,13 @@ module.exports = class XHRUpload extends Plugin {
       })
 
       xhr.upload.addEventListener('progress', (ev) => {
-        this.core.log(`[XHRUpload] ${id} progress: ${ev.loaded} / ${ev.total}`)
+        this.uppy.log(`[XHRUpload] ${id} progress: ${ev.loaded} / ${ev.total}`)
         if (opts.timeout > 0) {
           isAlive()
         }
 
         if (ev.lengthComputable) {
-          this.core.emit('core:upload-progress', {
+          this.uppy.emit('upload-progress', {
             uploader: this,
             id: file.id,
             bytesUploaded: ev.loaded,
@@ -140,34 +149,34 @@ module.exports = class XHRUpload extends Plugin {
       })
 
       xhr.addEventListener('load', (ev) => {
-        this.core.log(`[XHRUpload] ${id} finished`)
+        this.uppy.log(`[XHRUpload] ${id} finished`)
         clearTimeout(aliveTimer)
 
         if (ev.target.status >= 200 && ev.target.status < 300) {
           const resp = opts.getResponseData(xhr)
           const uploadURL = resp[opts.responseUrlFieldName]
 
-          this.core.emit('core:upload-success', file.id, resp, uploadURL)
+          this.uppy.emit('upload-success', file.id, resp, uploadURL)
 
           if (uploadURL) {
-            this.core.log(`Download ${file.name} from ${file.uploadURL}`)
+            this.uppy.log(`Download ${file.name} from ${file.uploadURL}`)
           }
 
           return resolve(file)
         } else {
           const error = opts.getResponseError(xhr) || new Error('Upload error')
           error.request = xhr
-          this.core.emit('core:upload-error', file.id, error)
+          this.uppy.emit('upload-error', file.id, error)
           return reject(error)
         }
       })
 
       xhr.addEventListener('error', (ev) => {
-        this.core.log(`[XHRUpload] ${id} errored`)
+        this.uppy.log(`[XHRUpload] ${id} errored`)
         clearTimeout(aliveTimer)
 
         const error = opts.getResponseError(xhr) || new Error('Upload error')
-        this.core.emit('core:upload-error', file.id, error)
+        this.uppy.emit('upload-error', file.id, error)
         return reject(error)
       })
 
@@ -179,26 +188,26 @@ module.exports = class XHRUpload extends Plugin {
 
       xhr.send(data)
 
-      this.core.on('core:upload-cancel', (fileID) => {
+      this.uppy.on('upload-cancel', (fileID) => {
         if (fileID === file.id) {
           xhr.abort()
         }
       })
 
-      this.core.on('core:cancel-all', () => {
-        // const files = this.core.getState().files
+      this.uppy.on('cancel-all', () => {
+        // const files = this.uppy.getState().files
         // if (!files[file.id]) return
         xhr.abort()
       })
 
-      this.core.emit('core:upload-started', file.id)
+      this.uppy.emit('upload-started', file.id)
     })
   }
 
   uploadRemote (file, current, total) {
     const opts = this.getOptions(file)
     return new Promise((resolve, reject) => {
-      this.core.emit('core:upload-started', file.id)
+      this.uppy.emit('upload-started', file.id)
 
       const fields = {}
       const metaFields = Array.isArray(opts.metaFields)
@@ -207,7 +216,7 @@ module.exports = class XHRUpload extends Plugin {
         : Object.keys(file.meta)
 
       metaFields.forEach((name) => {
-        fields[name] = file.meta.name
+        fields[name] = file.meta[name]
       })
 
       fetch(file.remote.url, {
@@ -238,7 +247,7 @@ module.exports = class XHRUpload extends Plugin {
           socket.on('progress', (progressData) => emitSocketProgress(this, progressData, file))
 
           socket.on('success', (data) => {
-            this.core.emit('core:upload-success', file.id, data, data.url)
+            this.uppy.emit('upload-success', file.id, data, data.url)
             socket.close()
             return resolve()
           })
@@ -248,40 +257,47 @@ module.exports = class XHRUpload extends Plugin {
   }
 
   uploadFiles (files) {
-    const promises = files.map((file, i) => {
+    const actions = files.map((file, i) => {
       const current = parseInt(i, 10) + 1
       const total = files.length
 
-      if (file.isRemote) {
-        return this.uploadRemote(file, current, total)
+      if (file.error) {
+        return () => Promise.reject(new Error(file.error))
+      } else if (file.isRemote) {
+        return this.uploadRemote.bind(this, file, current, total)
       } else {
-        return this.upload(file, current, total)
+        return this.upload.bind(this, file, current, total)
       }
     })
 
+    const promises = actions.map((action) => {
+      const limitedAction = this.limitUploads(action)
+      return limitedAction()
+    })
+
     return settle(promises)
   }
 
   handleUpload (fileIDs) {
     if (fileIDs.length === 0) {
-      this.core.log('[XHRUpload] No files to upload!')
+      this.uppy.log('[XHRUpload] No files to upload!')
       return Promise.resolve()
     }
 
-    this.core.log('[XHRUpload] Uploading...')
+    this.uppy.log('[XHRUpload] Uploading...')
     const files = fileIDs.map(getFile, this)
     function getFile (fileID) {
-      return this.core.state.files[fileID]
+      return this.uppy.state.files[fileID]
     }
 
     return this.uploadFiles(files).then(() => null)
   }
 
   install () {
-    this.core.addUploader(this.handleUpload)
+    this.uppy.addUploader(this.handleUpload)
   }
 
   uninstall () {
-    this.core.removeUploader(this.handleUpload)
+    this.uppy.removeUploader(this.handleUpload)
   }
 }

+ 1 - 1
src/react/Dashboard.js

@@ -1,6 +1,6 @@
 const React = require('react')
 const PropTypes = require('prop-types')
-const UppyCore = require('../core/Core')
+const UppyCore = require('../core/Core').Uppy
 const DashboardPlugin = require('../plugins/Dashboard')
 
 const h = React.createElement

+ 1 - 1
src/react/DashboardModal.js

@@ -1,6 +1,6 @@
 const React = require('react')
 const PropTypes = require('prop-types')
-const UppyCore = require('../core/Core')
+const UppyCore = require('../core/Core').Uppy
 const DashboardPlugin = require('../plugins/Dashboard')
 
 const h = React.createElement

+ 1 - 1
src/react/DragDrop.js

@@ -1,6 +1,6 @@
 const React = require('react')
 const PropTypes = require('prop-types')
-const UppyCore = require('../core')
+const UppyCore = require('../core').Uppy
 const DragDropPlugin = require('../plugins/DragDrop')
 
 const h = React.createElement

+ 1 - 1
src/react/ProgressBar.js

@@ -1,6 +1,6 @@
 const React = require('react')
 const PropTypes = require('prop-types')
-const UppyCore = require('../core')
+const UppyCore = require('../core').Uppy
 const ProgressBarPlugin = require('../plugins/ProgressBar')
 
 const h = React.createElement

Some files were not shown because too many files changed in this diff