Skip to content

Conversation

@tylerbutler
Copy link
Member

@tylerbutler tylerbutler commented Dec 3, 2025

This pull request refactors how zip file handling is performed in the build tools, replacing the jszip library with the more lightweight fflate library and introducing a new UnzippedContents abstraction. The changes improve memory efficiency by lazily decompressing files only when accessed, simplify APIs, and update all related code and types to use the new approach. The most important changes are grouped below.

Zip Handling Refactor:

  • Replaced the jszip dependency with fflate for unzipping files, and removed all usage and type references to jszip throughout the codebase.
  • Added a new UnzippedContents type (a Map<string, Buffer>) and implemented a LazyUnzippedContents class that lazily decompresses files on access, improving performance and memory usage.

API and Type Updates:

  • Updated all relevant function signatures and documentation to accept or return UnzippedContents instead of JSZip, including getZipObjectFromArtifact, getStatsFileFromZip, getBundleBuddyConfigFileFromZip, and getBundlePathsFromZipObject.

Code Coverage and Azure DevOps Integration:

  • Refactored code coverage and Azure DevOps artifact handling to use the new UnzippedContents abstraction, including updating logic for iterating over files and reading contents.

@tylerbutler tylerbutler marked this pull request as ready for review December 4, 2025 00:43
@tylerbutler tylerbutler requested review from a team and Copilot December 4, 2025 00:43
Copilot finished reviewing on behalf of tylerbutler December 4, 2025 00:46
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors zip file handling in the build tools by replacing the jszip library with the more lightweight fflate library. The changes introduce a new UnzippedContents type abstraction and implement lazy decompression to improve memory efficiency.

Key Changes:

  • Replaced jszip with fflate for zip file handling and introduced a LazyUnzippedContents class that decompresses files only when accessed
  • Updated all function signatures throughout the codebase to use the new UnzippedContents type (a Map<string, Buffer>) instead of JSZip
  • Simplified iteration patterns from forEach callbacks to for...of loops where applicable

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
build-tools/pnpm-lock.yaml Removed jszip dependency entries from lock file
build-tools/packages/bundle-size-tools/src/utilities/unzipStream.ts Implemented LazyUnzippedContents class and updated unzipStream function to use fflate
build-tools/packages/bundle-size-tools/src/utilities/index.ts Exported new UnzippedContents type
build-tools/packages/bundle-size-tools/src/index.ts Re-exported UnzippedContents type for public API
build-tools/packages/bundle-size-tools/src/ADO/AdoSizeComparator.ts Updated type signature to use UnzippedContents
build-tools/packages/bundle-size-tools/src/ADO/AdoArtifactFileProvider.ts Updated function signatures and implementations to work with UnzippedContents instead of JSZip
build-tools/packages/bundle-size-tools/package.json Removed jszip dependency
build-tools/packages/bundle-size-tools/api-report/bundle-size-tools.api.md Updated API surface to remove JSZip references and add UnzippedContents
build-tools/packages/build-cli/src/library/azureDevops/getBaselineBuildMetrics.ts Updated type references from JSZip to UnzippedContents
build-tools/packages/build-cli/src/codeCoverage/getCoverageMetrics.ts Updated to use UnzippedContents and simplified iteration from forEach to for...of
build-tools/packages/build-cli/package.json Removed jszip dependency
Files not reviewed (1)
  • build-tools/pnpm-lock.yaml: Language not supported


override get size(): number {
return this.fileKeys.size;
}
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LazyUnzippedContents class doesn't override the clear(), delete(), and set() methods from Map. While set() is inherited and works, clear() and delete() could cause inconsistencies between the fileKeys Set and the parent Map's cached entries. Consider either:

  1. Overriding these methods to maintain consistency between fileKeys and the parent Map
  2. Making the class read-only by throwing errors in these methods if mutation is not intended

For example:

override delete(key: string): boolean {
	this.fileKeys.delete(key);
	return super.delete(key);
}

override clear(): void {
	this.fileKeys.clear();
	super.clear();
}
Suggested change
}
}
return this.fileKeys.size;
}
override delete(key: string): boolean {
this.fileKeys.delete(key);
return super.delete(key);
}
override clear(): void {
this.fileKeys.clear();
super.clear();
}

Copilot uses AI. Check for mistakes.

/**
* Downloads an Azure Devops artifacts and parses it with the jszip library.
* Downloads an Azure Devops artifacts and unzips it.
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc has a typo: "Downloads an Azure Devops artifacts" should be "Downloads an Azure DevOps artifact" (singular, and correct capitalization).

Suggested change
* Downloads an Azure Devops artifacts and unzips it.
* Downloads an Azure DevOps artifact and unzips it.

Copilot uses AI. Check for mistakes.
import { type Unzipped, unzip } from "fflate";

/**
* Lazy wrapper around unzipped archive that decompresses files on access.
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding JSDoc documentation for the LazyUnzippedContents class. Since this is an internal implementation detail that implements lazy decompression (a key performance feature mentioned in the PR description), documenting its purpose and behavior would be helpful for future maintainers.

Suggested change
* Lazy wrapper around unzipped archive that decompresses files on access.
* Represents a lazily-decompressed view of a ZIP archive's contents.
*
* This class wraps the result of `fflate.unzip` and exposes a `Map<string, Buffer>`-like interface,
* where each file's contents are only decompressed from the underlying archive on first access.
* This reduces memory usage and improves performance when working with large archives or when
* only a subset of files are needed.
*
* The class overrides several `Map` methods (`get`, `has`, `keys`, `entries`, `values`, `size`)
* to provide lazy decompression and to filter files by an optional path prefix.
*
* @internal
* @extends Map<string, Buffer>
*
* @param unzipped - The raw unzipped archive object from `fflate.unzip`, mapping file paths to Uint8Array contents.
* @param prefix - Optional path prefix to filter files and strip from returned keys.
*
* Example usage:
* const contents = new LazyUnzippedContents(unzipped, "src/");
* const fileBuffer = contents.get("index.js"); // Decompresses "src/index.js" on first access

Copilot uses AI. Check for mistakes.

override get size(): number {
return this.fileKeys.size;
}
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LazyUnzippedContents class extends Map<string, Buffer> but doesn't override the forEach method. This means that if someone calls forEach on an instance, it will use the parent Map's implementation which will iterate over the cached entries in the parent Map, potentially missing entries that haven't been lazily loaded yet. Consider adding an override:

override forEach(callbackfn: (value: Buffer, key: string, map: Map<string, Buffer>) => void, thisArg?: any): void {
	for (const key of this.fileKeys) {
		callbackfn.call(thisArg, this.get(key)!, key, this);
	}
}
Suggested change
}
}
override forEach(
callbackfn: (value: Buffer, key: string, map: Map<string, Buffer>) => void,
thisArg?: any,
): void {
for (const key of this.fileKeys) {
callbackfn.call(thisArg, this.get(key)!, key, this);
}
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@alexvy86 alexvy86 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I find it confusing that what used to be a zip file, with variables and functions named accordingly, now is an UnzippedContents in many places, still using variable and functions that suggest it's a zip file.

Comment on lines +44 to +47
// Decompress on first access
const fullPath = this.prefix ? `${this.prefix}${key}` : key;
const buffer = Buffer.from(this.unzipped[fullPath]);
super.set(key, buffer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really "decompressing on access", is it? Feels like the constructor already receives the unzipped contents and this is just wrapping them somehow. I'm pretty skeptical about this class, and in general my gut feeling is that for anything in build-tools sync patterns are almost always better for simplicity and ease of use, because few places can really leverage async-ness much, and in most cases the gains are negligible in context (saving a few ms-to-seconds when running a tool in the pipeline).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The challenge imo is not the sync/async but the need to not load the whole thing into memory. I worry that our files in ci are too large to decompress everything in memory.

I guess this is why we might stick with jszip. It's just yet another decompression lib to maintain and understand.

@tylerbutler
Copy link
Member Author

Marking as a draft while I write some tests separately so we can be more confident in this change.

@tylerbutler tylerbutler marked this pull request as draft December 4, 2025 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants