Kaynağa Gözat

Merge pull request #1189 from weseek/master

release v3.5.12
Yuki Takei 6 yıl önce
ebeveyn
işleme
e47edd0f60

+ 1 - 0
.gitignore

@@ -19,6 +19,7 @@ package-lock.json
 # Dist #
 /report/
 /public/*.chunk.js
+/public/*.chunk.js.LICENSE
 /public/*.bundle.js
 /public/manifest.json
 /public/dll

+ 10 - 1
CHANGES.md

@@ -1,6 +1,15 @@
 # CHANGES
 
-## 3.5.11-RC
+## 3.5.12-RC
+
+* Improvement: Use Elasticsearch Alias
+* Improvement: Connect to HTTPS PlantUML URL in default
+* Fix: Global Notification doesn't work after updating Webhook URL
+* Fix: User Trigger Notification is not be sent when channel is not specified
+* Support: Upgrade libs
+    * terser-webpack-plugin
+
+## 3.5.11
 
 * Fix: HackMD Editor shows 404 error when HackMD redirect to fqdn URI
     * Introduced by 3.5.8

+ 1 - 1
README.md

@@ -258,7 +258,7 @@ License
 =======
 
 * The MIT License (MIT)
-* See LICENSE file.
+* See [LICENSE](https://github.com/weseek/growi/blob/master/LICENSE) and [THIRD-PARTY-NOTICES.md](https://github.com/weseek/growi/blob/master/THIRD-PARTY-NOTICES.md).
 
 
 [crowi]: https://github.com/crowi/crowi

+ 11 - 0
THIRD-PARTY-NOTICES.md

@@ -16,6 +16,7 @@ https://github.com/weseek/growi.
 2. crowi/crowi (https://github.com/crowi/crowi)
 3. Microsoft/vscode (https://github.com/Microsoft/vscode)
 4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
+5. EmojiOne Version 3 (https://github.com/joypixels/emojione/tree/v3.1.1)
 
 
 License Notice for Apache License, Version 2.0 Derivative Works
@@ -97,3 +98,13 @@ https://creativecommons.org/licenses/by-sa/3.0/
 ```
 Copyright (c) 2018 Stephen Hutchings
 ```
+
+
+License Notice for EmojiOne
+------------------------
+
+https://creativecommons.org/licenses/by/4.0/
+
+```
+author: "EmojiOne <ryan@emojione.com> (http://emojione.com)"
+```

+ 3 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.5.11-RC",
+  "version": "3.5.12-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -66,6 +66,7 @@
       "mongoose: somehow GlobalNotificationSetting CRUD does not work with mongoose v5.6.0",
       "openid-client: Node.js 12 or higher is required for openid-client@3 and above."
     ],
+    "archiver": "^3.1.1",
     "async": "^3.0.1",
     "aws-sdk": "^2.88.0",
     "axios": "^0.19.0",
@@ -220,7 +221,7 @@
     "style-loader": "^1.0.0",
     "stylelint-config-recess-order": "^2.0.1",
     "swagger-jsdoc": "^3.2.9",
-    "terser-webpack-plugin": "^1.2.2",
+    "terser-webpack-plugin": "^2.0.1",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "unstated": "^2.1.1",

+ 1 - 0
resource/locales/en-US/translation.json

@@ -112,6 +112,7 @@
   "UserGroup Management": "UserGroup Management",
   "Full Text Search Management": "Full Text Search Management",
   "Import Data": "Import Data",
+  "Export Data": "Export Data",
   "Basic Settings": "Basic Settings",
   "Basic authentication": "Basic authentication",
   "Register limitation": "Register limitation",

+ 1 - 0
resource/locales/ja/translation.json

@@ -111,6 +111,7 @@
   "UserGroup Management": "グループ管理",
   "Full Text Search Management": "全文検索管理",
   "Import Data": "データインポート",
+  "Export Data": "データエクスポート",
   "Basic Settings": "基本設定",
   "Register limitation": "登録の制限",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",

+ 15 - 0
src/client/js/app.jsx

@@ -38,6 +38,7 @@ import CustomCssEditor from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import AdminRebuildSearch from './components/Admin/AdminRebuildSearch';
+import ExportPage from './components/Admin/Export/ExportPage';
 import GroupDeleteModal from './components/GroupDeleteModal/GroupDeleteModal';
 
 import AppContainer from './services/AppContainer';
@@ -187,6 +188,20 @@ if (adminGrantSelectorElem != null) {
   );
 }
 
+const adminExportPageElem = document.getElementById('admin-export-page');
+if (adminExportPageElem != null) {
+  ReactDOM.render(
+    <Provider inject={[]}>
+      <I18nextProvider i18n={i18n}>
+        <ExportPage
+          crowi={appContainer}
+        />
+      </I18nextProvider>
+    </Provider>,
+    adminExportPageElem,
+  );
+}
+
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(

+ 100 - 0
src/client/js/components/Admin/Export/ExportAsZip.jsx

@@ -0,0 +1,100 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+// import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+class ExportPage extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      files: {},
+    };
+
+    this.createZipFile = this.createZipFile.bind(this);
+    this.deleteZipFile = this.deleteZipFile.bind(this);
+  }
+
+  async componentDidMount() {
+    const res = await this.props.appContainer.apiGet('/v3/export', {});
+
+    this.setState({ files: res.files });
+  }
+
+  async createZipFile() {
+    // TODO use appContainer.apiv3.post
+    const res = await this.props.appContainer.apiPost('/v3/export/pages', {});
+    // TODO toastSuccess, toastError
+    this.setState((prevState) => {
+      return {
+        files: {
+          ...prevState.files,
+          [res.collection]: res.file,
+        },
+      };
+    });
+  }
+
+  async deleteZipFile() {
+    // TODO use appContainer.apiv3.delete
+    // TODO toastSuccess, toastError
+  }
+
+  render() {
+    // const { t } = this.props;
+
+    return (
+      <Fragment>
+        <h2>Export Data as Zip</h2>
+        <form className="my-5">
+          {Object.keys(this.state.files).map((file) => {
+            const disabled = file !== 'pages';
+            return (
+              <div className="form-check" key={file}>
+                <input
+                  type="radio"
+                  id={file}
+                  name="collection"
+                  className="form-check-input"
+                  value={file}
+                  disabled={disabled}
+                  checked={!disabled}
+                  onChange={() => {}}
+                />
+                <label className={`form-check-label ml-3 ${disabled ? 'text-muted' : ''}`} htmlFor={file}>
+                  {file} ({this.state.files[file] || 'not found'})
+                </label>
+              </div>
+            );
+          })}
+        </form>
+        <button type="button" className="btn btn-sm btn-default" onClick={this.createZipFile}>Generate</button>
+        <a href="/_api/v3/export/pages">
+          <button type="button" className="btn btn-sm btn-primary ml-2">Download</button>
+        </a>
+        {/* <button type="button" className="btn btn-sm btn-danger ml-2" onClick={this.deleteZipFile}>Clear</button> */}
+      </Fragment>
+    );
+  }
+
+}
+
+ExportPage.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  isAclEnabled: PropTypes.bool,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const ExportPageWrapper = (props) => {
+  return createSubscribedElement(ExportPage, props, [AppContainer]);
+};
+
+export default withTranslation()(ExportPageWrapper);

+ 17 - 0
src/client/js/components/Admin/Export/ExportPage.jsx

@@ -0,0 +1,17 @@
+import React, { Fragment } from 'react';
+
+import ExportAsZip from './ExportAsZip';
+
+class ExportPage extends React.Component {
+
+  render() {
+    return (
+      <Fragment>
+        <ExportAsZip />
+      </Fragment>
+    );
+  }
+
+}
+
+export default ExportPage;

+ 1 - 1
src/client/js/util/markdown-it/plantuml.js

@@ -7,7 +7,7 @@ export default class PlantUMLConfigurer {
     this.crowi = crowi;
     const config = crowi.getConfig();
 
-    this.serverUrl = config.env.PLANTUML_URI || 'http://plantuml.com/plantuml';
+    this.serverUrl = config.env.PLANTUML_URI || 'https://plantuml.com/plantuml';
 
     this.generateSource = this.generateSource.bind(this);
   }

+ 20 - 15
src/server/crowi/index.js

@@ -46,6 +46,7 @@ function Crowi(rootdir) {
   this.appService = null;
   this.fileUploadService = null;
   this.restQiitaAPIService = null;
+  this.exportService = null;
   this.cdnResourcesService = new CdnResourcesService();
   this.interceptorManager = new InterceptorManager();
   this.xss = new Xss();
@@ -103,6 +104,7 @@ Crowi.prototype.init = async function() {
     this.setUpCustomize(),
     this.setUpRestQiitaAPI(),
     this.setupUserGroup(),
+    this.setupExport(),
   ]);
 
   // globalNotification depends on slack and mailer
@@ -321,18 +323,17 @@ Crowi.prototype.setupSearcher = async function() {
   const searcherUri = this.env.ELASTICSEARCH_URI
     || this.env.BONSAI_URL
     || null;
-  return new Promise(((resolve, reject) => {
-    if (searcherUri) {
-      try {
-        self.searcher = new (require(path.join(self.libDir, 'util', 'search')))(self, searcherUri);
-      }
-      catch (e) {
-        logger.error('Error on setup searcher', e);
-        self.searcher = null;
-      }
+
+  if (searcherUri) {
+    try {
+      self.searcher = new (require(path.join(self.libDir, 'util', 'search')))(self, searcherUri);
+      self.searcher.initIndices();
     }
-    resolve();
-  }));
+    catch (e) {
+      logger.error('Error on setup searcher', e);
+      self.searcher = null;
+    }
+  }
 };
 
 Crowi.prototype.setupMailer = async function() {
@@ -347,10 +348,7 @@ Crowi.prototype.setupSlack = async function() {
   const self = this;
 
   return new Promise(((resolve, reject) => {
-    if (this.slackNotificationService.hasSlackConfig()) {
-      self.slack = require('../util/slack')(self);
-    }
-
+    self.slack = require('../util/slack')(self);
     resolve();
   }));
 };
@@ -536,4 +534,11 @@ Crowi.prototype.setupUserGroup = async function() {
   }
 };
 
+Crowi.prototype.setupExport = async function() {
+  const ExportService = require('../service/export');
+  if (this.exportService == null) {
+    this.exportService = new ExportService(this);
+  }
+};
+
 module.exports = Crowi;

+ 8 - 26
src/server/routes/admin.js

@@ -868,6 +868,12 @@ module.exports = function(crowi, app) {
     });
   };
 
+  // Export management
+  actions.export = {};
+  actions.export.index = (req, res) => {
+    return res.render('admin/export');
+  };
+
   actions.api = {};
   actions.api.appSetting = async function(req, res) {
     const form = req.form.settingForm;
@@ -1366,38 +1372,14 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('ElasticSearch Integration is not set up.'));
     }
 
-    // first, delete index
-    try {
-      await search.deleteIndex();
-    }
-    catch (err) {
-      logger.warn('Delete index Error, but if it is initialize, its ok.', err);
-    }
-
-    // second, create index
-    try {
-      await search.buildIndex();
-    }
-    catch (err) {
-      logger.error('Error', err);
-      return res.json(ApiResponse.error(err));
-    }
-
     searchEvent.on('addPageProgress', (total, current, skip) => {
       crowi.getIo().sockets.emit('admin:addPageProgress', { total, current, skip });
     });
     searchEvent.on('finishAddPage', (total, current, skip) => {
       crowi.getIo().sockets.emit('admin:finishAddPage', { total, current, skip });
     });
-    // add all page
-    search
-      .addAllPages()
-      .then(() => {
-        debug('Data is successfully indexed. ------------------ ✧✧');
-      })
-      .catch((err) => {
-        logger.error('Error', err);
-      });
+
+    await search.buildIndex();
 
     return res.json(ApiResponse.success());
   };

+ 138 - 0
src/server/routes/apiv3/export.js

@@ -0,0 +1,138 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:export'); // eslint-disable-line no-unused-vars
+const path = require('path');
+
+const express = require('express');
+
+const router = express.Router();
+
+/**
+ * @swagger
+ *  tags:
+ *    name: Export
+ */
+
+module.exports = (crowi) => {
+  const { exportService } = crowi;
+  const { Page } = crowi.models;
+
+  /**
+   * @swagger
+   *
+   *  /export:
+   *    get:
+   *      tags: [Export]
+   *      description: get mongodb collections names and zip files for them
+   *      produces:
+   *        - application/json
+   *      responses:
+   *        200:
+   *          description: export cache info
+   *          content:
+   *            application/json:
+   */
+  router.get('/', async(req, res) => {
+    const files = exportService.getStatus();
+
+    // TODO: use res.apiv3
+    return res.json({ ok: true, files });
+  });
+
+  /**
+   * @swagger
+   *
+   *  /export/pages:
+   *    get:
+   *      tags: [Export]
+   *      description: download a zipped json for page collection
+   *      produces:
+   *        - application/json
+   *      responses:
+   *        200:
+   *          description: a zip file
+   *          content:
+   *            application/zip:
+   */
+  router.get('/pages', async(req, res) => {
+    // TODO: rename path to "/:collection" and add express validator
+    try {
+      const file = exportService.getZipFile(Page);
+
+      if (file == null) {
+        throw new Error('the target file does not exist');
+      }
+
+      return res.download(file);
+    }
+    catch (err) {
+      // TODO: use ApiV3Error
+      logger.error(err);
+      return res.status(500).send({ status: 'ERROR' });
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *  /export/pages:
+   *    post:
+   *      tags: [Export]
+   *      description: generate a zipped json for page collection
+   *      produces:
+   *        - application/json
+   *      responses:
+   *        200:
+   *          description: a zip file is generated
+   *          content:
+   *            application/json:
+   */
+  router.post('/pages', async(req, res) => {
+    // TODO: rename path to "/:collection" and add express validator
+    try {
+      const file = await exportService.exportCollection(Page);
+      // TODO: use res.apiv3
+      return res.status(200).json({
+        ok: true,
+        collection: [Page.collection.collectionName],
+        file: path.basename(file),
+      });
+    }
+    catch (err) {
+      // TODO: use ApiV3Error
+      logger.error(err);
+      return res.status(500).send({ status: 'ERROR' });
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *  /export/pages:
+   *    delete:
+   *      tags: [Export]
+   *      description: unlink a json and zip file for page collection
+   *      produces:
+   *        - application/json
+   *      responses:
+   *        200:
+   *          description: the json and zip file are removed
+   *          content:
+   *            application/json:
+   */
+  // router.delete('/pages', async(req, res) => {
+  //   // TODO: rename path to "/:collection" and add express validator
+  //   try {
+  //     // remove .json and .zip for collection
+  //     // TODO: use res.apiv3
+  //     return res.status(200).send({ status: 'DONE' });
+  //   }
+  //   catch (err) {
+  //     // TODO: use ApiV3Error
+  //     logger.error(err);
+  //     return res.status(500).send({ status: 'ERROR' });
+  //   }
+  // });
+
+  return router;
+};

+ 3 - 0
src/server/routes/apiv3/index.js

@@ -8,5 +8,8 @@ const router = express.Router();
 
 module.exports = (crowi) => {
   router.use('/healthcheck', require('./healthcheck')(crowi));
+
+  router.use('/export', require('./export')(crowi));
+
   return router;
 };

+ 13 - 13
src/server/routes/comment.js

@@ -140,22 +140,22 @@ module.exports = function(crowi, app) {
     // slack notification
     if (slackNotificationForm.isSlackEnabled) {
       const user = await User.findUserByUsername(req.user.username);
-      const channels = slackNotificationForm.slackChannels;
+      const channelsStr = slackNotificationForm.slackChannels || null;
 
-      if (channels) {
-        page.updateSlackChannel(channels).catch((err) => {
-          logger.error('Error occured in updating slack channels: ', err);
-        });
+      page.updateSlackChannel(channelsStr).catch((err) => {
+        logger.error('Error occured in updating slack channels: ', err);
+      });
 
-        const promises = channels.split(',').map((chan) => {
-          return crowi.slack.postComment(createdComment, user, chan, path);
-        });
+      const channels = channelsStr != null ? channelsStr.split(',') : [null];
 
-        Promise.all(promises)
-          .catch((err) => {
-            logger.error('Error occured in sending slack notification: ', err);
-          });
-      }
+      const promises = channels.map((chan) => {
+        return crowi.slack.postComment(createdComment, user, chan, path);
+      });
+
+      Promise.all(promises)
+        .catch((err) => {
+          logger.error('Error occured in sending slack notification: ', err);
+        });
     }
   };
 

+ 4 - 0
src/server/routes/index.js

@@ -163,6 +163,10 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/import/qiita'        , loginRequired() , adminRequired , admin.api.importDataFromQiita);
   app.post('/_api/admin/import/testQiitaAPI' , loginRequired() , adminRequired , csrf , form.admin.importerQiita , admin.api.testQiitaAPI);
 
+  // export management for admin
+  // FIXME: comment out for v3.5.12
+  // app.get('/admin/export' , loginRequired() , adminRequired ,admin.export.index);
+
   app.get('/me'                       , loginRequired() , me.index);
   app.get('/me/password'              , loginRequired() , me.password);
   app.get('/me/apiToken'              , loginRequired() , me.apiToken);

+ 16 - 5
src/server/routes/page.js

@@ -100,14 +100,25 @@ module.exports = function(crowi, app) {
 
   // user notification
   // TODO create '/service/user-notification' module
-  async function notifyToSlackByUser(page, user, slackChannels, updateOrCreate, previousRevision) {
-    await page.updateSlackChannel(slackChannels)
+  /**
+   *
+   * @param {Page} page
+   * @param {User} user
+   * @param {string} slackChannelsStr comma separated string. e.g. 'general,channel1,channel2'
+   * @param {boolean} updateOrCreate
+   * @param {string} previousRevision
+   */
+  async function notifyToSlackByUser(page, user, slackChannelsStr, updateOrCreate, previousRevision) {
+    await page.updateSlackChannel(slackChannelsStr)
       .catch((err) => {
         logger.error('Error occured in updating slack channels: ', err);
       });
 
+
     if (slackNotificationService.hasSlackConfig()) {
-      const promises = slackChannels.split(',').map((chan) => {
+      const slackChannels = slackChannelsStr != null ? slackChannelsStr.split(',') : [null];
+
+      const promises = slackChannels.map((chan) => {
         return crowi.slack.postPage(page, user, chan, updateOrCreate, previousRevision);
       });
 
@@ -615,7 +626,7 @@ module.exports = function(crowi, app) {
     }
 
     // user notification
-    if (isSlackEnabled && slackChannels != null) {
+    if (isSlackEnabled) {
       await notifyToSlackByUser(createdPage, req.user, slackChannels, 'create', false);
     }
   };
@@ -702,7 +713,7 @@ module.exports = function(crowi, app) {
     }
 
     // user notification
-    if (isSlackEnabled && slackChannels != null) {
+    if (isSlackEnabled) {
       await notifyToSlackByUser(page, req.user, slackChannels, 'update', previousRevision);
     }
   };

+ 205 - 0
src/server/service/export.js

@@ -0,0 +1,205 @@
+const logger = require('@alias/logger')('growi:services:ExportService'); // eslint-disable-line no-unused-vars
+const fs = require('fs');
+const path = require('path');
+const streamToPromise = require('stream-to-promise');
+const archiver = require('archiver');
+
+class ExportService {
+
+  constructor(crowi) {
+    this.baseDir = path.join(crowi.tmpDir, 'downloads');
+    this.extension = 'json';
+    this.encoding = 'utf-8';
+    this.per = 100;
+    this.zlibLevel = 9; // 0(min) - 9(max)
+
+    this.files = {};
+    // populate this.files
+    // this.files = {
+    //   configs: path.join(this.baseDir, 'configs.json'),
+    //   pages: path.join(this.baseDir, 'pages.json'),
+    //   pagetagrelations: path.join(this.baseDir, 'pagetagrelations.json'),
+    //   ...
+    // };
+    // TODO: handle 3 globalnotificationsettings collection properly
+    // see Object.values(crowi.models).forEach((m) => { return console.log(m.collection.collectionName) });
+    Object.values(crowi.models).forEach((m) => {
+      const name = m.collection.collectionName;
+      this.files[name] = path.join(this.baseDir, `${name}.${this.extension}`);
+    });
+  }
+
+  /**
+   * dump a collection into json
+   *
+   * @memberOf ExportService
+   * @return {object} cache info for exported zip files
+   */
+  getStatus() {
+    const status = {};
+    const collections = Object.keys(this.files);
+    collections.forEach((file) => {
+      status[path.basename(file, '.zip')] = null;
+    });
+
+    // extract ${collectionName}.zip
+    const files = fs.readdirSync(this.baseDir).filter((file) => { return path.extname(file) === '.zip' && collections.includes(path.basename(file, '.zip')) });
+
+    files.forEach((file) => {
+      status[path.basename(file, '.zip')] = file;
+    });
+
+    return status;
+  }
+
+  /**
+   * dump a collection into json
+   *
+   * @memberOf ExportService
+   * @param {string} file path to json file to be written
+   * @param {readStream} readStream  read stream
+   * @param {number} [total] number of target items (optional)
+   */
+  async export(file, readStream, total) {
+    let n = 0;
+    const ws = fs.createWriteStream(file, { encoding: this.encoding });
+
+    // open an array
+    ws.write('[');
+
+    readStream.on('data', (chunk) => {
+      if (n !== 0) ws.write(',');
+      ws.write(JSON.stringify(chunk));
+      n++;
+      this.logProgress(n, total);
+    });
+
+    readStream.on('end', () => {
+      // close the array
+      ws.write(']');
+      ws.close();
+    });
+
+    await streamToPromise(readStream);
+  }
+
+  /**
+   * dump a mongodb collection into json
+   *
+   * @memberOf ExportService
+   * @param {object} Model instance of mongoose model
+   * @return {string} path to zip file
+   */
+  async exportCollection(Model) {
+    const modelName = Model.collection.collectionName;
+    const file = this.files[modelName];
+    const total = await Model.countDocuments();
+    const readStream = Model.find().cursor();
+
+    await this.export(file, readStream, total);
+
+    const { file: zipFile, size } = await this.zipSingleFile(file);
+
+    logger.info(`exported ${modelName} collection into ${zipFile} (${size} bytes)`);
+
+    return zipFile;
+  }
+
+  /**
+   * log export progress
+   *
+   * @memberOf ExportService
+   * @param {number} n number of items exported
+   * @param {number} [total] number of target items (optional)
+   */
+  logProgress(n, total) {
+    let output;
+    if (total) {
+      output = `${n}/${total} written`;
+    }
+    else {
+      output = `${n} items written`;
+    }
+
+    // output every this.per items
+    if (n % this.per === 0) logger.debug(output);
+    // output last item
+    else if (n === total) logger.info(output);
+  }
+
+  /**
+   * zip a file
+   *
+   * @memberOf ExportService
+   * @param {string} from path to input file
+   * @param {string} [to=`${path.join(path.dirname(from), `${path.basename(from, path.extname(from))}.zip`)}`] path to output file
+   * @param {string} [as=path.basename(from)] file name after unzipped
+   * @return {object} file path and file size
+   * @see https://www.archiverjs.com/#quick-start
+   */
+  async zipSingleFile(from, to = this.replaceExtension(from, 'zip'), as = path.basename(from)) {
+    const archive = archiver('zip', {
+      zlib: { level: this.zlibLevel },
+    });
+    const input = fs.createReadStream(from);
+    const output = fs.createWriteStream(to);
+
+    // good practice to catch warnings (ie stat failures and other non-blocking errors)
+    archive.on('warning', (err) => {
+      if (err.code === 'ENOENT') logger.error(err);
+      else throw err;
+    });
+
+    // good practice to catch this error explicitly
+    archive.on('error', (err) => { throw err });
+
+    // append a file from stream
+    archive.append(input, { name: as });
+
+    // pipe archive data to the file
+    archive.pipe(output);
+
+    // finalize the archive (ie we are done appending files but streams have to finish yet)
+    // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
+    archive.finalize();
+
+    await streamToPromise(archive);
+
+    return {
+      file: to,
+      size: archive.pointer(),
+    };
+  }
+
+  /**
+   * replace a file extension
+   *
+   * @memberOf ExportService
+   * @param {string} file file path
+   * @param {string} extension new extension
+   * @return {string} path to file with new extension
+   */
+  replaceExtension(file, extension) {
+    return `${path.join(path.dirname(file), `${path.basename(file, path.extname(file))}.${extension}`)}`;
+  }
+
+  /**
+   * get the path to the zipped file for a collection
+   *
+   * @memberOf ExportService
+   * @param {object} Model instance of mongoose model
+   * @return {string} path to zip file
+   */
+  getZipFile(Model) {
+    const json = this.files[Model.collection.collectionName];
+    const zip = this.replaceExtension(json, 'zip');
+    if (!fs.existsSync(zip)) {
+      return null;
+    }
+
+    return zip;
+  }
+
+}
+
+module.exports = ExportService;

+ 83 - 13
src/server/util/search.js

@@ -44,6 +44,7 @@ function SearchClient(crowi, esUri) {
   const uri = this.parseUri(this.esUri);
   this.host = uri.host;
   this.indexName = uri.indexName;
+  this.aliasName = `${this.indexName}-alias`;
 
   this.client = new elasticsearch.Client({
     host: this.host,
@@ -117,17 +118,86 @@ SearchClient.prototype.parseUri = function(uri) {
   };
 };
 
-SearchClient.prototype.buildIndex = function(uri) {
-  return this.client.indices.create({
-    index: this.indexName,
-    body: require(this.mappingFile),
-  });
+SearchClient.prototype.initIndices = async function() {
+  await this.checkESVersion();
+
+  const { client, indexName, aliasName } = this;
+
+  const tmpIndexName = `${indexName}-tmp`;
+
+  // remove tmp index
+  const isExistsTmpIndex = await client.indices.exists({ index: tmpIndexName });
+  if (isExistsTmpIndex) {
+    await client.indices.delete({ index: tmpIndexName });
+  }
+
+  // create index
+  const isExistsIndex = await client.indices.exists({ index: indexName });
+  if (!isExistsIndex) {
+    await this.createIndex(indexName);
+  }
+
+  // create alias
+  const isExistsAlias = await client.indices.existsAlias({ name: aliasName, index: indexName });
+  if (!isExistsAlias) {
+    await client.indices.putAlias({
+      name: aliasName,
+      index: indexName,
+    });
+  }
 };
 
-SearchClient.prototype.deleteIndex = function(uri) {
-  return this.client.indices.delete({
-    index: this.indexName,
+SearchClient.prototype.createIndex = async function(index) {
+  const body = require(this.mappingFile);
+  return this.client.indices.create({ index, body });
+};
+
+SearchClient.prototype.buildIndex = async function(uri) {
+  await this.initIndices();
+
+  const { client, indexName } = this;
+
+  const aliasName = `${indexName}-alias`;
+  const tmpIndexName = `${indexName}-tmp`;
+
+  // reindex to tmp index
+  await this.createIndex(tmpIndexName);
+  await client.reindex({
+    body: {
+      source: { index: indexName },
+      dest: { index: tmpIndexName },
+    },
+  });
+
+  // update alias
+  await client.indices.updateAliases({
+    body: {
+      actions: [
+        { add: { alias: aliasName, index: tmpIndexName } },
+        { remove: { alias: aliasName, index: indexName } },
+      ],
+    },
   });
+
+  // flush index
+  await client.indices.delete({
+    index: indexName,
+  });
+  await this.createIndex(indexName);
+  await this.addAllPages();
+
+  // update alias
+  await client.indices.updateAliases({
+    body: {
+      actions: [
+        { add: { alias: aliasName, index: indexName } },
+        { remove: { alias: aliasName, index: tmpIndexName } },
+      ],
+    },
+  });
+
+  // remove tmp index
+  await client.indices.delete({ index: tmpIndexName });
 };
 
 /**
@@ -162,7 +232,7 @@ SearchClient.prototype.prepareBodyForUpdate = function(body, page) {
 
   const command = {
     update: {
-      _index: this.indexName,
+      _index: this.aliasName,
       _type: 'pages',
       _id: page._id.toString(),
     },
@@ -194,7 +264,7 @@ SearchClient.prototype.prepareBodyForCreate = function(body, page) {
 
   const command = {
     index: {
-      _index: this.indexName,
+      _index: this.aliasName,
       _type: 'pages',
       _id: page._id.toString(),
     },
@@ -226,7 +296,7 @@ SearchClient.prototype.prepareBodyForDelete = function(body, page) {
 
   const command = {
     delete: {
-      _index: this.indexName,
+      _index: this.aliasName,
       _type: 'pages',
       _id: page._id.toString(),
     },
@@ -399,7 +469,7 @@ SearchClient.prototype.createSearchQuerySortedByUpdatedAt = function(option) {
 
   // default is only id field, sorted by updated_at
   const query = {
-    index: this.indexName,
+    index: this.aliasName,
     type: 'pages',
     body: {
       sort: [{ updated_at: { order: 'desc' } }],
@@ -420,7 +490,7 @@ SearchClient.prototype.createSearchQuerySortedByScore = function(option) {
 
   // sort by score
   const query = {
-    index: this.indexName,
+    index: this.aliasName,
     type: 'pages',
     body: {
       sort: [{ _score: { order: 'desc' } }],

+ 3 - 3
src/server/util/slack.js

@@ -132,7 +132,7 @@ module.exports = function(crowi) {
     }
 
     const message = {
-      channel: `#${channel}`,
+      channel: (channel != null) ? `#${channel}` : undefined,
       username: appTitle,
       text: getSlackMessageTextForPage(page.path, page.id, user, updateType),
       attachments: [attachment],
@@ -159,7 +159,7 @@ module.exports = function(crowi) {
     }
 
     const message = {
-      channel: `#${channel}`,
+      channel: (channel != null) ? `#${channel}` : undefined,
       username: appTitle,
       text: getSlackMessageTextForComment(path, String(comment.page), user),
       attachments: [attachment],
@@ -185,7 +185,7 @@ module.exports = function(crowi) {
     };
 
     const message = {
-      channel: `#${slackChannel}`,
+      channel: (slackChannel != null) ? `#${slackChannel}` : undefined,
       username: appTitle,
       text: messageBody,
       attachments: [attachment],

+ 31 - 0
src/server/views/admin/export.html

@@ -0,0 +1,31 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Export Data')) }}{% endblock %}
+
+{% block content_header %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 id="admin-title" class="title">{{ t('Export Data') }}</h1>
+  </header>
+</div>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main admin-export">
+
+  <div class="row">
+    <div class="col-md-3">
+      {% include './widget/menu.html' with {current: 'export'} %}
+    </div>
+    <div
+      id="admin-export-page"
+      class="col-md-9"
+    >
+    </div>
+  </div>
+</div>
+
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock content_footer %}

+ 10 - 3
src/server/views/admin/notification.html

@@ -37,15 +37,19 @@
       {% endif %}
 
       <ul class="nav nav-tabs" role="tablist">
-        <li class="active">
+        <li role="tab" class="active">
+          <a href="#slack-configuration" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack Configuration</a>
+        </li>
+        <li role="tab">
           <a href="#user-trigger-notification" data-toggle="tab" role="tab"><i class="icon-settings"></i> User Trigger Notification</a>
         </li>
         <li role="tab">
           <a href="#global-notification" data-toggle="tab" role="tab"><i class="icon-settings"></i> Global Notification</a>
         </li>
       </ul>
+
       <div class="tab-content m-t-15">
-        <div id="user-trigger-notification" class="tab-pane active" role="tabpanel">
+        <div id="slack-configuration" class="tab-pane active" role="tabpanel">
 
           <select class="selectpicker" id="selectSlackOption" data-width="auto">
             <option value="1">Slack Incoming Webhooks</option>
@@ -53,6 +57,7 @@
           </select><!-- /.select-tab -->
 
           <div class="tab-content m-t-15">
+
             <div id="slack-incoming-webhooks" class="tab-pane active" role="tabpanel">
 
               <form action="/admin/notification/slackIwhSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
@@ -198,7 +203,9 @@
             </div><!-- /#slack-app -->
 
           </div><!-- /.tab-content -->
-          <hr>
+        </div>
+
+        <div id="user-trigger-notification" class="tab-pane" role="tabpanel">
           <h4>Default Notification Settings for Patterns</h4>
 
           <table class="table table-bordered">

+ 3 - 1
src/server/views/admin/widget/menu.html

@@ -7,7 +7,9 @@
   <li class="{% if current == 'security'%}active{% endif %}"><a href="/admin/security"><i class="icon-fw icon-shield"></i> {{ t('security_settings') }}</a></li>
   <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="icon-fw icon-note"></i> {{ t('Markdown Settings') }}</a></li>
   <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="icon-fw icon-wrench"></i> {{ t('Customize') }}</a></li>
-  <li class="{% if current == 'importer'%}active{% endif %}"><a href="/admin/importer"><i class="icon-fw icon-cloud-download"></i> {{ t('Import Data') }}</a></li>
+  <li class="{% if current == 'importer'%}active{% endif %}"><a href="/admin/importer"><i class="icon-fw icon-cloud-upload"></i> {{ t('Import Data') }}</a></li>
+  {# FIXME: comment out for v3.5.12 #}
+  {# <li class="{% if current == 'export'%}active{% endif %}"><a href="/admin/export"><i class="icon-fw icon-cloud-download"></i> {{ t('Export Data') }}</a></li> #}
   <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="icon-fw icon-bell"></i> {{ t('Notification Settings') }}</a></li>
   <li class="{% if current == 'user' || current == 'external-account' %}active{% endif %}"><a href="/admin/users"><i class="icon-fw icon-user"></i> {{ t('User_Management') }}</a></li>
   <li class="{% if current == 'user-group'%}active{% endif %}"><a href="/admin/user-groups"><i class="icon-fw icon-people"></i> {{ t('UserGroup Management') }}</a></li>

+ 0 - 0
tmp/downloads/.keep


+ 257 - 68
yarn.lock

@@ -1482,6 +1482,35 @@ aproba@^1.0.3, aproba@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
 
+archiver-utils@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2"
+  integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==
+  dependencies:
+    glob "^7.1.4"
+    graceful-fs "^4.2.0"
+    lazystream "^1.0.0"
+    lodash.defaults "^4.2.0"
+    lodash.difference "^4.5.0"
+    lodash.flatten "^4.4.0"
+    lodash.isplainobject "^4.0.6"
+    lodash.union "^4.6.0"
+    normalize-path "^3.0.0"
+    readable-stream "^2.0.0"
+
+archiver@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0"
+  integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==
+  dependencies:
+    archiver-utils "^2.1.0"
+    async "^2.6.3"
+    buffer-crc32 "^0.2.1"
+    glob "^7.1.4"
+    readable-stream "^3.4.0"
+    tar-stream "^2.1.0"
+    zip-stream "^2.1.2"
+
 are-we-there-yet@~1.1.2:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
@@ -1645,6 +1674,13 @@ async@^0.9.0:
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
 
+async@^2.6.3:
+  version "2.6.3"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
+  integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
+  dependencies:
+    lodash "^4.17.14"
+
 async@~0.2.6:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
@@ -1914,6 +1950,13 @@ binary-extensions@^1.0.0:
   version "1.11.0"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
 
+bl@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
+  integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==
+  dependencies:
+    readable-stream "^3.0.1"
+
 blob@0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
@@ -1932,10 +1975,6 @@ bluebird@3.5.1, bluebird@^3.5.1:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
 
-bluebird@^3.5.3:
-  version "3.5.3"
-  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
-
 bluebird@^3.5.5:
   version "3.5.5"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
@@ -2203,6 +2242,11 @@ bson@^1.1.0, bson@^1.1.1, bson@~1.1.0:
   resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.1.tgz#4330f5e99104c4e751e7351859e2d408279f2f13"
   integrity sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==
 
+buffer-crc32@^0.2.1, buffer-crc32@^0.2.13:
+  version "0.2.13"
+  resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
+  integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
+
 buffer-equal-constant-time@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
@@ -2223,6 +2267,14 @@ buffer@4.9.1, buffer@^4.3.0:
     ieee754 "^1.1.4"
     isarray "^1.0.0"
 
+buffer@^5.1.0:
+  version "5.4.2"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.2.tgz#2012872776206182480eccb2c0fba5f672a2efef"
+  integrity sha512-iy9koArjAFCzGnx3ZvNA6Z0clIbbFgbdWQ0mKD3hO0krOrZh8UgA6qMKcZvwLJxS+D6iVR76+5/pV56yMNYTag==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+
 builtin-modules@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@@ -2266,26 +2318,7 @@ bytes@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
 
-cacache@^11.0.2:
-  version "11.3.2"
-  resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa"
-  dependencies:
-    bluebird "^3.5.3"
-    chownr "^1.1.1"
-    figgy-pudding "^3.5.1"
-    glob "^7.1.3"
-    graceful-fs "^4.1.15"
-    lru-cache "^5.1.1"
-    mississippi "^3.0.0"
-    mkdirp "^0.5.1"
-    move-concurrently "^1.0.1"
-    promise-inflight "^1.0.1"
-    rimraf "^2.6.2"
-    ssri "^6.0.1"
-    unique-filename "^1.1.1"
-    y18n "^4.0.0"
-
-cacache@^12.0.2:
+cacache@^12.0.2, cacache@^12.0.3:
   version "12.0.3"
   resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390"
   integrity sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==
@@ -2789,7 +2822,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@2.17.1, commander@~2.17.1:
+commander@2.17.1:
   version "2.17.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
 
@@ -2835,6 +2868,16 @@ component-inherit@0.0.3:
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
 
+compress-commons@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-2.1.1.tgz#9410d9a534cf8435e3fbbb7c6ce48de2dc2f0610"
+  integrity sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==
+  dependencies:
+    buffer-crc32 "^0.2.13"
+    crc32-stream "^3.0.1"
+    normalize-path "^3.0.0"
+    readable-stream "^2.3.6"
+
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -3064,6 +3107,21 @@ cosmiconfig@^5.0.0:
     js-yaml "^3.9.0"
     parse-json "^4.0.0"
 
+crc32-stream@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85"
+  integrity sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==
+  dependencies:
+    crc "^3.4.4"
+    readable-stream "^3.4.0"
+
+crc@^3.4.4:
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
+  integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
+  dependencies:
+    buffer "^5.1.0"
+
 create-ecdh@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
@@ -3845,7 +3903,7 @@ encoding@^0.1.11:
   dependencies:
     iconv-lite "~0.4.13"
 
-end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
   dependencies:
@@ -4725,6 +4783,15 @@ find-cache-dir@^2.1.0:
     make-dir "^2.0.0"
     pkg-dir "^3.0.0"
 
+find-cache-dir@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.0.0.tgz#cd4b7dd97b7185b7e17dbfe2d6e4115ee3eeb8fc"
+  integrity sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw==
+  dependencies:
+    commondir "^1.0.1"
+    make-dir "^3.0.0"
+    pkg-dir "^4.1.0"
+
 find-up@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -4744,6 +4811,14 @@ find-up@^3.0.0:
   dependencies:
     locate-path "^3.0.0"
 
+find-up@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
 findup-sync@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1"
@@ -4885,6 +4960,11 @@ from@~0:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
 
+fs-constants@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
 fs-extra@3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291"
@@ -5248,6 +5328,11 @@ graceful-fs@^4.1.15:
   version "4.1.15"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
 
+graceful-fs@^4.2.0:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
+  integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
+
 growi-commons@^4.0.7:
   version "4.0.7"
   resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-4.0.7.tgz#f9ff9c2f6afe3a9982b689d368e8e7a000d137e8"
@@ -6646,6 +6731,14 @@ jest-worker@^24.6.0:
     merge-stream "^1.0.1"
     supports-color "^6.1.0"
 
+jest-worker@^24.9.0:
+  version "24.9.0"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5"
+  integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==
+  dependencies:
+    merge-stream "^2.0.0"
+    supports-color "^6.1.0"
+
 jest@^24.8.0:
   version "24.8.0"
   resolved "https://registry.yarnpkg.com/jest/-/jest-24.8.0.tgz#d5dff1984d0d1002196e9b7f12f75af1b2809081"
@@ -6984,6 +7077,13 @@ lazy-cache@^1.0.3:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
 
+lazystream@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4"
+  integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=
+  dependencies:
+    readable-stream "^2.0.5"
+
 lcid@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
@@ -7139,6 +7239,13 @@ locate-path@^3.0.0:
     p-locate "^3.0.0"
     path-exists "^3.0.0"
 
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
 lodash-webpack-plugin@^0.11.5:
   version "0.11.5"
   resolved "https://registry.yarnpkg.com/lodash-webpack-plugin/-/lodash-webpack-plugin-0.11.5.tgz#c4bd064b4f561c3f823fa5982bdeb12c475390b9"
@@ -7149,6 +7256,21 @@ lodash.debounce@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
 
+lodash.defaults@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
+  integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=
+
+lodash.difference@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
+  integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=
+
+lodash.flatten@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
+  integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
+
 lodash.foreach@^4.1.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
@@ -7170,6 +7292,11 @@ lodash.isfinite@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
 
+lodash.isplainobject@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+  integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
 lodash.memoize@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -7187,6 +7314,11 @@ lodash.sortby@^4.7.0:
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
 
+lodash.union@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
+  integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
+
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
@@ -7207,7 +7339,7 @@ lodash@^4.17.10, lodash@^4.17.5:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
-lodash@^4.17.15:
+lodash@^4.17.14, lodash@^4.17.15:
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -7293,6 +7425,13 @@ make-dir@^2.0.0, make-dir@^2.1.0:
     pify "^4.0.1"
     semver "^5.6.0"
 
+make-dir@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801"
+  integrity sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==
+  dependencies:
+    semver "^6.0.0"
+
 makeerror@1.0.x:
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
@@ -7517,6 +7656,11 @@ merge-stream@^1.0.1:
   dependencies:
     readable-stream "^2.0.1"
 
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
 method-override@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/method-override/-/method-override-3.0.0.tgz#6ab0d5d574e3208f15b0c9cf45ab52000468d7a2"
@@ -8672,6 +8816,13 @@ p-limit@^2.0.0:
   dependencies:
     p-try "^2.0.0"
 
+p-limit@^2.2.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537"
+  integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==
+  dependencies:
+    p-try "^2.0.0"
+
 p-locate@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
@@ -8684,6 +8835,13 @@ p-locate@^3.0.0:
   dependencies:
     p-limit "^2.0.0"
 
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
 p-reduce@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
@@ -8923,6 +9081,11 @@ path-exists@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
 
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
 path-is-absolute@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@@ -9046,6 +9209,13 @@ pkg-dir@^3.0.0:
   dependencies:
     find-up "^3.0.0"
 
+pkg-dir@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+  dependencies:
+    find-up "^4.0.0"
+
 plantuml-encoder@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/plantuml-encoder/-/plantuml-encoder-1.2.5.tgz#6b8e5b9e1a1dbd88b3fd9fb46f734eec7d44b548"
@@ -10016,7 +10186,7 @@ read-pkg@^3.0.0:
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
 
-"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.1.5:
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.3.6:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
   dependencies:
@@ -10058,6 +10228,15 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable
     string_decoder "~1.0.3"
     util-deprecate "~1.0.1"
 
+readable-stream@^3.0.1, readable-stream@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
+  integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
 readable-stream@^3.1.1:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d"
@@ -10550,7 +10729,7 @@ rimraf@2, rimraf@2.6.3, rimraf@^2.6.3:
   dependencies:
     glob "^7.1.3"
 
-rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
+rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -10715,7 +10894,7 @@ schema-utils@^2.0.1:
     ajv "^6.1.0"
     ajv-keywords "^3.1.0"
 
-schema-utils@^2.1.0:
+schema-utils@^2.1.0, schema-utils@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.2.0.tgz#48a065ce219e0cacf4631473159037b2c1ae82da"
   integrity sha512-5EwsCNhfFTZvUreQhx/4vVQpJ/lnCAkgoIHLhSpp4ZirE+4hzFvdJi0FMub6hxbFVBJYSpeVVmon+2e7uEGRrA==
@@ -10808,15 +10987,16 @@ send@0.16.2:
     range-parser "~1.2.0"
     statuses "~1.4.0"
 
-serialize-javascript@^1.4.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe"
-
 serialize-javascript@^1.7.0:
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb"
   integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==
 
+serialize-javascript@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.0.tgz#9310276819efd0eb128258bb341957f6eb2fc570"
+  integrity sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ==
+
 serve-index@1.9.1:
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
@@ -11162,13 +11342,6 @@ source-map-support@~0.5.12:
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
-source-map-support@~0.5.9:
-  version "0.5.10"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c"
-  dependencies:
-    buffer-from "^1.0.0"
-    source-map "^0.6.0"
-
 source-map-url@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
@@ -11764,6 +11937,17 @@ tapable@^1.1.3:
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
 
+tar-stream@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3"
+  integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw==
+  dependencies:
+    bl "^3.0.0"
+    end-of-stream "^1.4.1"
+    fs-constants "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^3.1.1"
+
 tar@^2.0.0:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
@@ -11812,19 +11996,6 @@ term-size@^1.2.0:
   dependencies:
     execa "^0.7.0"
 
-terser-webpack-plugin@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz#9bff3a891ad614855a7dde0d707f7db5a927e3d9"
-  dependencies:
-    cacache "^11.0.2"
-    find-cache-dir "^2.0.0"
-    schema-utils "^1.0.0"
-    serialize-javascript "^1.4.0"
-    source-map "^0.6.1"
-    terser "^3.16.1"
-    webpack-sources "^1.1.0"
-    worker-farm "^1.5.2"
-
 terser-webpack-plugin@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4"
@@ -11840,13 +12011,19 @@ terser-webpack-plugin@^1.4.1:
     webpack-sources "^1.4.0"
     worker-farm "^1.7.0"
 
-terser@^3.16.1:
-  version "3.16.1"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493"
-  dependencies:
-    commander "~2.17.1"
-    source-map "~0.6.1"
-    source-map-support "~0.5.9"
+terser-webpack-plugin@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.0.1.tgz#5e69a187133472174ef2fb51afc76a0f0a7b1d13"
+  integrity sha512-DXx8f7080P2dgW9Ydd7MUon81AL+GkGr9prfQRf+rH06dRrgCGIvtT7q73JRm2UzmjKeJGtro9O8zwL2y2udag==
+  dependencies:
+    cacache "^12.0.3"
+    find-cache-dir "^3.0.0"
+    jest-worker "^24.9.0"
+    schema-utils "^2.2.0"
+    serialize-javascript "^2.1.0"
+    source-map "^0.6.1"
+    terser "^4.2.1"
+    webpack-sources "^1.4.3"
 
 terser@^4.1.2:
   version "4.2.1"
@@ -11857,6 +12034,15 @@ terser@^4.1.2:
     source-map "~0.6.1"
     source-map-support "~0.5.12"
 
+terser@^4.2.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.1.tgz#09820bcb3398299c4b48d9a86aefc65127d0ed65"
+  integrity sha512-pnzH6dnFEsR2aa2SJaKb1uSCl3QmIsJ8dEkj0Fky+2AwMMcC9doMqLOQIH6wVTEKaVfKVvLSk5qxPBEZT9mywg==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.6.1"
+    source-map-support "~0.5.12"
+
 test-exclude@^5.2.3:
   version "5.2.3"
   resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0"
@@ -12616,7 +12802,7 @@ webpack-sources@^1.1.0:
     source-list-map "^2.0.0"
     source-map "~0.6.1"
 
-webpack-sources@^1.4.0, webpack-sources@^1.4.1:
+webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
   version "1.4.3"
   resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
   integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
@@ -12743,12 +12929,6 @@ wordwrap@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
 
-worker-farm@^1.5.2:
-  version "1.6.0"
-  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"
-  dependencies:
-    errno "~0.1.7"
-
 worker-farm@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
@@ -13093,3 +13273,12 @@ z-schema@^3.23.0:
     validator "^10.0.0"
   optionalDependencies:
     commander "^2.7.1"
+
+zip-stream@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.2.tgz#841efd23214b602ff49c497cba1a85d8b5fbc39c"
+  integrity sha512-ykebHGa2+uzth/R4HZLkZh3XFJzivhVsjJt8bN3GvBzLaqqrUdRacu+c4QtnUgjkkQfsOuNE1JgLKMCPNmkKgg==
+  dependencies:
+    archiver-utils "^2.1.0"
+    compress-commons "^2.1.1"
+    readable-stream "^3.4.0"