cdn-resources-downloader.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import path from 'path';
  2. import { URL } from 'url';
  3. import urljoin from 'url-join';
  4. import { Transform } from 'stream';
  5. import replaceStream from 'replacestream';
  6. import { cdnLocalScriptRoot, cdnLocalStyleRoot, cdnLocalStyleWebRoot } from '^/config/cdn';
  7. import * as cdnManifests from '^/resource/cdn-manifests';
  8. import { CdnResource, CdnManifest } from '~/interfaces/cdn';
  9. import loggerFactory from '~/utils/logger';
  10. import { downloadTo } from '~/utils/download';
  11. const logger = loggerFactory('growi:service:CdnResourcesDownloader');
  12. export default class CdnResourcesDownloader {
  13. async downloadAndWriteAll(): Promise<any> {
  14. const cdnScriptResources: CdnResource[] = cdnManifests.js.map((manifest: CdnManifest) => {
  15. return { manifest, outDir: cdnLocalScriptRoot };
  16. });
  17. const cdnStyleResources: CdnResource[] = cdnManifests.style.map((manifest) => {
  18. return { manifest, outDir: cdnLocalStyleRoot };
  19. });
  20. const dlStylesOptions = {
  21. replaceUrl: {
  22. webroot: cdnLocalStyleWebRoot,
  23. },
  24. };
  25. return Promise.all([
  26. this.downloadScripts(cdnScriptResources),
  27. this.downloadStyles(cdnStyleResources, dlStylesOptions),
  28. ]);
  29. }
  30. /**
  31. * Download script files from CDN
  32. * @param cdnResources JavaScript resource data
  33. * @param options
  34. */
  35. private async downloadScripts(cdnResources: CdnResource[], options?: any): Promise<any> {
  36. logger.debug('Downloading scripts', cdnResources);
  37. const opts = Object.assign({}, options);
  38. const ext = opts.ext || 'js';
  39. const promises = cdnResources.map((cdnResource) => {
  40. const { manifest } = cdnResource;
  41. logger.info(`Processing CdnResource '${manifest.name}'`);
  42. return downloadTo(
  43. manifest.url,
  44. cdnResource.outDir,
  45. `${manifest.name}.${ext}`,
  46. );
  47. });
  48. return Promise.all(promises);
  49. }
  50. /**
  51. * Download style sheet file from CDN
  52. * Assets in CSS is also downloaded
  53. * @param cdnResources CSS resource data
  54. * @param options
  55. */
  56. private async downloadStyles(cdnResources: CdnResource[], options?: any): Promise<any> {
  57. logger.debug('Downloading styles', cdnResources);
  58. const opts = Object.assign({}, options);
  59. const ext = opts.ext || 'css';
  60. // styles
  61. const assetsResourcesStore: CdnResource[] = [];
  62. const promisesForStyle = cdnResources.map((cdnResource) => {
  63. const { manifest } = cdnResource;
  64. logger.info(`Processing CdnResource '${manifest.name}'`);
  65. let urlReplacer: Transform|null = null;
  66. // generate replaceStream instance
  67. if (opts.replaceUrl != null) {
  68. urlReplacer = this.generateReplaceUrlInCssStream(cdnResource, assetsResourcesStore, opts.replaceUrl.webroot);
  69. }
  70. return downloadTo(
  71. manifest.url,
  72. cdnResource.outDir,
  73. `${manifest.name}.${ext}`,
  74. urlReplacer,
  75. );
  76. });
  77. // wait until all styles are downloaded
  78. await Promise.all(promisesForStyle);
  79. logger.debug('Downloading assets', assetsResourcesStore);
  80. // assets in css
  81. const promisesForAssets = assetsResourcesStore.map((cdnResource) => {
  82. const { manifest } = cdnResource;
  83. logger.info(`Processing assts in css '${manifest.name}'`);
  84. return downloadTo(
  85. manifest.url,
  86. cdnResource.outDir,
  87. manifest.name,
  88. );
  89. });
  90. return Promise.all(promisesForAssets);
  91. }
  92. /**
  93. * Generate replaceStream instance to replace 'url(..)'
  94. *
  95. * e.g.
  96. * Before : url(../images/logo.svg)
  97. * After : url(/path/to/webroot/${cdnResources.name}/logo.svg)
  98. *
  99. * @param cdnResource CSS resource data
  100. * @param assetsResourcesStore An array to store CdnResource that is detected by 'url()' in CSS
  101. * @param webroot
  102. */
  103. private generateReplaceUrlInCssStream(cdnResource: CdnResource, assetsResourcesStore: CdnResource[], webroot: string): Transform {
  104. return replaceStream(
  105. /url\((?!['"]?data:)["']?(.+?)["']?\)/g, // https://regex101.com/r/Sds38A/3
  106. (match, url) => {
  107. // generate URL Object
  108. const parsedUrl = url.startsWith('http')
  109. ? new URL(url) // when url is fqcn
  110. : new URL(url, cdnResource.manifest.url); // when url is relative
  111. const basename = path.basename(parsedUrl.pathname);
  112. logger.debug(`${cdnResource.manifest.name} has ${parsedUrl.toString()}`);
  113. // add assets metadata to download later
  114. const replacedCdnResource = {
  115. manifest: {
  116. name: basename,
  117. url: parsedUrl.toString(),
  118. },
  119. outDir: path.join(cdnResource.outDir, cdnResource.manifest.name),
  120. };
  121. assetsResourcesStore.push(replacedCdnResource);
  122. const replaceUrl = urljoin(webroot, cdnResource.manifest.name, basename);
  123. return `url(${replaceUrl})`;
  124. },
  125. );
  126. }
  127. }