compression.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { promises as fs } from 'fs';
  2. import path from 'path';
  3. import zlib from 'zlib';
  4. import { promisify } from 'util';
  5. import type { Plugin, ResolvedConfig } from 'vite';
  6. const gzip = promisify(zlib.gzip);
  7. const brotliCompress = promisify(zlib.brotliCompress);
  8. const compressibleFileRE = /\.(js|mjs|json|css|html)$/i;
  9. const defaultThreshold = 1025;
  10. type CompressionKind = 'gzip' | 'brotli';
  11. const compressionHandlers: Record<CompressionKind, { ext: string; compress: (content: Buffer) => Promise<Buffer> }> = {
  12. gzip: {
  13. ext: '.gz',
  14. compress: (content) => gzip(content, { level: zlib.constants.Z_BEST_COMPRESSION })
  15. },
  16. brotli: {
  17. ext: '.br',
  18. compress: (content) =>
  19. brotliCompress(content, {
  20. params: {
  21. [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
  22. [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT
  23. }
  24. })
  25. }
  26. };
  27. async function collectFiles(rootDir: string): Promise<string[]> {
  28. const entries = await fs.readdir(rootDir, { withFileTypes: true });
  29. const files = await Promise.all(
  30. entries.map(async (entry) => {
  31. const fullPath = path.join(rootDir, entry.name);
  32. if (entry.isDirectory()) {
  33. return collectFiles(fullPath);
  34. }
  35. return compressibleFileRE.test(entry.name) ? [fullPath] : [];
  36. })
  37. );
  38. return files.flat();
  39. }
  40. function createCompressionPlugin(kind: CompressionKind): Plugin {
  41. const handler = compressionHandlers[kind];
  42. let config: ResolvedConfig | undefined;
  43. return {
  44. name: `local:compression:${kind}`,
  45. apply: 'build',
  46. enforce: 'post',
  47. configResolved(resolvedConfig) {
  48. config = resolvedConfig;
  49. },
  50. async closeBundle() {
  51. const outputDir = path.resolve(process.cwd(), config?.build.outDir ?? 'dist');
  52. const files = await collectFiles(outputDir);
  53. const compressedEntries: Array<{ file: string; originalKb: string; compressedKb: string }> = [];
  54. await Promise.all(
  55. files.map(async (filePath) => {
  56. const stat = await fs.stat(filePath);
  57. if (stat.size < defaultThreshold) {
  58. return;
  59. }
  60. const content = await fs.readFile(filePath);
  61. const compressed = await handler.compress(content);
  62. const outputFile = `${filePath}${handler.ext}`;
  63. await fs.writeFile(outputFile, compressed);
  64. compressedEntries.push({
  65. file: path.relative(outputDir, outputFile).replaceAll('\\', '/'),
  66. originalKb: (stat.size / 1024).toFixed(2),
  67. compressedKb: (compressed.byteLength / 1024).toFixed(2)
  68. });
  69. })
  70. );
  71. if (!compressedEntries.length) {
  72. return;
  73. }
  74. compressedEntries.sort((a, b) => a.file.localeCompare(b.file));
  75. config?.logger.info(`\n[compression:${kind}] generated ${compressedEntries.length} files`);
  76. for (const entry of compressedEntries) {
  77. config?.logger.info(`${path.basename(outputDir)}/${entry.file} ${entry.originalKb}kb -> ${entry.compressedKb}kb`);
  78. }
  79. config?.logger.info('');
  80. }
  81. };
  82. }
  83. export default (env: Record<string, string>) => {
  84. const { VITE_BUILD_COMPRESS } = env;
  85. const plugins: Plugin[] = [];
  86. if (!VITE_BUILD_COMPRESS) {
  87. return plugins;
  88. }
  89. const compressionList = VITE_BUILD_COMPRESS.split(',').map((item) => item.trim()) as CompressionKind[];
  90. if (compressionList.includes('gzip')) {
  91. plugins.push(createCompressionPlugin('gzip'));
  92. }
  93. if (compressionList.includes('brotli')) {
  94. plugins.push(createCompressionPlugin('brotli'));
  95. }
  96. return plugins;
  97. };