index.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { createReadStream, createWriteStream, existsSync, mkdirSync } from 'node:fs';
  2. import { posix, resolve } from 'node:path';
  3. import glob from 'fast-glob';
  4. import { AsyncZipDeflate, Zip } from 'fflate';
  5. // Converts bytes to megabytes
  6. function toMB(bytes: number): number {
  7. return bytes / 1024 / 1024;
  8. }
  9. // Creates the build directory if it doesn't exist
  10. function ensureBuildDirectoryExists(buildDirectory: string): void {
  11. if (!existsSync(buildDirectory)) {
  12. mkdirSync(buildDirectory, { recursive: true });
  13. }
  14. }
  15. // Logs the package size and duration
  16. function logPackageSize(size: number, startTime: number): void {
  17. console.log(`Zip Package size: ${toMB(size).toFixed(2)} MB in ${Date.now() - startTime}ms`);
  18. }
  19. // Handles file streaming and zipping
  20. function streamFileToZip(
  21. absPath: string,
  22. relPath: string,
  23. zip: Zip,
  24. onAbort: () => void,
  25. onError: (error: Error) => void,
  26. ): void {
  27. const data = new AsyncZipDeflate(relPath, { level: 9 });
  28. zip.add(data);
  29. createReadStream(absPath)
  30. .on('data', (chunk: Buffer) => data.push(chunk, false))
  31. .on('end', () => data.push(new Uint8Array(0), true))
  32. .on('error', error => {
  33. onAbort();
  34. onError(error);
  35. });
  36. }
  37. // Zips the bundle
  38. export const zipBundle = async (
  39. {
  40. distDirectory,
  41. buildDirectory,
  42. archiveName,
  43. }: {
  44. distDirectory: string;
  45. buildDirectory: string;
  46. archiveName: string;
  47. },
  48. withMaps = false,
  49. ): Promise<void> => {
  50. ensureBuildDirectoryExists(buildDirectory);
  51. const zipFilePath = resolve(buildDirectory, archiveName);
  52. const output = createWriteStream(zipFilePath);
  53. const fileList = await glob(
  54. [
  55. '**/*', // Pick all nested files
  56. ...(!withMaps ? ['!**/(*.js.map|*.css.map)'] : []), // Exclude source maps conditionally
  57. ],
  58. {
  59. cwd: distDirectory,
  60. onlyFiles: true,
  61. },
  62. );
  63. return new Promise<void>((pResolve, pReject) => {
  64. let aborted = false;
  65. let totalSize = 0;
  66. const timer = Date.now();
  67. const zip = new Zip((err, data, final) => {
  68. if (err) {
  69. pReject(err);
  70. } else {
  71. totalSize += data.length;
  72. output.write(data);
  73. if (final) {
  74. logPackageSize(totalSize, timer);
  75. output.end();
  76. pResolve();
  77. }
  78. }
  79. });
  80. // Handle file read streams
  81. for (const file of fileList) {
  82. if (aborted) return;
  83. const absPath = resolve(distDirectory, file);
  84. const absPosixPath = posix.resolve(distDirectory, file);
  85. const relPosixPath = posix.relative(distDirectory, absPosixPath);
  86. console.log(`Adding file: ${relPosixPath}`);
  87. streamFileToZip(
  88. absPath,
  89. relPosixPath,
  90. zip,
  91. () => {
  92. aborted = true;
  93. zip.terminate();
  94. },
  95. error => pReject(`Error reading file ${absPath}: ${error.message}`),
  96. );
  97. }
  98. zip.end();
  99. });
  100. };