All files / lib zip.ts

86.66% Statements 13/15
77.77% Branches 14/18
100% Functions 4/4
100% Lines 12/12

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120                                          4x   6x         6x           6x 6x 3x 3x     6x     4x 4x 4x 4x                                                                                                                                                  
import type { FixturePaths } from '$e2e/filepaths.js';
 
import * as fflate from 'fflate';
 
import { mapValues } from './utils.js';
 
export type ZipEntry = {
	/** ArrayBuffer content is assumed to be a Uint8Array */
	content: string | Uint8Array | ArrayBuffer;
	mtime?: undefined | Date;
	compression?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
};
 
/**
 *
 * @param files maps filepaths inside the archive to their contents
 */
export async function createZipArchive(
	files: Record<string, ZipEntry['content'] | ZipEntry>,
	options: { comment?: string } = {}
): Promise<ArrayBuffer> {
	const entries = mapValues(files, (file): fflate.AsyncZippableFile => {
		const content =
			typeof file === 'string' || file instanceof Uint8Array || file instanceof ArrayBuffer
				? file
				: file.content;
 
		const bytes =
			typeof content === 'string'
				? fflate.strToU8(content)
				: content instanceof ArrayBuffer
					? new Uint8Array(content)
					: content;
 
		const opts: fflate.AsyncZipOptions = {};
		if (typeof file !== 'string') {
			Eif ('compression' in file) opts.level = file.compression;
			Iif ('mtime' in file) opts.mtime = file.mtime;
		}
 
		return [bytes, opts];
	});
 
	return new Promise((resolve, reject) => {
		fflate.zip(entries, options, (error, data) => {
			Iif (error) reject(error);
			else resolve(data.buffer);
		});
	});
}
 
if (import.meta.vitest) {
	const { readFileSync } = await import('node:fs');
 
	const { describe, beforeEach, afterEach, test: _test, expect, vi } = import.meta.vitest;
 
	beforeEach(() => {
		vi.useFakeTimers({ now: new Date(2026, 7, 2) });
	});
 
	afterEach(() => {
		vi.useRealTimers();
	});
 
	const test = _test.extend('testWith', async ({ annotate }) => {
		return async (...args: Parameters<typeof createZipArchive>) => {
			const zip = await createZipArchive(...args);
 
			annotate('created zip file', {
				contentType: 'application/zip',
				body: new Uint8Array(zip),
			});
 
			expect(zip).toMatchSnapshot();
		};
	});
 
	describe('createZipArchive', () => {
		test('empty zip', async ({ testWith }) => {
			await testWith({});
		});
 
		test('one text file', async ({ testWith }) => {
			await testWith({
				'test.txt': 'Hello there :)',
			});
		});
 
		test('a JPEG file (level set to 0)', async ({ testWith }) => {
			await testWith({
				'test.png': {
					content: readFileSync(
						'tests/fixtures/cyan.jpeg' satisfies FixturePaths.Absolute
					),
					compression: 0,
				},
			});
		});
 
		test('nested directories', async ({ testWith }) => {
			await testWith({
				'test.txt': 'hello!!',
				'photos/one.png': {
					content: readFileSync(
						'tests/fixtures/debugsquare.png' satisfies FixturePaths.Absolute
					),
					compression: 0,
				},
				'photos/two.jpeg': {
					content: readFileSync(
						'tests/fixtures/cyan.jpeg' satisfies FixturePaths.Absolute
					),
					compression: 0,
				},
				'photos/README': 'Héllo !! Do accents work?',
			});
		});
	});
}