UNPKG

@shopify/cli-kit

Version:

A set of utilities, interfaces, and models that are common across all the platform features

138 lines 5.68 kB
import { relativePath, joinPath, dirname } from './path.js'; import { glob, removeFile } from './fs.js'; import { outputDebug, outputContent, outputToken } from '../../public/node/output.js'; import archiver from 'archiver'; import { createWriteStream, readFileSync, writeFileSync } from 'fs'; import { readFile } from 'fs/promises'; import { tmpdir } from 'os'; import { randomUUID } from 'crypto'; /** * It zips a directory and by default normalizes the paths to be forward-slash. * Even with forward-slash paths, zip files should still be able to be opened on * Windows. * * @param options - ZipOptions. */ export async function zip(options) { const { inputDirectory, outputZipPath, matchFilePattern = '**/*' } = options; outputDebug(outputContent `Zipping ${outputToken.path(inputDirectory)} into ${outputToken.path(outputZipPath)}`); const pathsToZip = await glob(matchFilePattern, { cwd: inputDirectory, absolute: true, dot: true, followSymbolicLinks: false, }); return new Promise((resolve, reject) => { const archive = archiver('zip'); const output = createWriteStream(outputZipPath); output.on('close', () => { resolve(); }); archive.on('error', (error) => { reject(error); }); archive.pipe(output); const directoriesToAdd = new Set(); for (const filePath of pathsToZip) { const fileRelativePath = relativePath(inputDirectory, filePath); collectParentDirectories(fileRelativePath, directoriesToAdd); } const sortedDirs = Array.from(directoriesToAdd).sort((left, right) => left.localeCompare(right)); for (const dir of sortedDirs) { const dirName = dir.endsWith('/') ? dir : `${dir}/`; archive.append(Buffer.alloc(0), { name: dirName }); } // Read all files immediately before adding to archive to prevent ENOENT errors // Using archive.file() causes lazy loading which fails if files are deleted before finalize() const addFilesPromises = pathsToZip.map(async (filePath) => { await archiveFile(inputDirectory, filePath, archive); }); // Wait for all files to be read and added before finalizing Promise.all(addFilesPromises) .then(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises archive.finalize(); }) .catch((error) => { // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors reject(error); }); }); } function collectParentDirectories(fileRelativePath, accumulator) { let currentDir = dirname(fileRelativePath); while (currentDir && currentDir !== '.' && currentDir !== '/') { accumulator.add(currentDir); const parent = dirname(currentDir); if (parent === currentDir) break; currentDir = parent; } } async function archiveFile(inputDirectory, filePath, archive) { const fileRelativePath = relativePath(inputDirectory, filePath); if (!filePath || !fileRelativePath) return; // Read file content immediately to avoid race conditions const fileContent = await readFile(filePath); // Use append with Buffer instead of file() to avoid lazy file reading archive.append(fileContent, { name: fileRelativePath }); } /** * It compresses a directory with Brotli. * First creates a tar archive to preserve directory structure, * then compresses it with Brotli. * * @param options - BrotliOptions. */ export async function brotliCompress(options) { const tempTarPath = joinPath(tmpdir(), `${randomUUID()}.tar`); try { // Create tar archive using archiver await new Promise((resolve, reject) => { const archive = archiver('tar'); const output = createWriteStream(tempTarPath); output.on('close', () => resolve()); archive.on('error', (error) => reject(error)); archive.pipe(output); glob(options.matchFilePattern ?? '**/*', { cwd: options.inputDirectory, absolute: true, dot: true, followSymbolicLinks: false, }) .then(async (pathsToZip) => { // Read all files immediately to prevent ENOENT errors during race conditions const addFilesPromises = pathsToZip.map(async (filePath) => { await archiveFile(options.inputDirectory, filePath, archive); }); await Promise.all(addFilesPromises); // eslint-disable-next-line @typescript-eslint/no-floating-promises archive.finalize(); }) .catch((error) => { reject(error instanceof Error ? error : new Error(String(error))); }); }); const tarContent = readFileSync(tempTarPath); const brotli = await import('brotli'); const compressed = brotli.default.compress(tarContent, { quality: 7, mode: 0, }); if (!compressed) { throw new Error('Brotli compression failed'); } writeFileSync(options.outputPath, compressed); } finally { try { await removeFile(tempTarPath); // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { outputDebug(outputContent `Failed to clean up temporary file: ${outputToken.path(tempTarPath)}`); } } } //# sourceMappingURL=archiver.js.map