@mui/internal-benchmark 0.1.1-canary.0 → 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.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-benchmark",
3
- "version": "0.1.1-canary.0",
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": {
@@ -11,9 +11,11 @@
11
11
  "dependencies": {
12
12
  "@babel/runtime": "^8.0.5",
13
13
  "@vitejs/plugin-react": "^6.1.1",
14
+ "chalk": "^5.6.2",
14
15
  "env-ci": "^11.2.0",
16
+ "es-toolkit": "^1.52.0",
15
17
  "execa": "^10.0.1",
16
- "zod": "^4.6.4"
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": "764e3b3cfc14293e74e93f3413609025f502bab9"
73
+ "gitSha": "c2a0ca83b8dd00ae2fa2b5714dbc6684d50926d9"
72
74
  }
@@ -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
+ }