From 489e96ea58343a04517d4718587d97d29a164747 Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Mon, 14 Feb 2022 11:54:25 -0800 Subject: [PATCH 1/2] Workflow for running large monorepo benchmark --- .github/workflows/large-monorepo.yml | 54 +++++++ benchmark/.gitignore | 2 + benchmark/src/index.ts | 220 +++++++++++++++++++++++++++ 3 files changed, 276 insertions(+) create mode 100644 .github/workflows/large-monorepo.yml create mode 100644 benchmark/.gitignore create mode 100644 benchmark/src/index.ts diff --git a/.github/workflows/large-monorepo.yml b/.github/workflows/large-monorepo.yml new file mode 100644 index 0000000000000..b9a1fa9b5cee7 --- /dev/null +++ b/.github/workflows/large-monorepo.yml @@ -0,0 +1,54 @@ +name: Large Repo Benchmark + +on: + push: + branches: ["main"] + pull_request: + types: [opened, synchronize] + +jobs: + build: + name: Run Benchmarks + timeout-minutes: 40 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + + steps: + - name: Check out code + uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: 1.17.6 + id: go + + - name: Build + run: cd cli && make turbo + + - name: Setup Node.js environment + uses: actions/setup-node@v2 + + - name: Install dependencies + run: cd benchmark && yarn + + - name: Run benchmarks + run: cd benchmark && yarn benchmark + + - name: Store Benchmark Result + uses: benchmark-action/github-action-benchmark@v1 + with: + name: "${{ runner.os }} Benchmark" + # What benchmark tool the output is formatted as + tool: "customSmallerIsBetter" + output-file-path: ./benchmark/benchmarks.json + benchmark-data-dir-path: benchmarks/large-repo/${{ runner.os }} + auto-push: ${{ github.event_name == 'push' }} + comment-on-alert: true + comment-always: true + # GitHub API token to make a commit on push, and a comment always + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/benchmark/.gitignore b/benchmark/.gitignore new file mode 100644 index 0000000000000..92ed26d1da698 --- /dev/null +++ b/benchmark/.gitignore @@ -0,0 +1,2 @@ +large-monorepo +benchmarks.json diff --git a/benchmark/src/index.ts b/benchmark/src/index.ts new file mode 100644 index 0000000000000..2ea621cb9b910 --- /dev/null +++ b/benchmark/src/index.ts @@ -0,0 +1,220 @@ +import cp from "child_process"; +import fs from "fs"; +import fse from "fs-extra"; +import path from "path"; + +const REPO_ROOT = "large-monorepo"; +const REPO_ORIGIN = "https://github.com/gsoltis/large-monorepo.git"; +const REPO_PATH = path.join(process.cwd(), REPO_ROOT); +const REPETITIONS = 5; + +const DEFAULT_EXEC_OPTS = { stdio: "inherit" as const, cwd: REPO_PATH }; +const TURBO_BIN = path.resolve(path.join("..", "cli", "turbo")); +const DEFAULT_CACHE_PATH = path.join( + REPO_PATH, + "node_modules", + ".cache", + "turbo" +); +const ALT_CACHE_PATH = path.join( + REPO_PATH, + "node_modules", + ".cache", + "turbo-benchmark" +); + +type Benchmark = { + name: string; + unit: string; + value: number; + range?: string; + extra?: string; +}; + +function setup(): void { + // Clone repo if it doesn't exist, run clean + if (fs.existsSync(REPO_ROOT)) { + // reset the repo, remove all changed or untracked files + cp.execSync( + `cd ${REPO_ROOT} && git reset --hard HEAD && git clean -f -d -X`, + { + stdio: "inherit", + } + ); + } else { + cp.execSync(`git clone ${REPO_ORIGIN}`, { stdio: "ignore" }); + } + + // Run install so we aren't benchmarking node_modules ... + + cp.execSync("yarn install", DEFAULT_EXEC_OPTS); +} + +function cleanTurboCache(): void { + if (fs.existsSync(DEFAULT_CACHE_PATH)) { + console.log("clearing cache"); + fs.rmSync(DEFAULT_CACHE_PATH, { recursive: true }); + } +} + +function cleanBuild(): Benchmark { + const timings: number[] = []; + let total = 0; + // We aren't really benchmarking this one, it OOMs if run in full parallel + for (let i = 0; i < 1; i++) { + // clean first, we'll leave the cache in place for subsequent builds + cleanTurboCache(); + const start = new Date().getTime(); + cp.execSync(`${TURBO_BIN} run build --concurrency=1`, DEFAULT_EXEC_OPTS); + const end = new Date().getTime(); + const timing = end - start; + total += timing; + timings.push(timing); + } + const avg = total / REPETITIONS; + const max = Math.max(...timings); + const min = Math.min(...timings); + return { + name: "Clean Build", + value: avg, + unit: "ms", + range: String(max - min), + }; +} + +function cachedBuild(): Benchmark { + const timings: number[] = []; + let total = 0; + for (let i = 0; i < REPETITIONS; i++) { + const start = new Date().getTime(); + cp.execSync(`${TURBO_BIN} run build`, DEFAULT_EXEC_OPTS); + const end = new Date().getTime(); + const timing = end - start; + total += timing; + timings.push(timing); + } + const avg = total / REPETITIONS; + const max = Math.max(...timings); + const min = Math.min(...timings); + return { + name: "Cached Build - no changes", + value: avg, + unit: "ms", + range: String(max - min), + }; +} + +function saveCache() { + // Remove any existing backup + if (fs.existsSync(ALT_CACHE_PATH)) { + fs.rmSync(ALT_CACHE_PATH, { recursive: true }); + } + // copy the current cache to the backup + if (fs.existsSync(DEFAULT_CACHE_PATH)) { + fse.copySync(DEFAULT_CACHE_PATH, ALT_CACHE_PATH, { recursive: true }); + } else { + // make an empty cache + fs.mkdirSync(ALT_CACHE_PATH, { recursive: true }); + } +} + +function restoreSavedCache() { + // Remove any existing cache + if (fs.existsSync(DEFAULT_CACHE_PATH)) { + fs.rmSync(DEFAULT_CACHE_PATH, { recursive: true }); + } + // Copy the backed-up cache to the real cache + fse.copySync(ALT_CACHE_PATH, DEFAULT_CACHE_PATH, { recursive: true }); +} + +function cachedBuildWithDelta(): Benchmark { + // Save existing cache just once, we'll restore from it each time + saveCache(); + + // Edit a file in place + const file = path.join( + REPO_PATH, + "packages", + "crew", + "important-feature-0", + "src", + "lib", + "important-component-0", + "important-component-0.tsx" + ); + const contents = fs.readFileSync(file).toString("utf-8"); + // make a small edit + const updated = contents.replace("-0!", "-0!!"); + fs.writeFileSync(file, updated); + + const timings: number[] = []; + let total = 0; + for (let i = 0; i < REPETITIONS; i++) { + // Make sure we're starting with the cache from before we make the source code edit + restoreSavedCache(); + const start = new Date().getTime(); + cp.execSync(`${TURBO_BIN} run build`, DEFAULT_EXEC_OPTS); + const end = new Date().getTime(); + const timing = end - start; + total += timing; + timings.push(timing); + } + const avg = total / REPETITIONS; + const max = Math.max(...timings); + const min = Math.min(...timings); + return { + name: "Cached Build - source code change", + value: avg, + unit: "ms", + range: String(max - min), + }; +} + +function cachedBuildWithDependencyChange(): Benchmark { + // Save existing cache just once, we'll restore from it each time + saveCache(); + + // Edit a dependency + const file = path.join(REPO_PATH, "apps", "navigation", "package.json"); + const contents = JSON.parse(fs.readFileSync(file).toString("utf-8")); + contents.dependencies["crew-important-feature-0"] = "*"; + fs.writeFileSync(file, JSON.stringify(contents, null, 2)); + + const timings: number[] = []; + let total = 0; + for (let i = 0; i < REPETITIONS; i++) { + // Make sure we're starting with the cache from before we made the dependency edit + restoreSavedCache(); + const start = new Date().getTime(); + cp.execSync(`${TURBO_BIN} run build`, DEFAULT_EXEC_OPTS); + const end = new Date().getTime(); + const timing = end - start; + total += timing; + timings.push(timing); + } + const avg = total / REPETITIONS; + const max = Math.max(...timings); + const min = Math.min(...timings); + return { + name: "Cached Build - dependency change", + value: avg, + unit: "ms", + range: String(max - min), + }; +} + +cp.execSync(`${TURBO_BIN} --version`, { stdio: "inherit" }); + +const benchmarks: Benchmark[] = []; +console.log("setup"); +setup(); +console.log("clean build"); +benchmarks.push(cleanBuild()); +console.log("cached build - no change"); +benchmarks.push(cachedBuild()); +console.log("cached build - code change"); +benchmarks.push(cachedBuildWithDelta()); +console.log("cached build - dependency change"); +benchmarks.push(cachedBuildWithDependencyChange()); +console.log(JSON.stringify(benchmarks, null, 2)); +fs.writeFileSync("benchmarks.json", JSON.stringify(benchmarks, null, 2)); From a06c79ed6a458c5cf0cd5c91e0675ff15ee041dc Mon Sep 17 00:00:00 2001 From: Greg Soltis Date: Mon, 14 Feb 2022 16:55:13 -0800 Subject: [PATCH 2/2] Add package.json, handle local flag --- benchmark/package.json | 12 ++++ benchmark/src/index.ts | 10 ++- benchmark/yarn.lock | 156 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 benchmark/package.json create mode 100644 benchmark/yarn.lock diff --git a/benchmark/package.json b/benchmark/package.json new file mode 100644 index 0000000000000..38de6df0c68d9 --- /dev/null +++ b/benchmark/package.json @@ -0,0 +1,12 @@ +{ + "name": "benchmark", + "version": "1.0.0", + "dependencies": { + "esbuild": "^0.14.21", + "esbuild-register": "^3.3.2", + "fs-extra": "^10.0.0" + }, + "scripts": { + "benchmark": "node -r esbuild-register src/index.ts" + } +} diff --git a/benchmark/src/index.ts b/benchmark/src/index.ts index 2ea621cb9b910..d615c34ced633 100644 --- a/benchmark/src/index.ts +++ b/benchmark/src/index.ts @@ -8,7 +8,7 @@ const REPO_ORIGIN = "https://github.com/gsoltis/large-monorepo.git"; const REPO_PATH = path.join(process.cwd(), REPO_ROOT); const REPETITIONS = 5; -const DEFAULT_EXEC_OPTS = { stdio: "inherit" as const, cwd: REPO_PATH }; +const DEFAULT_EXEC_OPTS = { stdio: "ignore" as const, cwd: REPO_PATH }; const TURBO_BIN = path.resolve(path.join("..", "cli", "turbo")); const DEFAULT_CACHE_PATH = path.join( REPO_PATH, @@ -60,12 +60,16 @@ function cleanTurboCache(): void { function cleanBuild(): Benchmark { const timings: number[] = []; let total = 0; + const isLocal = process.argv[process.argv.length - 1] == "--local"; // We aren't really benchmarking this one, it OOMs if run in full parallel - for (let i = 0; i < 1; i++) { + // on GH actions + const repetitions = isLocal ? REPETITIONS : 1; + const concurrency = isLocal ? "" : " --concurrency=1"; + for (let i = 0; i < repetitions; i++) { // clean first, we'll leave the cache in place for subsequent builds cleanTurboCache(); const start = new Date().getTime(); - cp.execSync(`${TURBO_BIN} run build --concurrency=1`, DEFAULT_EXEC_OPTS); + cp.execSync(`${TURBO_BIN} run build${concurrency}`, DEFAULT_EXEC_OPTS); const end = new Date().getTime(); const timing = end - start; total += timing; diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock new file mode 100644 index 0000000000000..237dcf88d91ad --- /dev/null +++ b/benchmark/yarn.lock @@ -0,0 +1,156 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +esbuild-android-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.21.tgz#8842d0c3b7c81fbe2dc46ddb416ffd6eb822184b" + integrity sha512-Bqgld1TY0wZv8TqiQmVxQFgYzz8ZmyzT7clXBDZFkOOdRybzsnj8AZuK1pwcLVA7Ya6XncHgJqIao7NFd3s0RQ== + +esbuild-darwin-64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.21.tgz#ec7df02ad88ecf7f8fc23a3ed7917e07dea0c9c9" + integrity sha512-j+Eg+e13djzyYINVvAbOo2/zvZ2DivuJJTaBrJnJHSD7kUNuGHRkHoSfFjbI80KHkn091w350wdmXDNSgRjfYQ== + +esbuild-darwin-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.21.tgz#0c2a977edec1ef54097ee56a911518c820d4e5e4" + integrity sha512-nDNTKWDPI0RuoPj5BhcSB2z5EmZJJAyRtZLIjyXSqSpAyoB8eyAKXl4lB8U2P78Fnh4Lh1le/fmpewXE04JhBQ== + +esbuild-freebsd-64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.21.tgz#f5b5fc1d031286c3a0949d1bda7db774b7d0404e" + integrity sha512-zIurkCHXhxELiDZtLGiexi8t8onQc2LtuE+S7457H/pP0g0MLRKMrsn/IN4LDkNe6lvBjuoZZi2OfelOHn831g== + +esbuild-freebsd-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.21.tgz#a05cab908013e4992b31a675850b8c44eb468c0c" + integrity sha512-wdxMmkJfbwcN+q85MpeUEamVZ40FNsBa9mPq8tAszDn8TRT2HoJvVRADPIIBa9SWWwlDChIMjkDKAnS3KS/sPA== + +esbuild-linux-32@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.21.tgz#638d244cc58b951f447addb4bade628d126ef84b" + integrity sha512-fmxvyzOPPh2xiEHojpCeIQP6pXcoKsWbz3ryDDIKLOsk4xp3GbpHIEAWP0xTeuhEbendmvBDVKbAVv3PnODXLg== + +esbuild-linux-64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.21.tgz#8eb634abee928be7e35b985fafbfef2f2e31397f" + integrity sha512-edZyNOv1ql+kpmlzdqzzDjRQYls+tSyi4QFi+PdBhATJFUqHsnNELWA9vMSzAaInPOEaVUTA5Ml28XFChcy4DA== + +esbuild-linux-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.21.tgz#e05599ea6253b58394157da162d856f3ead62f9e" + integrity sha512-t5qxRkq4zdQC0zXpzSB2bTtfLgOvR0C6BXYaRE/6/k8/4SrkZcTZBeNu+xGvwCU4b5dU9ST9pwIWkK6T1grS8g== + +esbuild-linux-arm@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.21.tgz#1ae1078231cf689d3ba894a32d3723c0be9b91fd" + integrity sha512-aSU5pUueK6afqmLQsbU+QcFBT62L+4G9hHMJDHWfxgid6hzhSmfRH9U/f+ymvxsSTr/HFRU4y7ox8ZyhlVl98w== + +esbuild-linux-mips64le@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.21.tgz#f05be62d126764e99b37edcac5bb49b78c7a8890" + integrity sha512-jLZLQGCNlUsmIHtGqNvBs3zN+7a4D9ckf0JZ+jQTwHdZJ1SgV9mAjbB980OFo66LoY+WeM7t3WEnq3FjI1zw4A== + +esbuild-linux-ppc64le@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.21.tgz#592c98d82dad7982268ef8deed858c4566f07ab1" + integrity sha512-4TWxpK391en2UBUw6GSrukToTDu6lL9vkm3Ll40HrI08WG3qcnJu7bl8e1+GzelDsiw1QmfAY/nNvJ6iaHRpCQ== + +esbuild-linux-riscv64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.21.tgz#0db7bd6f10d8f9afea973a7d6bf87b449b864b7b" + integrity sha512-fElngqOaOfTsF+u+oetDLHsPG74vB2ZaGZUqmGefAJn3a5z9Z2pNa4WpVbbKgHpaAAy5tWM1m1sbGohj6Ki6+Q== + +esbuild-linux-s390x@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.21.tgz#254a9354d34c9d1b41a3e21d2ec9269cbbb2c5df" + integrity sha512-brleZ6R5fYv0qQ7ZBwenQmP6i9TdvJCB092c/3D3pTLQHBGHJb5zWgKxOeS7bdHzmLy6a6W7GbFk6QKpjyD6QA== + +esbuild-netbsd-64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.21.tgz#4cb783d060b02bf3b897a9a12cce2b3b547726f8" + integrity sha512-nCEgsLCQ8RoFWVV8pVI+kX66ICwbPP/M9vEa0NJGIEB/Vs5sVGMqkf67oln90XNSkbc0bPBDuo4G6FxlF7PN8g== + +esbuild-openbsd-64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.21.tgz#f886b93feefddbe573528fa4b421c9c6e2bc969b" + integrity sha512-h9zLMyVD0T73MDTVYIb/qUTokwI6EJH9O6wESuTNq6+XpMSr6C5aYZ4fvFKdNELW+Xsod+yDS2hV2JTUAbFrLA== + +esbuild-register@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/esbuild-register/-/esbuild-register-3.3.2.tgz#1c3dc7179cabb4c7bd640a393eb916b18b12a223" + integrity sha512-jceAtTO6zxPmCfSD5cBb3rgIK1vmuqCKYwgylHiS1BF4pq0jJiJb4K2QMuqF4BEw7XDBRatYzip0upyTzfkgsQ== + +esbuild-sunos-64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.21.tgz#3829e4d57d4cb6950837fe90b0b67cdfb37cf13a" + integrity sha512-Kl+7Cot32qd9oqpLdB1tEGXEkjBlijrIxMJ0+vlDFaqsODutif25on0IZlFxEBtL2Gosd4p5WCV1U7UskNQfXA== + +esbuild-windows-32@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.21.tgz#b858a22d1a82e53cdc59310cd56294133f7a95e7" + integrity sha512-V7vnTq67xPBUCk/9UtlolmQ798Ecjdr1ZoI1vcSgw7M82aSSt0eZdP6bh5KAFZU8pxDcx3qoHyWQfHYr11f22A== + +esbuild-windows-64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.21.tgz#7bb5a027d5720cf9caf18a4bedd11327208f1f12" + integrity sha512-kDgHjKOHwjfJDCyRGELzVxiP/RBJBTA+wyspf78MTTJQkyPuxH2vChReNdWc+dU2S4gIZFHMdP1Qrl/k22ZmaA== + +esbuild-windows-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.21.tgz#25df54521ad602c826b262ea2e7cc1fe80f5c2f5" + integrity sha512-8Sbo0zpzgwWrwjQYLmHF78f7E2xg5Ve63bjB2ng3V2aManilnnTGaliq2snYg+NOX60+hEvJHRdVnuIAHW0lVw== + +esbuild@^0.14.21: + version "0.14.21" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.21.tgz#b3e05f900f1c4394f596d60d63d9816468f0f671" + integrity sha512-7WEoNMBJdLN993dr9h0CpFHPRc3yFZD+EAVY9lg6syJJ12gc5fHq8d75QRExuhnMkT2DaRiIKFThRvDWP+fO+A== + optionalDependencies: + esbuild-android-arm64 "0.14.21" + esbuild-darwin-64 "0.14.21" + esbuild-darwin-arm64 "0.14.21" + esbuild-freebsd-64 "0.14.21" + esbuild-freebsd-arm64 "0.14.21" + esbuild-linux-32 "0.14.21" + esbuild-linux-64 "0.14.21" + esbuild-linux-arm "0.14.21" + esbuild-linux-arm64 "0.14.21" + esbuild-linux-mips64le "0.14.21" + esbuild-linux-ppc64le "0.14.21" + esbuild-linux-riscv64 "0.14.21" + esbuild-linux-s390x "0.14.21" + esbuild-netbsd-64 "0.14.21" + esbuild-openbsd-64 "0.14.21" + esbuild-sunos-64 "0.14.21" + esbuild-windows-32 "0.14.21" + esbuild-windows-64 "0.14.21" + esbuild-windows-arm64 "0.14.21" + +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==