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

WIP: devide CdnResourcesService to CdnResourcesDownloader

Yuki Takei 7 лет назад
Родитель
Сommit
1a33ebbc13
3 измененных файлов с 193 добавлено и 101 удалено
  1. 0 0
      resource/cdn-manifests.js
  2. 141 0
      src/lib/service/cdn-resources-downloader.js
  3. 52 101
      src/lib/service/cdn-resources-service.js

+ 0 - 0
resource/cdn-resources.js → resource/cdn-manifests.js


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

@@ -0,0 +1,141 @@
+const axios = require('axios');
+const path = require('path');
+const { URL } = require('url');
+const urljoin = require('url-join');
+const fs = require('graceful-fs');
+const replaceStream = require('replacestream');
+
+
+/**
+ * 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) {
+    const opts = Object.assign({}, options);
+    const ext = opts.ext || 'js';
+
+    const promises = cdnResources.map(cdnResource => {
+      return this.downloadAndWriteToFS(
+        cdnResource.url,
+        path.join(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) {
+    const opts = Object.assign({}, options);
+    const ext = opts.ext || 'css';
+
+    // styles
+    const assetsResourcesStore = [];
+    const promisesForStyle = cdnResources.map(cdnResource => {
+      let urlReplacer = null;
+
+      // generate replaceStream instance
+      if (opts.replaceUrl != null) {
+        urlReplacer = this.generateReplaceUrlInCssStream(cdnResource, assetsResourcesStore, opts.replaceUrl.webroot);
+      }
+
+      return this.downloadAndWriteToFS(
+        cdnResource.url,
+        path.join(cdnResource.outDir, `${cdnResource.name}.${ext}`),
+        urlReplacer);
+    });
+
+
+    // wait until all styles are downloaded
+    await Promise.all(promisesForStyle);
+
+    this.logger.info(assetsResourcesStore);
+
+    // assets in css
+    const promisesForAssets = assetsResourcesStore.map(cdnResource => {
+      // create dir if dir does not exist
+      if (!fs.existsSync(cdnResource.outDir)) {
+        fs.mkdirSync(cdnResource.outDir);
+      }
+
+      return this.downloadAndWriteToFS(
+        cdnResource.url,
+        path.join(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.info(cdnResource.name, 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, file, replacestream) {
+    // get
+    const response = await axios.get(url, { responseType: 'stream' });
+    // replace and write
+    let stream = response.data;
+    if (replacestream != null) {
+      stream = response.data.pipe(replacestream);
+    }
+    return stream.pipe(fs.createWriteStream(file));
+  }
+
+}
+
+CdnResourcesDownloader.CdnResource = CdnResource;
+module.exports = CdnResourcesDownloader;

+ 52 - 101
src/lib/service/cdn-resources-service.js

@@ -1,111 +1,62 @@
-const axios = require('axios');
-const path = require('path');
-const { URL } = require('url');
 const urljoin = require('url-join');
 const urljoin = require('url-join');
-const fs = require('graceful-fs');
-const replaceStream = require('replacestream');
 
 
 const helpers = require('@commons/util/helpers');
 const helpers = require('@commons/util/helpers');
+
+const CdnResourcesDownloader = require('./cdn-resources-downloader');
+const CdnResource = CdnResourcesDownloader.CdnResource;
+
 const cdnLocalScriptRoot = 'public/js/cdn';
 const cdnLocalScriptRoot = 'public/js/cdn';
 const cdnLocalScriptWebRoot = '/js/cdn';
 const cdnLocalScriptWebRoot = '/js/cdn';
 const cdnLocalStyleRoot = 'public/styles/cdn';
 const cdnLocalStyleRoot = 'public/styles/cdn';
 const cdnLocalStyleWebRoot = '/styles/cdn';
 const cdnLocalStyleWebRoot = '/styles/cdn';
 
 
+
 class CdnResourcesService {
 class CdnResourcesService {
   constructor() {
   constructor() {
-    this.logger = require('@alias/logger')('growi:service:CdnResourcesResolver');
+    this.logger = require('@alias/logger')('growi:service:CdnResourcesService');
 
 
     this.noCdn = !!process.env.NO_CDN;
     this.noCdn = !!process.env.NO_CDN;
-    this.loadMetaData();
+    this.loadManifests();
   }
   }
 
 
-  loadMetaData() {
-    this.cdnResources = require('@root/resource/cdn-resources');
-    this.logger.debug('meta data loaded : ', this.cdnResources);
+  loadManifests() {
+    this.cdnManifests = require('@root/resource/cdn-manifests');
+    this.logger.debug('manifest data loaded : ', this.cdnManifests);
   }
   }
 
 
-  async downloadAndWrite(url, file, replacestream) {
-    // get
-    const response = await axios.get(url, { responseType: 'stream' });
-    // replace and write
-    let stream = response.data;
-    if (replacestream != null) {
-      stream = response.data.pipe(replacestream);
-    }
-    return stream.pipe(fs.createWriteStream(file));
-  }
+  async downloadAndWriteAll() {
+    const downloader = new CdnResourcesDownloader();
 
 
-  async downloadAndWriteScripts() {
-    const promisesForScript = this.cdnResources.js.map(resource => {
-      return this.downloadAndWrite(
-        resource.url,
-        helpers.root(cdnLocalScriptRoot, `${resource.name}.js`));
+    const cdnScriptResources = this.cdnManifests.js.map(manifest => {
+      const outDir = helpers.root(cdnLocalScriptRoot);
+      return new CdnResource(manifest.name, manifest.url, outDir);
     });
     });
-
-    return Promise.all(promisesForScript);
-  }
-
-  async downloadAndWriteStyles() {
-    // styles
-    const assets = [];
-    const promisesForStyle = this.cdnResources.style.map(resource => {
-      const urlReplacer = replaceStream(
-        /url\((?!"data:)["']?(.+?)["']?\)/g,    // https://regex101.com/r/Sds38A/2
-        (match, relativeUrl) => {
-          // get basename
-          const parsedUrl = new URL(relativeUrl, resource.url);
-          const basename = path.basename(parsedUrl.pathname);
-
-          // add assets metadata to download later
-          assets.push({
-            url: parsedUrl.toString(),
-            dir: helpers.root(cdnLocalStyleRoot, resource.name),
-            basename: basename,
-          });
-
-          const replaceUrl = urljoin(cdnLocalStyleWebRoot, resource.name, basename);
-          return `url(${replaceUrl})`;
-        });
-
-      return this.downloadAndWrite(
-        resource.url,
-        helpers.root(cdnLocalStyleRoot, `${resource.name}.css`),
-        urlReplacer);
+    const cdnStyleResources = this.cdnManifests.style.map(manifest => {
+      const outDir = helpers.root(cdnLocalStyleRoot);
+      return new CdnResource(manifest.name, manifest.url, outDir);
     });
     });
 
 
-    await Promise.all(promisesForStyle);
-
-    // assets in css
-    const promisesForAssets = assets.map(resource => {
-      // create dir if dir does not exist
-      if (!fs.existsSync(resource.dir)) {
-        fs.mkdirSync(resource.dir);
+    const dlStylesOptions = {
+      replaceUrl: {
+        webroot: cdnLocalStyleWebRoot,
       }
       }
+    };
 
 
-      return this.downloadAndWrite(
-        resource.url,
-        path.join(resource.dir, resource.basename));
-    });
-
-    return Promise.all(promisesForAssets);
-  }
-
-  async downloadAndWriteAll() {
     return Promise.all([
     return Promise.all([
-      this.downloadAndWriteScripts(),
-      this.downloadAndWriteStyles(),
+      downloader.downloadScripts(cdnScriptResources),
+      downloader.downloadStyles(cdnStyleResources, dlStylesOptions),
     ]);
     ]);
   }
   }
 
 
   /**
   /**
    * Generate script tag string
    * Generate script tag string
    *
    *
-   * @param {Object} resource
+   * @param {Object} manifest
    * @param {boolean} noCdn
    * @param {boolean} noCdn
    */
    */
-  generateScriptTag(resource, noCdn) {
+  generateScriptTag(manifest, noCdn) {
     const attrs = [];
     const attrs = [];
-    const args = resource.args || {};
+    const args = manifest.args || {};
 
 
     if (args.async) {
     if (args.async) {
       attrs.push('async');
       attrs.push('async');
@@ -117,39 +68,39 @@ class CdnResourcesService {
     // TODO process integrity
     // TODO process integrity
 
 
     const url = noCdn
     const url = noCdn
-      ? urljoin(cdnLocalScriptWebRoot, resource.name) + '.js'
-      : resource.url;
+      ? urljoin(cdnLocalScriptWebRoot, manifest.name) + '.js'
+      : manifest.url;
     return `<script src="${url}" ${attrs.join(' ')}></script>`;
     return `<script src="${url}" ${attrs.join(' ')}></script>`;
   }
   }
 
 
   getScriptTagByName(name) {
   getScriptTagByName(name) {
-    const tags = this.cdnResources.js
-      .filter(resource => resource.name === name)
-      .map(resource => {
-        return this.generateScriptTag(resource, this.noCdn);
+    const tags = this.cdnManifests.js
+      .filter(manifest => manifest.name === name)
+      .map(manifest => {
+        return this.generateScriptTag(manifest, this.noCdn);
       });
       });
     return tags[0];
     return tags[0];
   }
   }
 
 
   getScriptTagsByGroup(group) {
   getScriptTagsByGroup(group) {
-    return this.cdnResources.js
-      .filter(resource => {
-        return resource.groups != null && resource.groups.includes(group);
+    return this.cdnManifests.js
+      .filter(manifest => {
+        return manifest.groups != null && manifest.groups.includes(group);
       })
       })
-      .map(resource => {
-        return this.generateScriptTag(resource, this.noCdn);
+      .map(manifest => {
+        return this.generateScriptTag(manifest, this.noCdn);
       });
       });
   }
   }
 
 
   /**
   /**
    * Generate style tag string
    * Generate style tag string
    *
    *
-   * @param {Object} resource
+   * @param {Object} manifest
    * @param {boolean} noCdn
    * @param {boolean} noCdn
    */
    */
-  generateStyleTag(resource, noCdn) {
+  generateStyleTag(manifest, noCdn) {
     const attrs = [];
     const attrs = [];
-    const args = resource.args || {};
+    const args = manifest.args || {};
 
 
     if (args.async) {
     if (args.async) {
       attrs.push('async');
       attrs.push('async');
@@ -161,28 +112,28 @@ class CdnResourcesService {
     // TODO process integrity
     // TODO process integrity
 
 
     const url = noCdn
     const url = noCdn
-      ? urljoin(cdnLocalStyleWebRoot, resource.name) + '.css'
-      : resource.url;
+      ? urljoin(cdnLocalStyleWebRoot, manifest.name) + '.css'
+      : manifest.url;
 
 
     return `<link rel="stylesheet" href="${url}" ${attrs.join(' ')}>`;
     return `<link rel="stylesheet" href="${url}" ${attrs.join(' ')}>`;
   }
   }
 
 
   getStyleTagByName(name) {
   getStyleTagByName(name) {
-    const tags = this.cdnResources.style
-      .filter(resource => resource.name === name)
-      .map(resource => {
-        return this.generateStyleTag(resource, this.noCdn);
+    const tags = this.cdnManifests.style
+      .filter(manifest => manifest.name === name)
+      .map(manifest => {
+        return this.generateStyleTag(manifest, this.noCdn);
       });
       });
     return tags[0];
     return tags[0];
   }
   }
 
 
   getStyleTagsByGroup(group) {
   getStyleTagsByGroup(group) {
-    return this.cdnResources.style
-      .filter(resource => {
-        return resource.groups != null && resource.groups.includes(group);
+    return this.cdnManifests.style
+      .filter(manifest => {
+        return manifest.groups != null && manifest.groups.includes(group);
       })
       })
-      .map(resource => {
-        return this.generateStyleTag(resource, this.noCdn);
+      .map(manifest => {
+        return this.generateStyleTag(manifest, this.noCdn);
       });
       });
   }
   }
 }
 }