@mui/internal-benchmark 0.0.3-canary.9 → 0.1.1-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
+ import * as React from 'react';
1
2
  export declare function ElementTiming({
2
3
  name
3
4
  }: {
4
5
  name: string;
5
- }): import("react/jsx-runtime").JSX.Element;
6
+ }): React.JSX.Element;
package/Metric.mjs CHANGED
@@ -33,6 +33,8 @@ function flush(test, accumulator) {
33
33
  * tests and loop iterations, inside or outside React.
34
34
  */
35
35
  export class Metric {
36
+ name;
37
+ config;
36
38
  constructor(config) {
37
39
  this.config = typeof config === 'string' ? {
38
40
  name: config
package/ciApi.d.mts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Posts `body` to the dashboard, authenticated as this CI job, and returns the response text. `what`
3
+ * names the request in the errors it raises.
4
+ */
5
+ export declare function postToDashboard(pathname: string, body: unknown, what: string): Promise<string>;
package/ciApi.mjs ADDED
@@ -0,0 +1,34 @@
1
+ /* eslint-disable no-console -- progress belongs in the CI log. */
2
+
3
+ // The dashboard's CI-report API: a bearer-token POST of one JSON body, with the OIDC token
4
+ // CircleCI mints for the job.
5
+
6
+ const DEFAULT_API_URL = 'https://frontend-public.mui.com';
7
+
8
+ /**
9
+ * Posts `body` to the dashboard, authenticated as this CI job, and returns the response text. `what`
10
+ * names the request in the errors it raises.
11
+ */
12
+ export async function postToDashboard(pathname, body, what) {
13
+ const oidcToken = process.env.CIRCLE_OIDC_TOKEN_V2;
14
+ if (!oidcToken) {
15
+ throw new Error(`CIRCLE_OIDC_TOKEN_V2 environment variable is required for ${what}`);
16
+ }
17
+
18
+ // The override exists so a pull request can point at its own dashboard preview deployment.
19
+ const url = new URL(pathname, process.env.CI_REPORT_API_URL || DEFAULT_API_URL);
20
+ console.log(`${what} → ${url.href}`);
21
+ const response = await fetch(url, {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ Authorization: `Bearer ${oidcToken}`
26
+ },
27
+ body: JSON.stringify(body)
28
+ });
29
+ const responseText = await response.text();
30
+ if (!response.ok) {
31
+ throw new Error(`${what} failed (${response.status}): ${responseText}`);
32
+ }
33
+ return responseText;
34
+ }
package/ciReport.d.mts CHANGED
@@ -1,4 +1,18 @@
1
1
  import { z } from 'zod/v4';
2
+ /**
3
+ * Creates a CI report upload schema for a specific report type.
4
+ * Common fields (commitSha, repo, branch, prNumber) are shared across all report types.
5
+ */
6
+ export declare function ciReportUploadSchema<ReportSchema extends z.ZodType>(type: string, version: number, reportSchema: ReportSchema): z.ZodObject<{
7
+ version: z.ZodLiteral<number>;
8
+ timestamp: z.ZodNumber;
9
+ commitSha: z.ZodString;
10
+ repo: z.ZodString;
11
+ reportType: z.ZodLiteral<string>;
12
+ prNumber: z.ZodOptional<z.ZodNumber>;
13
+ branch: z.ZodString;
14
+ report: ReportSchema;
15
+ }, z.core.$strip>;
2
16
  declare const renderStatsSchema: z.ZodObject<{
3
17
  id: z.ZodString;
4
18
  phase: z.ZodEnum<{
@@ -81,7 +95,27 @@ declare const benchmarkBaseUploadSchema: z.ZodObject<{
81
95
  reportType: z.ZodLiteral<string>;
82
96
  prNumber: z.ZodOptional<z.ZodNumber>;
83
97
  branch: z.ZodString;
84
- report: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
98
+ report: z.ZodRecord<z.ZodString, z.ZodObject<{
99
+ iterations: z.ZodNumber;
100
+ totalDuration: z.ZodNumber;
101
+ renders: z.ZodArray<z.ZodObject<{
102
+ id: z.ZodString;
103
+ phase: z.ZodEnum<{
104
+ mount: "mount";
105
+ "nested-update": "nested-update";
106
+ update: "update";
107
+ }>;
108
+ startTime: z.ZodNumber;
109
+ actualDuration: z.ZodNumber;
110
+ stdDev: z.ZodNumber;
111
+ outliers: z.ZodNumber;
112
+ }, z.core.$strip>>;
113
+ metrics: z.ZodRecord<z.ZodString, z.ZodObject<{
114
+ mean: z.ZodNumber;
115
+ stdDev: z.ZodNumber;
116
+ outliers: z.ZodNumber;
117
+ }, z.core.$strip>>;
118
+ }, z.core.$strip>>;
85
119
  metricDefinitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
86
120
  kind: z.ZodEnum<{
87
121
  discrete: "discrete";
@@ -106,7 +140,27 @@ export declare const benchmarkUploadSchema: z.ZodObject<{
106
140
  reportType: z.ZodLiteral<string>;
107
141
  prNumber: z.ZodOptional<z.ZodNumber>;
108
142
  branch: z.ZodString;
109
- report: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
143
+ report: z.ZodRecord<z.ZodString, z.ZodObject<{
144
+ iterations: z.ZodNumber;
145
+ totalDuration: z.ZodNumber;
146
+ renders: z.ZodArray<z.ZodObject<{
147
+ id: z.ZodString;
148
+ phase: z.ZodEnum<{
149
+ mount: "mount";
150
+ "nested-update": "nested-update";
151
+ update: "update";
152
+ }>;
153
+ startTime: z.ZodNumber;
154
+ actualDuration: z.ZodNumber;
155
+ stdDev: z.ZodNumber;
156
+ outliers: z.ZodNumber;
157
+ }, z.core.$strip>>;
158
+ metrics: z.ZodRecord<z.ZodString, z.ZodObject<{
159
+ mean: z.ZodNumber;
160
+ stdDev: z.ZodNumber;
161
+ outliers: z.ZodNumber;
162
+ }, z.core.$strip>>;
163
+ }, z.core.$strip>>;
110
164
  metricDefinitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
111
165
  kind: z.ZodEnum<{
112
166
  discrete: "discrete";
@@ -130,7 +184,27 @@ export declare const benchmarkUploadSchema: z.ZodObject<{
130
184
  reportType: z.ZodLiteral<string>;
131
185
  prNumber: z.ZodOptional<z.ZodNumber>;
132
186
  branch: z.ZodString;
133
- report: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
187
+ report: z.ZodRecord<z.ZodString, z.ZodObject<{
188
+ iterations: z.ZodNumber;
189
+ totalDuration: z.ZodNumber;
190
+ renders: z.ZodArray<z.ZodObject<{
191
+ id: z.ZodString;
192
+ phase: z.ZodEnum<{
193
+ mount: "mount";
194
+ "nested-update": "nested-update";
195
+ update: "update";
196
+ }>;
197
+ startTime: z.ZodNumber;
198
+ actualDuration: z.ZodNumber;
199
+ stdDev: z.ZodNumber;
200
+ outliers: z.ZodNumber;
201
+ }, z.core.$strip>>;
202
+ metrics: z.ZodRecord<z.ZodString, z.ZodObject<{
203
+ mean: z.ZodNumber;
204
+ stdDev: z.ZodNumber;
205
+ outliers: z.ZodNumber;
206
+ }, z.core.$strip>>;
207
+ }, z.core.$strip>>;
134
208
  metricDefinitions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
135
209
  kind: z.ZodEnum<{
136
210
  discrete: "discrete";
package/ciReport.mjs CHANGED
@@ -13,7 +13,7 @@ async function getGitInfo() {
13
13
  * Creates a CI report upload schema for a specific report type.
14
14
  * Common fields (commitSha, repo, branch, prNumber) are shared across all report types.
15
15
  */
16
- function ciReportUploadSchema(type, version, reportSchema) {
16
+ export function ciReportUploadSchema(type, version, reportSchema) {
17
17
  return z.object({
18
18
  version: z.literal(version),
19
19
  timestamp: z.number(),
package/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mui/internal-benchmark v0.0.2
2
+ * @mui/internal-benchmark v0.1.0
3
3
  *
4
4
  * @license undefined
5
5
  * This source code is licensed under the undefined license found in the
@@ -206,17 +206,15 @@ export function benchmark(name, renderFn, interactionOrOptions, maybeOptions) {
206
206
 
207
207
  // Paint timings are recorded as one harness-owned `bench:paint` metric: the default sentinel
208
208
  // is the base series (`bench:paint`) and named `elementtiming` markers are sub-series
209
- // (`bench:paint#grid-header`, …), all sharing a single definition. A default alarm keeps a
210
- // >20% paint regression flagged, matching the previous behavior.
209
+ // (`bench:paint#grid-header`, …), all sharing a single definition. Paint is informational (no
210
+ // alarm): it dominates each test's total duration, so a per-test paint alarm just duplicates the
211
+ // Duration regression signal and floods the report on any broadly-regressed run.
211
212
  const paint = new ScalarMetric({
212
213
  name: 'bench:paint',
213
214
  format: {
214
215
  style: 'unit',
215
216
  unit: 'millisecond',
216
217
  maximumFractionDigits: 2
217
- },
218
- alarm: {
219
- error: 0.2
220
218
  }
221
219
  });
222
220
  if (typeof window.gc !== 'function') {
@@ -286,13 +284,13 @@ export function benchmark(name, renderFn, interactionOrOptions, maybeOptions) {
286
284
  if (!isWarmup) {
287
285
  for (const entry of timing.elementEntries) {
288
286
  // Skip paints that happened while recording was paused. Attribute by the paint's
289
- // `renderTime`, not by when the observer callback fired (which can lag the paint).
290
- if (!recording.activeAt(entry.renderTime)) {
287
+ // `paintTime`, not by when the observer callback fired (which can lag the paint).
288
+ if (!recording.activeAt(entry.paintTime)) {
291
289
  continue;
292
290
  }
293
291
  // The default sentinel is the base series; named markers become sub-series.
294
292
  const id = entry.identifier === 'default' ? undefined : entry.identifier;
295
- paint.record(entry.renderTime - iterationStart, id !== undefined ? {
293
+ paint.record(entry.paintTime - iterationStart, id !== undefined ? {
296
294
  id
297
295
  } : undefined);
298
296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-benchmark",
3
- "version": "0.0.3-canary.9",
3
+ "version": "0.1.1-canary.1",
4
4
  "author": "MUI Team",
5
5
  "description": "Benchmark utilities for MUI projects. Internal package.",
6
6
  "repository": {
@@ -9,11 +9,13 @@
9
9
  "directory": "packages/benchmark"
10
10
  },
11
11
  "dependencies": {
12
- "@babel/runtime": "^7.29.2",
13
- "@vitejs/plugin-react": "^6.0.2",
12
+ "@babel/runtime": "^8.0.5",
13
+ "@vitejs/plugin-react": "^6.1.1",
14
+ "chalk": "^5.6.2",
14
15
  "env-ci": "^11.2.0",
15
- "execa": "^9.6.1",
16
- "zod": "^4.4.3"
16
+ "es-toolkit": "^1.52.0",
17
+ "execa": "^10.0.1",
18
+ "zod": "^4.6.5"
17
19
  },
18
20
  "peerDependencies": {
19
21
  "@vitest/browser-playwright": ">=4.1",
@@ -68,5 +70,5 @@
68
70
  }
69
71
  }
70
72
  },
71
- "gitSha": "cb672accc6b61d455e18ce874f99ea841831a734"
73
+ "gitSha": "c2a0ca83b8dd00ae2fa2b5714dbc6684d50926d9"
72
74
  }
@@ -5,7 +5,7 @@ export interface ReactRecordingControls {
5
5
  readonly hadEmptyActiveWindow: boolean;
6
6
  /**
7
7
  * Whether recording was active at `time` (a `performance.now()` timestamp). Paint entries are
8
- * observed asynchronously, so they are attributed by their `renderTime` rather than by the
8
+ * observed asynchronously, so they are attributed by their `paintTime` rather than by the
9
9
  * recording state at the moment the observer callback happens to fire.
10
10
  */
11
11
  activeAt(time: number): boolean;
package/reporter.mjs CHANGED
@@ -240,6 +240,9 @@ async function loadBaselineReport(baselinePath) {
240
240
  class BenchmarkReporter {
241
241
  benchmarks = {};
242
242
  metricDefinitions = {};
243
+ outputPath;
244
+ upload;
245
+ baselinePath;
243
246
  hasFailures = false;
244
247
  constructor(options) {
245
248
  this.outputPath = options?.outputPath ?? process.env.BENCHMARK_OUTPUT_PATH ?? path.resolve(process.cwd(), 'benchmarks', 'results.json');
@@ -2,8 +2,6 @@ interface SyncPrCommentResult {
2
2
  success: boolean;
3
3
  skipped?: boolean;
4
4
  }
5
- /**
6
- * Syncs a PR comment via the dashboard API.
7
- */
5
+ /** Asks the dashboard to regenerate the pull request comment for `repo`. */
8
6
  export declare function syncPrComment(repo: string): Promise<SyncPrCommentResult>;
9
7
  export {};
package/syncPrComment.mjs CHANGED
@@ -1,27 +1,8 @@
1
- const DEFAULT_API_URL = 'https://frontend-public.mui.com';
2
- /**
3
- * Syncs a PR comment via the dashboard API.
4
- */
1
+ import { postToDashboard } from "./ciApi.mjs";
2
+ /** Asks the dashboard to regenerate the pull request comment for `repo`. */
5
3
  export async function syncPrComment(repo) {
6
- const oidcToken = process.env.CIRCLE_OIDC_TOKEN_V2;
7
- if (!oidcToken) {
8
- throw new Error('CIRCLE_OIDC_TOKEN_V2 environment variable is required for PR comment sync');
9
- }
10
- const apiUrl = process.env.CI_REPORT_API_URL || DEFAULT_API_URL;
11
- const url = new URL('/api/ci-reports/sync-pr-comment', apiUrl);
12
- const response = await fetch(url, {
13
- method: 'POST',
14
- headers: {
15
- 'Content-Type': 'application/json',
16
- Authorization: `Bearer ${oidcToken}`
17
- },
18
- body: JSON.stringify({
19
- repo
20
- })
21
- });
22
- if (!response.ok) {
23
- const responseText = await response.text();
24
- throw new Error(`Sync PR comment API returned ${response.status} ${response.statusText}: ${responseText}`);
25
- }
26
- return response.json();
4
+ const responseText = await postToDashboard('/api/ci-reports/sync-pr-comment', {
5
+ repo
6
+ }, 'The pull request comment sync');
7
+ return JSON.parse(responseText);
27
8
  }
package/upload.d.mts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { BenchmarkUpload } from "./ciReport.mjs";
2
- declare function uploadCiReport(report: BenchmarkUpload, options?: {
3
- apiUrl?: string;
4
- }): Promise<void>;
2
+ /**
3
+ * Uploads a benchmark report to the CI report store.
4
+ *
5
+ * Validated before sending: the server stores `report` without inspecting it, so a malformed one
6
+ * would be accepted here and only fail later, when the dashboard tried to render it.
7
+ */
8
+ declare function uploadCiReport(report: BenchmarkUpload): Promise<void>;
5
9
  export { uploadCiReport };
package/upload.mjs CHANGED
@@ -1,34 +1,17 @@
1
+ /* eslint-disable no-console -- progress belongs in the CI log. */
2
+
3
+ import { postToDashboard } from "./ciApi.mjs";
1
4
  import { benchmarkUploadSchema } from "./ciReport.mjs";
2
- async function uploadCiReport(report, options) {
5
+ /**
6
+ * Uploads a benchmark report to the CI report store.
7
+ *
8
+ * Validated before sending: the server stores `report` without inspecting it, so a malformed one
9
+ * would be accepted here and only fail later, when the dashboard tried to render it.
10
+ */
11
+ async function uploadCiReport(report) {
3
12
  benchmarkUploadSchema.parse(report);
4
- const apiUrl = options?.apiUrl ?? process.env.CI_REPORT_API_URL ?? 'https://frontend-public.mui.com';
5
- const url = new URL('/api/ci-reports/upload', apiUrl);
6
- const oidcToken = process.env.CIRCLE_OIDC_TOKEN_V2;
7
- if (!oidcToken) {
8
- throw new Error('CIRCLE_OIDC_TOKEN_V2 environment variable is required for uploads');
9
- }
10
-
11
- // eslint-disable-next-line no-console
12
- console.log('Upload request body:', JSON.stringify({
13
- ...report,
14
- report: '...'
15
- }, null, 2));
16
- // eslint-disable-next-line no-console
17
- console.log(`Uploading benchmark to ${url.href}`);
18
- const response = await fetch(url, {
19
- method: 'POST',
20
- headers: {
21
- 'Content-Type': 'application/json',
22
- Authorization: `Bearer ${oidcToken}`
23
- },
24
- body: JSON.stringify(report)
25
- });
26
- const responseText = await response.text();
27
- if (!response.ok) {
28
- throw new Error(`Upload failed (${response.status}): ${responseText}`);
29
- }
13
+ const responseText = await postToDashboard('/api/ci-reports/upload', report, 'The benchmark upload');
30
14
  const result = JSON.parse(responseText);
31
- // eslint-disable-next-line no-console
32
15
  console.log(`Benchmark results uploaded successfully. S3 key: ${result.key}`);
33
16
  }
34
17
  export { uploadCiReport };
@@ -0,0 +1,2 @@
1
+ /** Resolves a committish to the commit it names, peeling an annotated tag to its commit. */
2
+ export declare function resolveCommit(repoRoot: string, ref: string): Promise<string>;
package/utils/git.mjs ADDED
@@ -0,0 +1,13 @@
1
+ import { execa } from 'execa';
2
+
3
+ /** Resolves a committish to the commit it names, peeling an annotated tag to its commit. */
4
+ export async function resolveCommit(repoRoot, ref) {
5
+ const result = await execa('git', ['rev-parse', '--verify', `${ref}^{commit}`], {
6
+ cwd: repoRoot,
7
+ reject: false
8
+ });
9
+ if (result.exitCode !== 0) {
10
+ throw new Error(`Could not resolve git ref "${ref}": ${String(result.stderr).trim()}`);
11
+ }
12
+ return result.stdout.trim();
13
+ }
@@ -0,0 +1,46 @@
1
+ export interface PackedPackage {
2
+ /** Package name from its package.json. */
3
+ name: string;
4
+ version: string;
5
+ /** Absolute path to the packed `.tgz`. */
6
+ tarball: string;
7
+ }
8
+ export interface PackRefOptions {
9
+ /** The repository the ref lives in; also where the worktree is added from. */
10
+ repoRoot: string;
11
+ /** A git ref (SHA, branch, or tag) to pack. */
12
+ ref: string;
13
+ /** Cache root. The packed folder lands at `<outRoot>/<sha>` and is reused on the next call. */
14
+ outRoot: string;
15
+ /**
16
+ * Command run in the checkout to install before building. Defaults to a frozen-lockfile
17
+ * `pnpm install`; pass `''` to skip installing.
18
+ */
19
+ installCmd?: string;
20
+ /**
21
+ * Command run in the checkout to build before packing. Defaults to `pnpm release:build`. Part of
22
+ * the cache key.
23
+ */
24
+ buildCmd?: string;
25
+ }
26
+ /**
27
+ * Packs every public workspace package at `ref` into `<outRoot>/<sha>`, returning the packed
28
+ * tarballs.
29
+ *
30
+ * The folder is cached by SHA: a later call for the same commit, built with the same `buildCmd`,
31
+ * reuses it and skips the checkout, install and build. It is assembled in a staging folder and
32
+ * renamed into place, so an interrupted run never leaves something that looks like a cache.
33
+ */
34
+ export declare function packRef(options: PackRefOptions): Promise<PackedPackage[]>;
35
+ /**
36
+ * Builds and packs the working tree into `outRoot`, naming each tarball by a hash of its bytes: an
37
+ * unchanged build keeps its path, so an install that depends on it has nothing to redo.
38
+ */
39
+ export declare function packWorkingTree(options: {
40
+ /** The workspace to build and pack. */
41
+ repoRoot: string;
42
+ /** Directory to hold the hashed tarballs. Replaced on each call. */
43
+ outRoot: string;
44
+ /** Command that builds the publishable packages. Defaults to `pnpm release:build`. */
45
+ buildCmd?: string;
46
+ }): Promise<PackedPackage[]>;
@@ -0,0 +1,217 @@
1
+ /* eslint-disable no-console */
2
+
3
+ // Packs a workspace's public packages with `pnpm pack`, so each tarball is exactly what
4
+ // `pnpm publish` would upload — for installing a build the way a consumer installs a release.
5
+
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ import { createHash } from 'node:crypto';
9
+ import { createReadStream } from 'node:fs';
10
+ import { access, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
11
+ import { pipeline } from 'node:stream/promises';
12
+ import chalk from 'chalk';
13
+ import { mapAsync } from 'es-toolkit/array';
14
+ import { execa, parseCommandString } from 'execa';
15
+ import { resolveCommit } from "./git.mjs";
16
+ import { listPublishablePackages } from "./pnpm.mjs";
17
+ const MANIFEST = 'manifest.json';
18
+
19
+ /** `@scope/pkg` → `scope+pkg.tgz`; `+` cannot appear in a package name, so no two collide. */
20
+ function tarballName(pkgName) {
21
+ return `${pkgName.replace(/^@/, '').replace('/', '+')}.tgz`;
22
+ }
23
+
24
+ /** Removes a temporary checkout, and git's record of it. */
25
+ async function removeCheckout(repoRoot, checkout) {
26
+ await rm(checkout, {
27
+ recursive: true,
28
+ force: true,
29
+ maxRetries: 5,
30
+ retryDelay: 200
31
+ });
32
+ await execa('git', ['worktree', 'prune'], {
33
+ cwd: repoRoot
34
+ });
35
+ }
36
+
37
+ /** Runs `buildCmd` in `cwd`, with the nx daemon off so nothing keeps writing after it returns. */
38
+ async function build(cwd, buildCmd) {
39
+ const [file, ...args] = parseCommandString(buildCmd);
40
+ await execa(file, args, {
41
+ cwd,
42
+ env: {
43
+ NX_DAEMON: 'false'
44
+ },
45
+ stdio: 'inherit',
46
+ verbose: 'short'
47
+ });
48
+ }
49
+
50
+ /** Content hash of a file, short enough to live in a filename. */
51
+ async function hashFile(file) {
52
+ const hash = createHash('sha256');
53
+ await pipeline(createReadStream(file), hash);
54
+ return hash.digest('hex').slice(0, 12);
55
+ }
56
+
57
+ /**
58
+ * `dir`'s packages if it holds a cache of the requested build — built with the same `buildCmd`,
59
+ * every tarball still present — or `null` otherwise.
60
+ */
61
+ async function readFreshCache(dir, buildCmd) {
62
+ let manifest;
63
+ try {
64
+ manifest = JSON.parse(await readFile(path.join(dir, MANIFEST), 'utf8'));
65
+ } catch (error) {
66
+ if (error.code === 'ENOENT') {
67
+ return null;
68
+ }
69
+ throw error;
70
+ }
71
+ if (manifest.buildCmd !== buildCmd) {
72
+ return null;
73
+ }
74
+ const packages = manifest.packages.map(pkg => ({
75
+ ...pkg,
76
+ tarball: path.join(dir, pkg.tarball)
77
+ }));
78
+ const present = await Promise.allSettled(packages.map(pkg => access(pkg.tarball)));
79
+ return present.every(result => result.status === 'fulfilled') ? packages : null;
80
+ }
81
+
82
+ /**
83
+ * Packs the already-built public workspace packages of `workspaceDir` into `outDir`.
84
+ *
85
+ * A public package that depends on a private workspace package cannot be installed from these —
86
+ * the private one is never packed — which is a packaging bug in the ref, not something to hide here.
87
+ */
88
+ async function packBuiltPackages(workspaceDir, outDir) {
89
+ await mkdir(outDir, {
90
+ recursive: true
91
+ });
92
+ const packages = await listPublishablePackages(workspaceDir);
93
+ if (packages.length === 0) {
94
+ throw new Error(`No public workspace packages found in ${workspaceDir}.`);
95
+ }
96
+ // Each `pnpm pack` is a node process of its own.
97
+ return mapAsync(packages, async ({
98
+ path: pkgDir,
99
+ name,
100
+ version
101
+ }) => {
102
+ const tarball = path.join(outDir, tarballName(name));
103
+ await execa('pnpm', ['pack', '--out', tarball], {
104
+ cwd: pkgDir,
105
+ verbose: 'short'
106
+ });
107
+ return {
108
+ name,
109
+ version,
110
+ tarball
111
+ };
112
+ }, {
113
+ concurrency: os.availableParallelism()
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Packs every public workspace package at `ref` into `<outRoot>/<sha>`, returning the packed
119
+ * tarballs.
120
+ *
121
+ * The folder is cached by SHA: a later call for the same commit, built with the same `buildCmd`,
122
+ * reuses it and skips the checkout, install and build. It is assembled in a staging folder and
123
+ * renamed into place, so an interrupted run never leaves something that looks like a cache.
124
+ */
125
+ export async function packRef(options) {
126
+ const {
127
+ repoRoot,
128
+ ref,
129
+ outRoot,
130
+ installCmd = 'pnpm install --frozen-lockfile --prefer-offline --config.engine-strict=false',
131
+ buildCmd = 'pnpm release:build'
132
+ } = options;
133
+ const sha = await resolveCommit(repoRoot, ref);
134
+ const dir = path.join(outRoot, sha);
135
+ const cached = await readFreshCache(dir, buildCmd);
136
+ if (cached) {
137
+ console.log(chalk.green(`\nReusing packed workspace for "${ref}" (${sha.slice(0, 9)}).`));
138
+ return cached;
139
+ }
140
+ await mkdir(outRoot, {
141
+ recursive: true
142
+ });
143
+ const staging = await mkdtemp(path.join(outRoot, '.staging-'));
144
+ const checkout = await mkdtemp(path.join(os.tmpdir(), 'pack-workspace-'));
145
+ try {
146
+ console.log(chalk.cyan(`\nChecking out "${ref}" (${sha.slice(0, 9)}) at ${checkout}`));
147
+ await execa('git', ['worktree', 'add', '--detach', checkout, sha], {
148
+ cwd: repoRoot,
149
+ verbose: 'short'
150
+ });
151
+ if (installCmd) {
152
+ console.log(chalk.cyan(`\nInstalling dependencies for ${sha.slice(0, 9)}…`));
153
+ const [installFile, ...installArgs] = parseCommandString(installCmd);
154
+ await execa(installFile, installArgs, {
155
+ cwd: checkout,
156
+ stdio: 'inherit',
157
+ verbose: 'short'
158
+ });
159
+ }
160
+ console.log(chalk.cyan(`\nBuilding packages for ${sha.slice(0, 9)}…`));
161
+ await build(checkout, buildCmd);
162
+ const packages = await packBuiltPackages(checkout, staging);
163
+ const manifest = {
164
+ buildCmd,
165
+ packages: packages.map(pkg => ({
166
+ ...pkg,
167
+ tarball: path.basename(pkg.tarball)
168
+ }))
169
+ };
170
+ // Written last: it is what marks the folder complete.
171
+ await writeFile(path.join(staging, MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`);
172
+ await rm(dir, {
173
+ recursive: true,
174
+ force: true
175
+ });
176
+ await rename(staging, dir);
177
+ return packages.map(pkg => ({
178
+ ...pkg,
179
+ tarball: path.join(dir, path.basename(pkg.tarball))
180
+ }));
181
+ } finally {
182
+ // On success the staging folder has already become `dir`.
183
+ await Promise.all([rm(staging, {
184
+ recursive: true,
185
+ force: true
186
+ }), removeCheckout(repoRoot, checkout)]);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Builds and packs the working tree into `outRoot`, naming each tarball by a hash of its bytes: an
192
+ * unchanged build keeps its path, so an install that depends on it has nothing to redo.
193
+ */
194
+ export async function packWorkingTree(options) {
195
+ const {
196
+ repoRoot,
197
+ outRoot,
198
+ buildCmd = 'pnpm release:build'
199
+ } = options;
200
+ console.log(chalk.cyan(`\nBuilding workspace packages for "working tree" (${buildCmd})…`));
201
+ await build(repoRoot, buildCmd);
202
+ console.log(chalk.cyan('\nPacking the working tree…'));
203
+ await rm(outRoot, {
204
+ recursive: true,
205
+ force: true
206
+ });
207
+ const packed = await packBuiltPackages(repoRoot, outRoot);
208
+ return Promise.all(packed.map(async pkg => {
209
+ const hash = await hashFile(pkg.tarball);
210
+ const tarball = path.join(outRoot, `${path.basename(pkg.tarball, '.tgz')}-${hash}.tgz`);
211
+ await rename(pkg.tarball, tarball);
212
+ return {
213
+ ...pkg,
214
+ tarball
215
+ };
216
+ }));
217
+ }
@@ -0,0 +1,12 @@
1
+ /** A publishable workspace package, as `pnpm ls` reports it. */
2
+ export interface PublishablePackage {
3
+ name: string;
4
+ version: string;
5
+ /** Absolute path to the package directory. */
6
+ path: string;
7
+ }
8
+ /**
9
+ * The publishable workspace packages of `cwd`: those pnpm lists that are not private and have the
10
+ * name and version publishing requires.
11
+ */
12
+ export declare function listPublishablePackages(cwd: string): Promise<PublishablePackage[]>;
package/utils/pnpm.mjs ADDED
@@ -0,0 +1,19 @@
1
+ import { $ } from 'execa';
2
+
3
+ /** A publishable workspace package, as `pnpm ls` reports it. */
4
+
5
+ /**
6
+ * The publishable workspace packages of `cwd`: those pnpm lists that are not private and have the
7
+ * name and version publishing requires.
8
+ */
9
+ export async function listPublishablePackages(cwd) {
10
+ const result = await $({
11
+ cwd
12
+ })`pnpm ls -r --json --depth -1`;
13
+ const listed = JSON.parse(result.stdout);
14
+ return listed.flatMap(pkg => pkg.private || !pkg.name || !pkg.version ? [] : [{
15
+ name: pkg.name,
16
+ version: pkg.version,
17
+ path: pkg.path
18
+ }]);
19
+ }
@@ -0,0 +1,2 @@
1
+ /** Creates a temporary directory and removes it when the current test ends, even if it throws. */
2
+ export declare function makeTempDir(): Promise<string>;
@@ -0,0 +1,16 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { onTestFinished } from 'vitest';
5
+
6
+ /** Creates a temporary directory and removes it when the current test ends, even if it throws. */
7
+ export async function makeTempDir() {
8
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'benchmark-test-'));
9
+ onTestFinished(async () => {
10
+ await fs.rm(tmpDir, {
11
+ recursive: true,
12
+ force: true
13
+ });
14
+ });
15
+ return tmpDir;
16
+ }
package/vitest.mjs CHANGED
@@ -34,6 +34,18 @@ function resolveViewport(option) {
34
34
  // workload needs them.
35
35
  const LAUNCH_ARGS = ['--js-flags=--expose-gc', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding'];
36
36
  export function createBenchmarkVitestConfig(options) {
37
+ // Benchmarks have to measure production React, which is why the config below sets NODE_ENV to
38
+ // "production" for the code running in the browser. That alone isn't enough: Vite picks which
39
+ // JSX runtime to compile against by reading NODE_ENV from the Node process, where Vitest has set
40
+ // it to "test". Benchmarks then get development JSX calling into a production React build, which
41
+ // has no `jsxDEV` function, and every benchmark crashes on its first render with
42
+ // "_jsxDEV is not a function".
43
+ //
44
+ // Setting NODE_ENV here, while this config file is still being evaluated, means Vite reads
45
+ // "production" as well and the two agree. Vitest considers this the intended way to run tests
46
+ // against production React: https://github.com/vitest-dev/vitest/issues/11265
47
+ // eslint-disable-next-line mui/consistent-production-guard -- an assignment, not a guard
48
+ process.env.NODE_ENV = 'production';
37
49
  const {
38
50
  outputPath,
39
51
  baselinePath,