Просмотр исходного кода

Merge pull request #742 from weseek/feature/no-cdn-mode

Feature/no cdn mode
Yuki Takei 7 лет назад
Родитель
Сommit
10f17310ef

+ 30 - 0
bin/download-cdn-resources.js

@@ -0,0 +1,30 @@
+/**
+ * the tool for download CDN resources and save as file
+ *
+ * @author Yuki Takei <yuki@weseek.co.jp>
+ */
+require('module-alias/register');
+
+const logger = require('@alias/logger')('growi:bin:download-resources');
+
+// check env var
+const noCdn = !!process.env.NO_CDN;
+if (!noCdn) {
+  logger.info('Using CDN. No resources are downloaded.');
+  // exit
+  process.exit(0);
+}
+
+const CdnResourcesService = require('@commons/service/cdn-resources-service');
+
+const service = new CdnResourcesService();
+
+logger.info('This is NO_CDN mode. Start to download resources.');
+
+service.downloadAndWriteAll()
+  .then(() => {
+    logger.info('Download is terminated successfully');
+  })
+  .catch(err => {
+    logger.error(err);
+  });

+ 22 - 0
bin/download-resources.js

@@ -0,0 +1,22 @@
+/**
+ * the tool for download CDN resources and save as file
+ *
+ * @author Yuki Takei <yuki@weseek.co.jp>
+ */
+
+require('module-alias/register');
+
+const logger = require('@alias/logger')('growi:bin:download-resources');
+const CdnResourcesService = require('@commons/service/cdn-resources-service');
+
+const service = new CdnResourcesService();
+
+logger.info('Start to download.');
+
+service.downloadAndWriteAll()
+  .then(() => {
+    logger.info('Download is terminated successfully');
+  })
+  .catch(err => {
+    logger.error(err);
+  });

+ 1 - 0
config/env.dev.js

@@ -3,6 +3,7 @@ module.exports = {
   FILE_UPLOAD: 'mongodb',
   FILE_UPLOAD: 'mongodb',
   // MONGODB_GRIDFS_LIMIT: 10485760,   // 10MB
   // MONGODB_GRIDFS_LIMIT: 10485760,   // 10MB
   // MATHJAX: 1,
   // MATHJAX: 1,
+  // NO_CDN: true,
   ELASTICSEARCH_URI: 'http://localhost:9200/growi',
   ELASTICSEARCH_URI: 'http://localhost:9200/growi',
   HACKMD_URI: 'http://localhost:3010',
   HACKMD_URI: 'http://localhost:3010',
   PLUGIN_NAMES_TOBE_LOADED: [
   PLUGIN_NAMES_TOBE_LOADED: [

+ 8 - 7
package.json

@@ -20,18 +20,15 @@
     "url": "https://github.com/weseek/growi/issues"
     "url": "https://github.com/weseek/growi/issues"
   },
   },
   "scripts": {
   "scripts": {
-    "build:dev:analyze": "npm-run-all -s build:dev:dll build:dev:app:analyze",
-    "build:dev:app:analyze": "cross-env ANALYZE=1 npm run build:dev:app:watch -- --profile",
     "build:dev:app:watch": "npm run build:dev:app -- --watch",
     "build:dev:app:watch": "npm run build:dev:app -- --watch",
-    "build:dev:app": "npm run clean:app && env-cmd config/env.dev.js webpack --config config/webpack.dev.js --progress",
+    "build:dev:app": "env-cmd config/env.dev.js webpack --config config/webpack.dev.js --progress",
     "build:dev:dll": "webpack --config config/webpack.dev.dll.js",
     "build:dev:dll": "webpack --config config/webpack.dev.dll.js",
     "build:dev:watch": "npm-run-all -s build:dev:dll build:dev:app:watch",
     "build:dev:watch": "npm-run-all -s build:dev:dll build:dev:app:watch",
     "build:dev": "npm-run-all -s build:dev:dll build:dev:app",
     "build:dev": "npm-run-all -s build:dev:dll build:dev:app",
     "build:prod:analyze": "cross-env ANALYZE=1 npm run build:prod",
     "build:prod:analyze": "cross-env ANALYZE=1 npm run build:prod",
-    "build:prod": "npm run clean && env-cmd config/env.prod.js webpack --config config/webpack.prod.js --profile --bail",
+    "build:prod": "env-cmd config/env.prod.js webpack --config config/webpack.prod.js --profile --bail",
     "build": "npm run build:dev:watch",
     "build": "npm run build:dev:watch",
     "clean:app": "rimraf -- public/js public/styles",
     "clean:app": "rimraf -- public/js public/styles",
-    "clean:dll": "rimraf -- public/dll",
     "clean:report": "rimraf -- report",
     "clean:report": "rimraf -- report",
     "clean": "npm-run-all -p clean:*",
     "clean": "npm-run-all -p clean:*",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
     "heroku-postbuild": "sh bin/heroku/install-plugins.sh && npm run build:prod",
@@ -44,10 +41,12 @@
     "migrate:down": "migrate-mongo down -f config/migrate.js",
     "migrate:down": "migrate-mongo down -f config/migrate.js",
     "mkdirp": "mkdirp",
     "mkdirp": "mkdirp",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
-    "prebuild:dev:app": "env-cmd config/env.dev.js npm run plugin:def",
-    "prebuild:prod": "npm run plugin:def",
+    "prebuild:dev:watch": "npm run prebuild:dev",
+    "prebuild:dev": "npm run clean:app && env-cmd config/env.dev.js npm run plugin:def && env-cmd config/env.dev.js npm run resource",
+    "prebuild:prod": "npm run clean && env-cmd config/env.prod.js npm run plugin:def && env-cmd config/env.dev.js npm run resource",
     "preserver:prod": "npm run migrate",
     "preserver:prod": "npm run migrate",
     "prestart": "npm run build:prod",
     "prestart": "npm run build:prod",
+    "resource": "node bin/download-cdn-resources.js",
     "server:debug": "env-cmd config/env.dev.js node-dev --inspect src/server/app.js",
     "server:debug": "env-cmd config/env.dev.js node-dev --inspect src/server/app.js",
     "server:dev": "env-cmd config/env.dev.js node-dev --respawn src/server/app.js",
     "server:dev": "env-cmd config/env.dev.js node-dev --respawn src/server/app.js",
     "server:prod:ci": "npm run server:prod -- --ci",
     "server:prod:ci": "npm run server:prod -- --ci",
@@ -194,12 +193,14 @@
     "react-frame-component": "^4.0.0",
     "react-frame-component": "^4.0.0",
     "react-i18next": "=7.13.0",
     "react-i18next": "=7.13.0",
     "react-waypoint": "^8.1.0",
     "react-waypoint": "^8.1.0",
+    "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
     "sass-loader": "^7.1.0",
     "sass-loader": "^7.1.0",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
     "sinon": "^7.0.0",
     "sinon": "^7.0.0",
     "sinon-chai": "^3.2.0",
     "sinon-chai": "^3.2.0",
     "socket.io-client": "^2.0.3",
     "socket.io-client": "^2.0.3",
+    "stream-to-promise": "^2.2.0",
     "style-loader": "^0.23.0",
     "style-loader": "^0.23.0",
     "throttle-debounce": "^2.0.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "toastr": "^2.1.2",

+ 191 - 0
resource/cdn-manifests.js

@@ -0,0 +1,191 @@
+module.exports = {
+  js: [
+    {
+      name: 'basis',
+      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.3.1,npm/bootstrap@3.3.7/dist/js/bootstrap.min.js',
+      groups: ['basis'],
+      args: {
+        integrity: '',
+      }
+    },
+    {
+      name: 'highlight',
+      url: 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.12.0/build/highlight.min.js',
+      groups: ['basis'],
+      args: {
+        integrity: '',
+      }
+    },
+    {
+      name: 'highlight-addons',
+      url: 'https://cdn.jsdelivr.net/combine/' +
+'gh/highlightjs/cdn-release@9.12.0/build/languages/dockerfile.min.js,' +
+'gh/highlightjs/cdn-release@9.12.0/build/languages/go.min.js,' +
+'gh/highlightjs/cdn-release@9.12.0/build/languages/gradle.min.js,' +
+'gh/highlightjs/cdn-release@9.12.0/build/languages/json.min.js,' +
+'gh/highlightjs/cdn-release@9.12.0/build/languages/less.min.js,' +
+'gh/highlightjs/cdn-release@9.12.0/build/languages/scss.min.js,' +
+'gh/highlightjs/cdn-release@9.12.0/build/languages/typescript.min.js,' +
+'gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js',
+      args: {
+        async: true,
+        integrity: '',
+      }
+    },
+    {
+      name: 'mathjax',
+      url: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js',
+      args: {
+        async: true,
+        integrity: '',
+      }
+    },
+    {
+      name: 'codemirror-dialog',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/addon/dialog/dialog.min.js',
+      args: {
+        integrity: '',
+      }
+    },
+    {
+      name: 'codemirror-keymap-vim',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/keymap/vim.min.js',
+      args: {
+        integrity: '',
+      }
+    },
+    {
+      name: 'codemirror-keymap-emacs',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/keymap/emacs.min.js',
+      args: {
+        integrity: '',
+      }
+    },
+    {
+      name: 'codemirror-keymap-sublime',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/keymap/sublime.min.js',
+      args: {
+        integrity: '',
+      }
+    },
+  ],
+  style: [
+    {
+      name: 'lato',
+      url: 'https://fonts.googleapis.com/css?family=Lato:400,700',
+      groups: ['basis'],
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'font-awesome',
+      url: 'https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css',
+      groups: ['basis'],
+      args: {
+        integrity: '',
+      }
+    },
+    {
+      name: 'themify-icons',
+      url: 'https://cdn.jsdelivr.net/npm/cd-themify-icons@0.0.1/index.min.css',
+      groups: ['basis'],
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'simple-line-icons',
+      url: 'https://cdn.jsdelivr.net/npm/simple-line-icons@2.4.1/css/simple-line-icons.min.css',
+      groups: ['basis'],
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'emojione',
+      url: 'https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css',
+      groups: ['basis'],
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'jquery-ui',
+      url: 'https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css',
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'highlight-theme-github',
+      url: 'https://cdn.jsdelivr.net/npm/highlight.js@9.12.0/styles/github.css',
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'codemirror-dialog',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/addon/dialog/dialog.min.css',
+      args: {
+        integrity: '',
+      }
+    },
+    {
+      name: 'codemirror-theme-eclipse',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/eclipse.min.css',
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'codemirror-theme-elegant',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/elegant.min.css',
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'codemirror-theme-neo',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/neo.min.css',
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'codemirror-theme-mdn-like',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/mdn-like.min.css',
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'codemirror-theme-material',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/material.min.css',
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'codemirror-theme-dracula',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/dracula.min.css',
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'codemirror-theme-monokai',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/monokai.min.css',
+      args: {
+        integrity: ''
+      },
+    },
+    {
+      name: 'codemirror-theme-twilight',
+      url: 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0/theme/twilight.min.css',
+      args: {
+        integrity: ''
+      },
+    },
+  ]
+};

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

@@ -509,6 +509,7 @@
     "tab_switch": "Save tab-switching in the browser",
     "tab_switch": "Save tab-switching in the browser",
     "save_edit": "Save edit tab and history tab switching in the browser and make it object for forward/back command of the browser.",
     "save_edit": "Save edit tab and history tab switching in the browser and make it object for forward/back command of the browser.",
     "by_invalidating": "By invalidating, you can make page transition as the only object for forward/back command of the browser.",
     "by_invalidating": "By invalidating, you can make page transition as the only object for forward/back command of the browser.",
+    "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
     "Custom CSS": "Custom CSS",
     "Custom CSS": "Custom CSS",
     "write_CSS": "You can write CSS that is applied to whole system.",
     "write_CSS": "You can write CSS that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change.",
     "reflect_change": "You need to reload the page to reflect the change.",

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

@@ -524,6 +524,7 @@
     "tab_switch": "タブ変更をブラウザ履歴に保存",
     "tab_switch": "タブ変更をブラウザ履歴に保存",
     "save_edit": "編集タブやヒストリータブ等の切り替えをブラウザ履歴に保存し、ブラウザの戻る/進む操作の対象にします。",
     "save_edit": "編集タブやヒストリータブ等の切り替えをブラウザ履歴に保存し、ブラウザの戻る/進む操作の対象にします。",
     "by_invalidating": "無効化することで、ページ遷移のみを戻る/進む操作の対象にすることができます。",
     "by_invalidating": "無効化することで、ページ遷移のみを戻る/進む操作の対象にすることができます。",
+    "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
     "Custom CSS": "カスタム CSS",
     "Custom CSS": "カスタム CSS",
     "write_CSS": " システム全体に適用されるCSSを記述できます。",
     "write_CSS": " システム全体に適用されるCSSを記述できます。",
     "reflect_change": "変更の反映はページの更新が必要です。",
     "reflect_change": "変更の反映はページの更新が必要です。",

+ 3 - 0
src/client/js/components/PageEditor.js

@@ -320,6 +320,8 @@ export default class PageEditor extends React.Component {
   }
   }
 
 
   render() {
   render() {
+    const config = this.props.crowi.getConfig();
+    const noCdn = !!config.env.NO_CDN;
     const emojiStrategy = this.props.crowi.getEmojiStrategy();
     const emojiStrategy = this.props.crowi.getEmojiStrategy();
 
 
     return (
     return (
@@ -327,6 +329,7 @@ export default class PageEditor extends React.Component {
         <div className="col-md-6 col-sm-12 page-editor-editor-container">
         <div className="col-md-6 col-sm-12 page-editor-editor-container">
           <Editor ref="editor" value={this.state.markdown}
           <Editor ref="editor" value={this.state.markdown}
             editorOptions={this.state.editorOptions}
             editorOptions={this.state.editorOptions}
+            noCdn={noCdn}
             isMobile={this.props.crowi.isMobile}
             isMobile={this.props.crowi.isMobile}
             isUploadable={this.state.isUploadable}
             isUploadable={this.state.isUploadable}
             isUploadableFile={this.state.isUploadableFile}
             isUploadableFile={this.state.isUploadableFile}

+ 20 - 4
src/client/js/components/PageEditor/CodeMirrorEditor.js

@@ -97,6 +97,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
 
   init() {
   init() {
     this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0';
     this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0';
+    this.cmNoCdnScriptRoot = '/js/cdn';
+    this.cmNoCdnStyleRoot = '/styles/cdn';
 
 
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager.addInterceptors([
     this.interceptorManager.addInterceptors([
@@ -308,7 +310,11 @@ export default class CodeMirrorEditor extends AbstractEditor {
    */
    */
   loadTheme(theme) {
   loadTheme(theme) {
     if (!this.loadedThemeSet.has(theme)) {
     if (!this.loadedThemeSet.has(theme)) {
-      this.loadCss(urljoin(this.cmCdnRoot, `theme/${theme}.min.css`));
+      const url = this.props.noCdn
+        ? urljoin(this.cmNoCdnStyleRoot, `codemirror-theme-${theme}.css`)
+        : urljoin(this.cmCdnRoot, `theme/${theme}.min.css`);
+
+      this.loadCss(url);
 
 
       // update Set
       // update Set
       this.loadedThemeSet.add(theme);
       this.loadedThemeSet.add(theme);
@@ -326,12 +332,22 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
 
     // add dependencies
     // add dependencies
     if (this.loadedKeymapSet.size == 0) {
     if (this.loadedKeymapSet.size == 0) {
-      scriptList.push(loadScript(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js')));
-      cssList.push(loadCss(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css')));
+      const dialogScriptUrl = this.props.noCdn
+        ? urljoin(this.cmNoCdnScriptRoot, 'codemirror-dialog.js')
+        : urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js');
+      const dialogStyleUrl = this.props.noCdn
+        ? urljoin(this.cmNoCdnStyleRoot, 'codemirror-dialog.css')
+        : urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css');
+
+      scriptList.push(loadScript(dialogScriptUrl));
+      cssList.push(loadCss(dialogStyleUrl));
     }
     }
     // load keymap
     // load keymap
     if (!this.loadedKeymapSet.has(keymapMode)) {
     if (!this.loadedKeymapSet.has(keymapMode)) {
-      scriptList.push(loadScript(urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`)));
+      const keymapScriptUrl = this.props.noCdn
+        ? urljoin(this.cmNoCdnScriptRoot, `codemirror-keymap-${keymapMode}.js`)
+        : urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`);
+      scriptList.push(loadScript(keymapScriptUrl));
       // update Set
       // update Set
       this.loadedKeymapSet.add(keymapMode);
       this.loadedKeymapSet.add(keymapMode);
     }
     }

+ 1 - 0
src/client/js/components/PageEditor/Editor.jsx

@@ -286,6 +286,7 @@ export default class Editor extends AbstractEditor {
 }
 }
 
 
 Editor.propTypes = Object.assign({
 Editor.propTypes = Object.assign({
+  noCdn: PropTypes.bool,
   isMobile: PropTypes.bool,
   isMobile: PropTypes.bool,
   isUploadable: PropTypes.bool,
   isUploadable: PropTypes.bool,
   isUploadableFile: PropTypes.bool,
   isUploadableFile: PropTypes.bool,

+ 2 - 2
src/client/styles/agile-admin/inverse/eliteadmin.scss

@@ -12,7 +12,7 @@ body {
   margin: 0;
   margin: 0;
   overflow-x: hidden;
   overflow-x: hidden;
   color: $bodytext;
   color: $bodytext;
-  font-weight:300;
+  font-weight:400;
 }
 }
 html {
 html {
     position: relative;
     position: relative;
@@ -23,7 +23,7 @@ h1, h2, h3, h4, h5, h6 {
   color: $headingtext;
   color: $headingtext;
   font-family: $basefont2;
   font-family: $basefont2;
   margin: 10px 0;
   margin: 10px 0;
-  font-weight:300;
+  font-weight:400;
 }
 }
 h1 {
 h1 {
   line-height: 48px;
   line-height: 48px;

+ 156 - 0
src/lib/service/cdn-resources-downloader.js

@@ -0,0 +1,156 @@
+const axios = require('axios');
+const path = require('path');
+const { URL } = require('url');
+const urljoin = require('url-join');
+const fs = require('graceful-fs');
+const mkdirp = require('mkdirp');
+const replaceStream = require('replacestream');
+const streamToPromise = require('stream-to-promise');
+
+
+/**
+ * Value Object
+ */
+class CdnResource {
+  constructor(name, url, outDir) {
+    this.name = name;
+    this.url = url;
+    this.outDir = outDir;
+  }
+}
+
+class CdnResourcesDownloader {
+  constructor() {
+    this.logger = require('@alias/logger')('growi:service:CdnResourcesDownloader');
+  }
+
+  /**
+   * Download script files from CDN
+   * @param {CdnResource[]} cdnResources JavaScript resource data
+   * @param {any} options
+   */
+  async downloadScripts(cdnResources, options) {
+    this.logger.debug('Downloading scripts', cdnResources);
+
+    const opts = Object.assign({}, options);
+    const ext = opts.ext || 'js';
+
+    const promises = cdnResources.map(cdnResource => {
+      this.logger.info(`Processing CdnResource '${cdnResource.name}'`);
+
+      return this.downloadAndWriteToFS(
+        cdnResource.url,
+        cdnResource.outDir,
+        `${cdnResource.name}.${ext}`);
+    });
+
+    return Promise.all(promises);
+  }
+
+  /**
+   * Download style sheet file from CDN
+   *  Assets in CSS is also downloaded
+   * @param {CdnResource[]} cdnResources CSS resource data
+   * @param {any} options
+   */
+  async downloadStyles(cdnResources, options) {
+    this.logger.debug('Downloading styles', cdnResources);
+
+    const opts = Object.assign({}, options);
+    const ext = opts.ext || 'css';
+
+    // styles
+    const assetsResourcesStore = [];
+    const promisesForStyle = cdnResources.map(cdnResource => {
+      this.logger.info(`Processing CdnResource '${cdnResource.name}'`);
+
+      let urlReplacer = null;
+
+      // generate replaceStream instance
+      if (opts.replaceUrl != null) {
+        urlReplacer = this.generateReplaceUrlInCssStream(cdnResource, assetsResourcesStore, opts.replaceUrl.webroot);
+      }
+
+      return this.downloadAndWriteToFS(
+        cdnResource.url,
+        cdnResource.outDir,
+        `${cdnResource.name}.${ext}`,
+        urlReplacer);
+    });
+
+    // wait until all styles are downloaded
+    await Promise.all(promisesForStyle);
+
+    this.logger.debug('Downloading assets', assetsResourcesStore);
+
+    // assets in css
+    const promisesForAssets = assetsResourcesStore.map(cdnResource => {
+      this.logger.info(`Processing assts in css '${cdnResource.name}'`);
+
+      return this.downloadAndWriteToFS(
+        cdnResource.url,
+        cdnResource.outDir,
+        cdnResource.name);
+    });
+
+    return Promise.all(promisesForAssets);
+  }
+
+  /**
+   * Generate replaceStream instance to replace 'url(..)'
+   *
+   * e.g.
+   *  Before  : url(../images/logo.svg)
+   *  After   : url(/path/to/webroot/${cdnResources.name}/logo.svg)
+   *
+   * @param {CdnResource[]} cdnResource CSS resource data
+   * @param {CdnResource[]} assetsResourcesStore An array to store CdnResource that is detected by 'url()' in CSS
+   * @param {string} webroot
+   */
+  generateReplaceUrlInCssStream(cdnResource, assetsResourcesStore, webroot) {
+    return replaceStream(
+      /url\((?!"data:)["']?(.+?)["']?\)/g,    // https://regex101.com/r/Sds38A/2
+      (match, url) => {
+        // generate URL Object
+        const parsedUrl = url.startsWith('http')
+          ? new URL(url)                    // when url is fqcn
+          : new URL(url, cdnResource.url);  // when url is relative
+        const basename = path.basename(parsedUrl.pathname);
+
+        this.logger.debug(`${cdnResource.name} has ${parsedUrl.toString()}`);
+
+        // add assets metadata to download later
+        assetsResourcesStore.push(
+          new CdnResource(
+            basename,
+            parsedUrl.toString(),
+            path.join(cdnResource.outDir, cdnResource.name)
+          )
+        );
+
+        const replaceUrl = urljoin(webroot, cdnResource.name, basename);
+        return `url(${replaceUrl})`;
+      });
+  }
+
+  async downloadAndWriteToFS(url, outDir, fileName, replacestream) {
+    // get
+    const response = await axios.get(url, { responseType: 'stream' });
+    // mkdir -p
+    mkdirp.sync(outDir);
+
+    // replace and write
+    let stream = response.data;
+    if (replacestream != null) {
+      stream = stream.pipe(replacestream);
+    }
+    const file = path.join(outDir, fileName);
+    stream = stream.pipe(fs.createWriteStream(file));
+
+    return streamToPromise(stream);
+  }
+
+}
+
+CdnResourcesDownloader.CdnResource = CdnResource;
+module.exports = CdnResourcesDownloader;

+ 163 - 0
src/lib/service/cdn-resources-service.js

@@ -0,0 +1,163 @@
+const { URL } = require('url');
+const urljoin = require('url-join');
+
+const helpers = require('@commons/util/helpers');
+
+const CdnResourcesDownloader = require('./cdn-resources-downloader');
+const CdnResource = CdnResourcesDownloader.CdnResource;
+
+const cdnLocalScriptRoot = 'public/js/cdn';
+const cdnLocalScriptWebRoot = '/js/cdn';
+const cdnLocalStyleRoot = 'public/styles/cdn';
+const cdnLocalStyleWebRoot = '/styles/cdn';
+
+
+class CdnResourcesService {
+  constructor() {
+    this.logger = require('@alias/logger')('growi:service:CdnResourcesService');
+
+    this.noCdn = !!process.env.NO_CDN;
+    this.loadManifests();
+  }
+
+  loadManifests() {
+    this.cdnManifests = require('@root/resource/cdn-manifests');
+    this.logger.debug('manifest data loaded : ', this.cdnManifests);
+  }
+
+  getScriptManifestByName(name) {
+    const manifests = this.cdnManifests.js
+      .filter(manifest => manifest.name === name);
+
+    return (manifests.length > 0) ? manifests[0] : null;
+  }
+
+  getStyleManifestByName(name) {
+    const manifests = this.cdnManifests.style
+      .filter(manifest => manifest.name === name);
+
+    return (manifests.length > 0) ? manifests[0] : null;
+  }
+
+  async downloadAndWriteAll() {
+    const downloader = new CdnResourcesDownloader();
+
+    const cdnScriptResources = this.cdnManifests.js.map(manifest => {
+      const outDir = helpers.root(cdnLocalScriptRoot);
+      return new CdnResource(manifest.name, manifest.url, outDir);
+    });
+    const cdnStyleResources = this.cdnManifests.style.map(manifest => {
+      const outDir = helpers.root(cdnLocalStyleRoot);
+      return new CdnResource(manifest.name, manifest.url, outDir);
+    });
+
+    const dlStylesOptions = {
+      replaceUrl: {
+        webroot: cdnLocalStyleWebRoot,
+      }
+    };
+
+    return Promise.all([
+      downloader.downloadScripts(cdnScriptResources),
+      downloader.downloadStyles(cdnStyleResources, dlStylesOptions),
+    ]);
+  }
+
+  /**
+   * Generate script tag string
+   *
+   * @param {Object} manifest
+   * @param {boolean} noCdn
+   */
+  generateScriptTag(manifest, noCdn) {
+    const attrs = [];
+    const args = manifest.args || {};
+
+    if (args.async) {
+      attrs.push('async');
+    }
+    if (args.defer) {
+      attrs.push('defer');
+    }
+
+    // TODO process integrity
+
+    const url = noCdn
+      ? urljoin(cdnLocalScriptWebRoot, manifest.name) + '.js'
+      : manifest.url;
+    return `<script src="${url}" ${attrs.join(' ')}></script>`;
+  }
+
+  getScriptTagByName(name) {
+    const manifest = this.getScriptManifestByName(name);
+    return this.generateScriptTag(manifest, this.noCdn);
+  }
+
+  getScriptTagsByGroup(group) {
+    return this.cdnManifests.js
+      .filter(manifest => {
+        return manifest.groups != null && manifest.groups.includes(group);
+      })
+      .map(manifest => {
+        return this.generateScriptTag(manifest, this.noCdn);
+      });
+  }
+
+  /**
+   * Generate style tag string
+   *
+   * @param {Object} manifest
+   * @param {boolean} noCdn
+   */
+  generateStyleTag(manifest, noCdn) {
+    const attrs = [];
+    const args = manifest.args || {};
+
+    if (args.async) {
+      attrs.push('async');
+    }
+    if (args.defer) {
+      attrs.push('defer');
+    }
+
+    // TODO process integrity
+
+    const url = noCdn
+      ? urljoin(cdnLocalStyleWebRoot, manifest.name) + '.css'
+      : manifest.url;
+
+    return `<link rel="stylesheet" href="${url}" ${attrs.join(' ')}>`;
+  }
+
+  getStyleTagByName(name) {
+    const manifest = this.getStyleManifestByName(name);
+    return this.generateStyleTag(manifest, this.noCdn);
+  }
+
+  getStyleTagsByGroup(group) {
+    return this.cdnManifests.style
+      .filter(manifest => {
+        return manifest.groups != null && manifest.groups.includes(group);
+      })
+      .map(manifest => {
+        return this.generateStyleTag(manifest, this.noCdn);
+      });
+  }
+
+  getHighlightJsStyleTag(styleName) {
+    let manifest = this.getStyleManifestByName('highlight-theme-github');
+
+    // replace style
+    if (!this.noCdn) {
+      const url = new URL(`${styleName}.css`, manifest.url);  // resolve `${styleName}.css` from manifest.url
+
+      // clone manifest
+      manifest = Object.assign(manifest, { url: url.toString() });
+    }
+
+    return this.generateStyleTag(manifest, this.noCdn);
+  }
+
+}
+
+module.exports = CdnResourcesService;

+ 19 - 32
src/server/crowi/index.js

@@ -5,6 +5,7 @@ const debug = require('debug')('growi:crowi')
   , logger = require('@alias/logger')('growi:crowi')
   , logger = require('@alias/logger')('growi:crowi')
   , pkg = require('@root/package.json')
   , pkg = require('@root/package.json')
   , InterceptorManager = require('@commons/service/interceptor-manager')
   , InterceptorManager = require('@commons/service/interceptor-manager')
+  , CdnResourcesService = require('@commons/service/cdn-resources-service')
   , Xss = require('@commons/service/xss')
   , Xss = require('@commons/service/xss')
   , path = require('path')
   , path = require('path')
   , sep = path.sep
   , sep = path.sep
@@ -39,6 +40,7 @@ function Crowi(rootdir) {
   this.passportService = null;
   this.passportService = null;
   this.globalNotificationService = null;
   this.globalNotificationService = null;
   this.restQiitaAPIService = null;
   this.restQiitaAPIService = null;
+  this.cdnResourcesService = new CdnResourcesService();
   this.interceptorManager = new InterceptorManager();
   this.interceptorManager = new InterceptorManager();
   this.xss = new Xss();
   this.xss = new Xss();
 
 
@@ -68,38 +70,23 @@ function getMongoUrl(env) {
     ((process.env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi');
     ((process.env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi');
 }
 }
 
 
-Crowi.prototype.init = function() {
-  var self = this;
-
-  return Promise.resolve()
-    .then(function() {
-      // setup database server and load all modesl
-      return self.setupDatabase();
-    }).then(function() {
-      return self.setupModels();
-    }).then(function() {
-      return self.setupSessionConfig();
-    }).then(function() {
-      return self.setupAppConfig();
-    }).then(function() {
-      return self.setupConfigManager();
-    }).then(function() {
-      return self.scanRuntimeVersions();
-    }).then(function() {
-      return self.setupPassport();
-    }).then(function() {
-      return self.setupSearcher();
-    }).then(function() {
-      return self.setupMailer();
-    }).then(function() {
-      return self.setupSlack();
-    }).then(function() {
-      return self.setupCsrf();
-    }).then(function() {
-      return self.setUpGlobalNotification();
-    }).then(function() {
-      return self.setUpRestQiitaAPI();
-    });
+Crowi.prototype.init = async function() {
+  await this.setupDatabase();
+  await this.setupModels();
+  await this.setupSessionConfig();
+  await this.setupAppConfig();
+  await this.setupConfigManager();
+
+  await Promise.all([
+    this.scanRuntimeVersions(),
+    this.setupPassport(),
+    this.setupSearcher(),
+    this.setupMailer(),
+    this.setupSlack(),
+    this.setupCsrf(),
+    this.setUpGlobalNotification(),
+    this.setUpRestQiitaAPI(),
+  ]);
 };
 };
 
 
 Crowi.prototype.isPageId = function(pageId) {
 Crowi.prototype.isPageId = function(pageId) {

+ 1 - 0
src/server/models/config.js

@@ -641,6 +641,7 @@ module.exports = function(crowi) {
         BLOCKDIAG_URI: env.BLOCKDIAG_URI || null,
         BLOCKDIAG_URI: env.BLOCKDIAG_URI || null,
         HACKMD_URI: env.HACKMD_URI || null,
         HACKMD_URI: env.HACKMD_URI || null,
         MATHJAX: env.MATHJAX || null,
         MATHJAX: env.MATHJAX || null,
+        NO_CDN: env.NO_CDN || null,
       },
       },
       recentCreatedLimit: Config.showRecentCreatedNumber(config),
       recentCreatedLimit: Config.showRecentCreatedNumber(config),
       isAclEnabled: !Config.isPublicWikiOnly(config),
       isAclEnabled: !Config.isPublicWikiOnly(config),

+ 26 - 0
src/server/util/swigFunctions.js

@@ -5,6 +5,7 @@ module.exports = function(crowi, app, req, locals) {
     , Config = crowi.model('Config')
     , Config = crowi.model('Config')
     , User = crowi.model('User')
     , User = crowi.model('User')
     , passportService = crowi.passportService
     , passportService = crowi.passportService
+    , cdnResourcesService = crowi.cdnResourcesService
   ;
   ;
 
 
   debug('initializing swigFunctions');
   debug('initializing swigFunctions');
@@ -57,6 +58,31 @@ module.exports = function(crowi, app, req, locals) {
     return Config.globalLang(config);
     return Config.globalLang(config);
   };
   };
 
 
+  locals.noCdn = function() {
+    return !!process.env.NO_CDN;
+  };
+
+  locals.cdnScriptTag = function(name) {
+    return cdnResourcesService.getScriptTagByName(name);
+  };
+  locals.cdnScriptTagsByGroup = function(group) {
+    const tags = cdnResourcesService.getScriptTagsByGroup(group);
+    return tags.join('\n');
+  };
+
+  locals.cdnStyleTag = function(name) {
+    return cdnResourcesService.getStyleTagByName(name);
+  };
+
+  locals.cdnStyleTagsByGroup = function(group) {
+    const tags = cdnResourcesService.getStyleTagsByGroup(group);
+    return tags.join('\n');
+  };
+
+  locals.cdnHighlightJsStyleTag = function(styleName) {
+    return cdnResourcesService.getHighlightJsStyleTag(styleName);
+  };
+
   /**
   /**
    * return true if enabled
    * return true if enabled
    */
    */

+ 7 - 4
src/server/views/admin/customize.html

@@ -25,7 +25,7 @@
 {% block html_additional_headers %}
 {% block html_additional_headers %}
   {% parent %}
   {% parent %}
   <!-- CodeMirror -->
   <!-- CodeMirror -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css">
+  {{ cdnStyleTag('jquery-ui') }}
   <style>
   <style>
     .CodeMirror {
     .CodeMirror {
       border: 1px solid #eee;
       border: 1px solid #eee;
@@ -315,11 +315,12 @@
           <div class="form-group">
           <div class="form-group">
             <label for="settingForm[customize:highlightJsStyle]" class="col-xs-3 control-label">{{ t('customize_page.Theme') }}</label>
             <label for="settingForm[customize:highlightJsStyle]" class="col-xs-3 control-label">{{ t('customize_page.Theme') }}</label>
             <div class="col-xs-9">
             <div class="col-xs-9">
-              <select class="form-control selectpicker" name="settingForm[customize:highlightJsStyle]" onChange="selectHighlightJsStyle(event)">
+              <select class="form-control selectpicker" name="settingForm[customize:highlightJsStyle]" onChange="selectHighlightJsStyle(event)" {% if noCdn() %}disabled{% endif %}>
                 {% for key in Object.keys(highlightJsCssSelectorOptions) %}
                 {% for key in Object.keys(highlightJsCssSelectorOptions) %}
                   <option value={{key}} {% if key == highlightJsStyle() %} selected {% endif %}>{{highlightJsCssSelectorOptions[key].name}}</option>
                   <option value={{key}} {% if key == highlightJsStyle() %} selected {% endif %}>{{highlightJsCssSelectorOptions[key].name}}</option>
                 {% endfor %}
                 {% endfor %}
               </select>
               </select>
+              <p class="help-block text-warning">{{ t('customize_page.nocdn_desc') }}</p>
             </div>
             </div>
           </div>
           </div>
 
 
@@ -339,7 +340,9 @@
             </div>
             </div>
           </div>
           </div>
 
 
-          <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@9.12.0/styles/{{ highlightJsStyle() }}.css" class="highlightJsCss">
+          <div id="highlightJsCssContainer">
+            {{ cdnHighlightJsStyleTag(highlightJsStyle()) }}
+          </div>
 
 
           <p class="help-block">
           <p class="help-block">
             Examples:
             Examples:
@@ -591,7 +594,7 @@ window.addEventListener('load', (event) => {
     hljs.initHighlightingOnLoad()
     hljs.initHighlightingOnLoad()
 
 
     function selectHighlightJsStyle(event) {
     function selectHighlightJsStyle(event) {
-      var highlightJsCssDOM = $(".highlightJsCss")[0]
+      var highlightJsCssDOM = $("#highlightJsCssContainer link")[0]
       // selected value
       // selected value
       var val = event.target.value
       var val = event.target.value
       // replace css url
       // replace css url

+ 5 - 0
src/server/views/layout-crowi/base/layout.html

@@ -2,6 +2,11 @@
 
 
 {% block html_title %}{{ customTitle(path) }}{% endblock %}
 {% block html_title %}{{ customTitle(path) }}{% endblock %}
 
 
+{% block html_additional_headers %}
+  {% parent %}
+  {{ cdnScriptTag('highlight-addons') }}
+{% endblock %}
+
 {% block layout_main %}
 {% block layout_main %}
 <div class="container-fluid">
 <div class="container-fluid">
 
 

+ 5 - 0
src/server/views/layout-growi/base/layout.html

@@ -1,5 +1,10 @@
 {% extends '../../layout/layout.html' %}
 {% extends '../../layout/layout.html' %}
 
 
+{% block html_additional_headers %}
+  {% parent %}
+  {{ cdnScriptTag('highlight-addons') }}
+{% endblock %}
+
 {% block layout_main %}
 {% block layout_main %}
 <div class="container-fluid">
 <div class="container-fluid">
 
 

+ 5 - 0
src/server/views/layout-kibela/base/layout.html

@@ -1,5 +1,10 @@
 {% extends '../../layout/layout.html' %}
 {% extends '../../layout/layout.html' %}
 
 
+{% block html_additional_headers %}
+  {% parent %}
+  {{ cdnScriptTag('highlight-addons') }}
+{% endblock %}
+
 {% block layout_main %}
 {% block layout_main %}
 <div class="container-fluid">
 <div class="container-fluid">
 
 

+ 4 - 27
src/server/views/layout/layout.html

@@ -27,20 +27,7 @@
     }
     }
   </script>
   </script>
 
 
-  <!-- jQuery, emojione, bootstrap -->
-  <script src="https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.3.1,npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
-  <!-- highlight.js -->
-  <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.12.0/build/highlight.min.js"></script>
-  <script src="https://cdn.jsdelivr.net/combine/
-gh/highlightjs/cdn-release@9.12.0/build/languages/dockerfile.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/go.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/gradle.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/json.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/less.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/scss.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/typescript.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
-" defer></script>
+  {{ cdnScriptTagsByGroup('basis') }}
 
 
   {% if local_config.env.MATHJAX %}
   {% if local_config.env.MATHJAX %}
     <!-- Mathjax -->
     <!-- Mathjax -->
@@ -58,7 +45,7 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
         messageStyle: "none"
         messageStyle: "none"
       });
       });
     </script>
     </script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js" async></script>
+    {{ cdnScriptTag('mathjax') }}
   {% endif %}
   {% endif %}
 
 
   {% if env === 'development' %}
   {% if env === 'development' %}
@@ -103,18 +90,8 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
     {% endif %}
     {% endif %}
   {% endblock %}
   {% endblock %}
 
 
-  <!-- Google Fonts -->
-  <link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
-  <!-- Font Awesome -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
-  <!-- Themify Icons -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cd-themify-icons@0.0.1/index.min.css">
-  <!-- Simple Line icons -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-line-icons@2.4.1/css/simple-line-icons.min.css">
-  <!-- emojione -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css">
-  <!-- highlight.js -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@9.12.0/styles/{{ highlightJsStyle() }}.css">
+  {{ cdnStyleTagsByGroup('basis') }}
+  {{ cdnHighlightJsStyleTag(highlightJsStyle()) }}
 
 
   {% block html_additional_headers %}{% endblock %}
   {% block html_additional_headers %}{% endblock %}
 
 

+ 2 - 16
src/server/views/page_presentation.html

@@ -19,20 +19,7 @@
       }
       }
     </script>
     </script>
 
 
-    <!-- jQuery, emojione (expect to hit the cache) -->
-    <script src="https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.3.1"></script>
-    <!-- highlight.js -->
-    <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.12.0/build/highlight.min.js"></script>
-    <script src="https://cdn.jsdelivr.net/combine/
-gh/highlightjs/cdn-release@9.12.0/build/languages/dockerfile.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/go.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/gradle.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/json.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/less.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/scss.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/typescript.min.js,
-gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
-" defer></script>
+    {{ cdnScriptTagsByGroup('basis') }}
 
 
     {% if env === 'development' %}
     {% if env === 'development' %}
       <script src="/dll/dll.js"></script>
       <script src="/dll/dll.js"></script>
@@ -46,8 +33,7 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
     <!-- styles -->
     <!-- styles -->
     <link rel="stylesheet" href="{{ webpack_asset('styles/style-presentation.css') }}">
     <link rel="stylesheet" href="{{ webpack_asset('styles/style-presentation.css') }}">
 
 
-    <!-- Google Fonts -->
-    <link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
+    {{ cdnStyleTagsByGroup('basis') }}
 
 
     <style>
     <style>
       {{ customCss() }}
       {{ customCss() }}

+ 5 - 0
src/server/views/search.html

@@ -1,5 +1,10 @@
 {% extends 'layout/layout.html' %}
 {% extends 'layout/layout.html' %}
 
 
+{% block html_additional_headers %}
+  {% parent %}
+  {{ cdnScriptTag('highlight-addons') }}
+{% endblock %}
+
 {% block html_base_attr %}
 {% block html_base_attr %}
   data-spy="scroll"
   data-spy="scroll"
   data-target="#search-result-list"
   data-target="#search-result-list"

+ 39 - 1
yarn.lock

@@ -409,6 +409,10 @@ ansistyles@~0.1.1:
   version "0.1.3"
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
   resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
 
 
+any-promise@^1.1.0, any-promise@~1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
+
 anymatch@^1.3.0:
 anymatch@^1.3.0:
   version "1.3.2"
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -2944,6 +2948,12 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
   dependencies:
   dependencies:
     once "^1.4.0"
     once "^1.4.0"
 
 
+end-of-stream@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.1.0.tgz#e9353258baa9108965efc41cb0ef8ade2f3cfb07"
+  dependencies:
+    once "~1.3.0"
+
 engine.io-client@~3.1.0:
 engine.io-client@~3.1.0:
   version "3.1.4"
   version "3.1.4"
   resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.1.4.tgz#4fcf1370b47163bd2ce9be2733972430350d4ea1"
   resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.1.4.tgz#4fcf1370b47163bd2ce9be2733972430350d4ea1"
@@ -3082,7 +3092,7 @@ escape-html@~1.0.3:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
 
 
-escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5:
   version "1.0.5"
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 
 
@@ -6453,6 +6463,12 @@ once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
   dependencies:
   dependencies:
     wrappy "1"
     wrappy "1"
 
 
+once@~1.3.0:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20"
+  dependencies:
+    wrappy "1"
+
 onetime@^2.0.0:
 onetime@^2.0.0:
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
   resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@@ -7772,6 +7788,14 @@ repeating@^2.0.0:
   dependencies:
   dependencies:
     is-finite "^1.0.0"
     is-finite "^1.0.0"
 
 
+replacestream@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/replacestream/-/replacestream-4.0.3.tgz#3ee5798092be364b1cdb1484308492cb3dff2f36"
+  dependencies:
+    escape-string-regexp "^1.0.3"
+    object-assign "^4.0.1"
+    readable-stream "^2.0.2"
+
 request@2:
 request@2:
   version "2.83.0"
   version "2.83.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
   resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
@@ -8656,6 +8680,20 @@ stream-throttle@^0.1.3:
     commander "^2.2.0"
     commander "^2.2.0"
     limiter "^1.0.5"
     limiter "^1.0.5"
 
 
+stream-to-array@~2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353"
+  dependencies:
+    any-promise "^1.1.0"
+
+stream-to-promise@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/stream-to-promise/-/stream-to-promise-2.2.0.tgz#b1edb2e1c8cb11289d1b503c08d3f2aef51e650f"
+  dependencies:
+    any-promise "~1.3.0"
+    end-of-stream "~1.1.0"
+    stream-to-array "~2.3.0"
+
 streamsearch@0.1.2:
 streamsearch@0.1.2:
   version "0.1.2"
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
   resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"