Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion build-tools/packages/build-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@
"issue-parser": "^7.0.1",
"json5": "^2.2.3",
"jssm": "^5.104.2",
"jszip": "^3.10.1",
"latest-version": "^9.0.0",
"mdast": "^3.0.0",
"mdast-util-heading-range": "^4.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/

import type JSZip from "jszip";
import type { UnzippedContents } from "@fluidframework/bundle-size-tools";
import { Parser } from "xml2js";
import type { CommandLogger } from "../logging.js";

Expand Down Expand Up @@ -56,20 +56,20 @@ const extractCoverageMetrics = (

/**
* Method that returns the coverage report for the build from the artifact.
* @param baselineZip - zipped coverage files for the build
* @param artifactZip - unzipped coverage files for the build
* @param logger - The logger to log messages.
* @returns an map of coverage metrics for build containing packageName, lineCoverage and branchCoverage
*/
export const getCoverageMetricsFromArtifact = async (
artifactZip: JSZip,
artifactZip: UnzippedContents,
logger?: CommandLogger,
): Promise<Map<string, CoverageMetric>> => {
const coverageReportsFiles: string[] = [];
// eslint-disable-next-line unicorn/no-array-for-each -- required as JSZip does not implement [Symbol.iterator]() which is required by for...of
artifactZip.forEach((filePath) => {
if (filePath.endsWith("cobertura-coverage-patched.xml"))
for (const filePath of artifactZip.keys()) {
if (filePath.endsWith("cobertura-coverage-patched.xml")) {
coverageReportsFiles.push(filePath);
});
}
}

let coverageMetricsForBaseline: Map<string, CoverageMetric> = new Map();
const xmlParser = new Parser();
Expand All @@ -78,29 +78,22 @@ export const getCoverageMetricsFromArtifact = async (
logger?.info(`${coverageReportsFiles.length} coverage data files found.`);

for (const coverageReportFile of coverageReportsFiles) {
const jsZipObject = artifactZip.file(coverageReportFile);
if (jsZipObject === undefined) {
const coverageReportXML = artifactZip.get(coverageReportFile);
if (coverageReportXML === undefined) {
logger?.warning(
`could not find file ${coverageReportFile} in the code coverage artifact`,
);
continue;
}

// eslint-disable-next-line no-await-in-loop -- Since we only need 1 report file, it is easier to run it serially rather than extracting all jsZipObjects and then awaiting promises in parallel
const coverageReportXML = await jsZipObject?.async("nodebuffer");
if (coverageReportXML !== undefined) {
xmlParser.parseString(
coverageReportXML,
(err: Error | null, result: unknown): void => {
if (err) {
console.warn(`Error processing file ${coverageReportFile}: ${err}`);
return;
}
coverageMetricsForBaseline = extractCoverageMetrics(
result as XmlCoverageReportSchema,
);
},
);
}
xmlParser.parseString(coverageReportXML, (err: Error | null, result: unknown): void => {
if (err) {
console.warn(`Error processing file ${coverageReportFile}: ${err}`);
return;
}
coverageMetricsForBaseline = extractCoverageMetrics(result as XmlCoverageReportSchema);
});

if (coverageMetricsForBaseline.size > 0) {
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@
*/

import { strict as assert } from "node:assert";
import { getZipObjectFromArtifact } from "@fluidframework/bundle-size-tools";
import {
type UnzippedContents,
getZipObjectFromArtifact,
} from "@fluidframework/bundle-size-tools";
import type { WebApi } from "azure-devops-node-api";
import { BuildResult } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
import type { Build } from "azure-devops-node-api/interfaces/BuildInterfaces.js";
import type JSZip from "jszip";
import type { CommandLogger } from "../../logging.js";
import type { IAzureDevopsBuildCoverageConstants } from "./constants.js";
import { getBuild, getBuilds } from "./utils.js";

export interface IBuildMetrics {
build: Build & { id: number };
/**
* The artifact that was published by the PR build in zip format
* The artifact that was published by the PR build as unzipped contents
*/
artifactZip: JSZip;
artifactZip: UnzippedContents;
}

/**
Expand All @@ -43,7 +45,7 @@ export async function getBaselineBuildMetrics(
});

let baselineBuild: Build | undefined;
let baselineArtifactZip: JSZip | undefined;
let baselineArtifactZip: UnzippedContents | undefined;
for (const build of recentBuilds) {
if (build.result !== BuildResult.Succeeded) {
continue;
Expand Down Expand Up @@ -159,7 +161,7 @@ export async function getBuildArtifactForSpecificBuild(
`codeCoverageAnalysisArtifactName: ${azureDevopsBuildCoverageConstants.artifactName}`,
);

const artifactZip: JSZip | undefined = await getZipObjectFromArtifact(
const artifactZip: UnzippedContents | undefined = await getZipObjectFromArtifact(
adoConnection,
azureDevopsBuildCoverageConstants.projectName,
build.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import type { CommentThreadStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { Compiler } from 'webpack';
import type JSZip from 'jszip';
import { jszip } from 'jszip';
import type { StatsCompilation } from 'webpack';
import { WebApi } from 'azure-devops-node-api';
import type Webpack from 'webpack';
Expand Down Expand Up @@ -183,7 +181,7 @@ export function getBuilds(adoConnection: WebApi, options: GetBuildOptions): Prom
export function getBuildTagForCommit(commitHash: string): string;

// @public
export function getBundleBuddyConfigFileFromZip(jsZip: JSZip, relativePath: string): Promise<BundleBuddyConfig>;
export function getBundleBuddyConfigFileFromZip(files: UnzippedContents, relativePath: string): Promise<BundleBuddyConfig>;

// @public
export function getBundleBuddyConfigFromFileSystem(path: string): Promise<BundleBuddyConfig>;
Expand All @@ -209,7 +207,7 @@ export function getBundleFilePathsFromFolder(relativePathsInFolder: string[]): B
export function getBundlePathsFromFileSystem(bundleReportPath: string): Promise<BundleFileData[]>;

// @public
export function getBundlePathsFromZipObject(jsZip: JSZip): BundleFileData[];
export function getBundlePathsFromZipObject(files: UnzippedContents): BundleFileData[];

// @public (undocumented)
export function getBundleSummaries(args: GetBundleSummariesArgs): Promise<BundleSummaries>;
Expand Down Expand Up @@ -251,13 +249,13 @@ export function getSimpleComment(message: string, baselineCommit: string): strin
export function getStatsFileFromFileSystem(path: string): Promise<StatsCompilation>;

// @public
export function getStatsFileFromZip(jsZip: JSZip, relativePath: string): Promise<StatsCompilation>;
export function getStatsFileFromZip(files: UnzippedContents, relativePath: string): Promise<StatsCompilation>;

// @public
export function getTotalSizeStatsProcessor(options: TotalSizeStatsProcessorOptions): WebpackStatsProcessor;

// @public
export function getZipObjectFromArtifact(adoConnection: WebApi, projectName: string, buildNumber: number, bundleAnalysisArtifactName: string): Promise<JSZip>;
export function getZipObjectFromArtifact(adoConnection: WebApi, projectName: string, buildNumber: number, bundleAnalysisArtifactName: string): Promise<UnzippedContents>;

// @public (undocumented)
export interface IADOConstants {
Expand Down Expand Up @@ -295,8 +293,11 @@ export interface TotalSizeStatsProcessorOptions {
metricName: string;
}

// @public (undocumented)
export function unzipStream(stream: NodeJS.ReadableStream): Promise<jszip>;
// @public
export type UnzippedContents = Map<string, Buffer>;

// @public
export function unzipStream(stream: NodeJS.ReadableStream, baseFolder?: string): Promise<UnzippedContents>;

// @public
export type WebpackStatsProcessor = (stats: StatsCompilation, config: BundleBuddyConfig | undefined) => BundleMetricSet | undefined;
Expand Down
12 changes: 9 additions & 3 deletions build-tools/packages/bundle-size-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,25 @@
"build:compile": "fluid-build --task compile",
"build:copy": "copyfiles -u 1 \"src/**/*.fsl\" dist",
"build:docs": "api-extractor run --local",
"build:test": "tsc --project ./src/test/tsconfig.json",
"check:biome": "biome check .",
"check:format": "npm run check:biome",
"ci:build:docs": "api-extractor run",
"clean": "rimraf --glob dist \"*.tsbuildinfo\" _api-extractor-temp \"*.build.log\"",
"clean": "rimraf --glob dist lib \"*.tsbuildinfo\" _api-extractor-temp \"*.build.log\" nyc",
"compile": "fluid-build . --task compile",
"eslint": "eslint --format stylish src",
"eslint:fix": "eslint --format stylish src --fix",
"format": "npm run format:biome",
"format:biome": "biome check --write .",
"lint": "npm run eslint",
"lint:fix": "npm run eslint:fix",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "npm run test:mocha",
"test:mocha": "mocha --forbid-only \"lib/test/**/*.test.js\"",
"tsc": "tsc"
},
"dependencies": {
"azure-devops-node-api": "^11.2.0",
"fflate": "^0.8.2",
"jszip": "^3.10.1",
"msgpack-lite": "^0.1.26",
"typescript": "~5.4.5",
"webpack": "^5.103.0"
Expand All @@ -53,10 +54,15 @@
"@fluidframework/build-tools-bin": "npm:@fluidframework/build-tools@~0.49.0",
"@fluidframework/eslint-config-fluid": "^8.1.0",
"@microsoft/api-extractor": "^7.55.1",
"@types/chai": "^5.2.3",
"@types/mocha": "^10.0.10",
"@types/msgpack-lite": "^0.1.12",
"@types/node": "^22.19.1",
"c8": "^10.1.3",
"chai": "^6.2.1",
"copyfiles": "^2.4.1",
"eslint": "~8.57.0",
"mocha": "^11.7.5",
"rimraf": "^6.1.2"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,36 @@

import { strict as assert } from "assert";
import type { WebApi } from "azure-devops-node-api";
import type JSZip from "jszip";
import type { StatsCompilation } from "webpack";

import type { BundleBuddyConfig } from "../BundleBuddyTypes";
import { decompressStatsFile, unzipStream } from "../utilities";
import { type UnzippedContents, decompressStatsFile, unzipStream } from "../utilities";
import {
type BundleFileData,
getBundleFilePathsFromFolder,
} from "./getBundleFilePathsFromFolder";

/**
* Gets a list of all paths relevant to bundle buddy from the zip archive
* @param jsZip - A zip file that has been processed with the jszip library
* Gets a list of all paths relevant to bundle buddy from the unzipped archive
* @param files - The unzipped archive contents as a Map of paths to Buffers
*/
export function getBundlePathsFromZipObject(jsZip: JSZip): BundleFileData[] {
const relativePaths: string[] = [];
jsZip.forEach((path) => {
relativePaths.push(path);
});

export function getBundlePathsFromZipObject(files: UnzippedContents): BundleFileData[] {
const relativePaths: string[] = [...files.keys()];
return getBundleFilePathsFromFolder(relativePaths);
}

/**
* 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.
* @param adoConnection - A connection to the ADO api.
* @param buildNumber - The ADO build number that contains the artifact we wish to fetch
* @returns A Map of file paths to their contents as Buffers
*/
export async function getZipObjectFromArtifact(
adoConnection: WebApi,
projectName: string,
buildNumber: number,
bundleAnalysisArtifactName: string,
): Promise<JSZip> {
): Promise<UnzippedContents> {
const buildApi = await adoConnection.getBuildApi();

// IMPORTANT
Expand All @@ -57,44 +53,42 @@ export async function getZipObjectFromArtifact(
// Undo hack from above
buildApi.createAcceptHeader = originalCreateAcceptHeader;

// We want our relative paths to be clean, so navigating JsZip into the top level folder
const result = (await unzipStream(artifactStream)).folder(bundleAnalysisArtifactName);
// We want our relative paths to be clean, so filter to files within the artifact folder
const result = await unzipStream(artifactStream, bundleAnalysisArtifactName);
assert(
result,
result.size > 0,
`getZipObjectFromArtifact could not find the folder ${bundleAnalysisArtifactName}`,
);

return result;
}

/**
* Retrieves a decompressed stats file from a jszip object
* @param jsZip - A zip file that has been processed with the jszip library
* Retrieves a decompressed stats file from an unzipped archive
* @param files - The unzipped archive contents as a Map of paths to Buffers
* @param relativePath - The relative path to the file that will be retrieved
*/
export async function getStatsFileFromZip(
jsZip: JSZip,
files: UnzippedContents,
relativePath: string,
): Promise<StatsCompilation> {
const jsZipObject = jsZip.file(relativePath);
assert(jsZipObject, `getStatsFileFromZip could not find file ${relativePath}`);
const buffer = files.get(relativePath);
assert(buffer, `getStatsFileFromZip could not find file ${relativePath}`);

const buffer = await jsZipObject.async("nodebuffer");
return decompressStatsFile(buffer);
}

/**
* Retrieves and parses a bundle buddy config file from a jszip object
* @param jsZip - A zip file that has been processed with the jszip library
* Retrieves and parses a bundle buddy config file from an unzipped archive
* @param files - The unzipped archive contents as a Map of paths to Buffers
* @param relativePath - The relative path to the file that will be retrieved
*/
export async function getBundleBuddyConfigFileFromZip(
jsZip: JSZip,
files: UnzippedContents,
relativePath: string,
): Promise<BundleBuddyConfig> {
const jsZipObject = jsZip.file(relativePath);
assert(jsZipObject, `getBundleBuddyConfigFileFromZip could not find file ${relativePath}`);
const buffer = files.get(relativePath);
assert(buffer, `getBundleBuddyConfigFileFromZip could not find file ${relativePath}`);

const buffer = await jsZipObject.async("nodebuffer");
return JSON.parse(buffer.toString());
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import { join } from "path";
import type { WebApi } from "azure-devops-node-api";
import { BuildResult, BuildStatus } from "azure-devops-node-api/interfaces/BuildInterfaces";
import type JSZip from "jszip";

import type { BundleComparison, BundleComparisonResult } from "../BundleBuddyTypes";
import { compareBundles } from "../compareBundles";
import type { UnzippedContents } from "../utilities";
import { getBaselineCommit, getBuilds, getPriorCommit } from "../utilities";
import {
getBundlePathsFromZipObject,
Expand Down Expand Up @@ -216,7 +216,9 @@ export class ADOSizeComparator {
}
}

private async createComparisonFromZip(baselineZip: JSZip): Promise<BundleComparison[]> {
private async createComparisonFromZip(
baselineZip: UnzippedContents,
): Promise<BundleComparison[]> {
const baselineZipBundlePaths = getBundlePathsFromZipObject(baselineZip);

const prBundleFileSystemPaths = await getBundlePathsFromFileSystem(this.localReportPath);
Expand Down
1 change: 1 addition & 0 deletions build-tools/packages/bundle-size-tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@ export {
getChunkParsedSize,
getLastCommitHashFromPR,
getPriorCommit,
UnzippedContents,
unzipStream,
} from "./utilities";
22 changes: 22 additions & 0 deletions build-tools/packages/bundle-size-tools/src/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"extends": "@fluidframework/build-common/ts-common-config.json",
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"declarationMap": false,
"rootDir": "./",
"outDir": "../../lib/test",
"types": ["node", "mocha"],
"typeRoots": [
"../../../../node_modules/@types",
"../../../../node_modules/.pnpm/node_modules/@types",
],
"skipLibCheck": true,
},
"include": ["./**/*"],
"references": [
{
"path": "../..",
},
],
}
Loading
Loading