From d3f4fdda3283ccce84f7f555ff7d5e70abc37a26 Mon Sep 17 00:00:00 2001 From: AriPerkkio Date: Mon, 26 Jun 2023 13:42:48 +0300 Subject: [PATCH] feat: first projects --- builds/.gitkeep | 0 ecosystem-ci.ts | 177 ++++++++++ package.json | 20 +- pnpm-lock.yaml | 414 ++++++++++++++++++++++- tests/_selftest.ts | 30 ++ tests/vitest-sonar-reporter.ts | 11 + tsconfig.json | 18 + types.d.ts | 56 +++ utils.ts | 598 +++++++++++++++++++++++++++++++++ 9 files changed, 1303 insertions(+), 21 deletions(-) create mode 100644 builds/.gitkeep create mode 100644 ecosystem-ci.ts create mode 100644 tests/_selftest.ts create mode 100644 tests/vitest-sonar-reporter.ts create mode 100644 tsconfig.json create mode 100644 types.d.ts create mode 100644 utils.ts diff --git a/builds/.gitkeep b/builds/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ecosystem-ci.ts b/ecosystem-ci.ts new file mode 100644 index 0000000..dcd75ba --- /dev/null +++ b/ecosystem-ci.ts @@ -0,0 +1,177 @@ +import fs from 'fs' +import path from 'path' +import process from 'process' +import { cac } from 'cac' + +import { + setupEnvironment, + setupVitestRepo, + buildVitest, + bisectVitest, + parseVitestMajor, + parseMajorVersion, +} from './utils' +import { CommandOptions, RunOptions } from './types' + +const cli = cac() +cli + .command('[...suites]', 'build vitest and run selected suites') + .option('--verify', 'verify checkouts by running tests', { default: false }) + .option('--repo ', 'vitest repository to use', { + default: 'vitest-dev/vitest', + }) + .option('--branch ', 'vitest branch to use', { default: 'main' }) + .option('--tag ', 'vitest tag to use') + .option('--commit ', 'vitest commit sha to use') + .option('--release ', 'vitest release to use from npm registry') + .action(async (suites, options: CommandOptions) => { + const { root, vitestPath, workspace } = await setupEnvironment() + const suitesToRun = getSuitesToRun(suites, root) + let vitestMajor + if (!options.release) { + await setupVitestRepo(options) + await buildVitest({ verify: options.verify }) + vitestMajor = parseVitestMajor(vitestPath) + } else { + vitestMajor = parseMajorVersion(options.release) + } + const runOptions: RunOptions = { + root, + vitestPath, + vitestMajor, + workspace, + release: options.release, + verify: options.verify, + skipGit: false, + } + for (const suite of suitesToRun) { + await run(suite, runOptions) + } + }) + +cli + .command('build-vitest', 'build vitest only') + .option('--verify', 'verify vitest checkout by running tests', { + default: false, + }) + .option('--repo ', 'vitest repository to use', { + default: 'vitest-dev/vitest', + }) + .option('--branch ', 'vitest branch to use', { default: 'main' }) + .option('--tag ', 'vitest tag to use') + .option('--commit ', 'vitest commit sha to use') + .action(async (options: CommandOptions) => { + await setupEnvironment() + await setupVitestRepo(options) + await buildVitest({ verify: options.verify }) + }) + +cli + .command('run-suites [...suites]', 'run single suite with pre-built vitest') + .option( + '--verify', + 'verify checkout by running tests before using local vitest', + { default: false }, + ) + .option('--repo ', 'vitest repository to use', { + default: 'vitest-dev/vitest', + }) + .option('--release ', 'vitest release to use from npm registry') + .action(async (suites, options: CommandOptions) => { + const { root, vitestPath, workspace } = await setupEnvironment() + const suitesToRun = getSuitesToRun(suites, root) + const runOptions: RunOptions = { + ...options, + root, + vitestPath, + vitestMajor: parseVitestMajor(vitestPath), + workspace, + } + for (const suite of suitesToRun) { + await run(suite, runOptions) + } + }) + +cli + .command( + 'bisect [...suites]', + 'use git bisect to find a commit in vitest that broke suites', + ) + .option('--good ', 'last known good ref, e.g. a previous tag. REQUIRED!') + .option('--verify', 'verify checkouts by running tests', { default: false }) + .option('--repo ', 'vitest repository to use', { + default: 'vitest-dev/vitest', + }) + .option('--branch ', 'vitest branch to use', { default: 'main' }) + .option('--tag ', 'vitest tag to use') + .option('--commit ', 'vitest commit sha to use') + .action(async (suites, options: CommandOptions & { good: string }) => { + if (!options.good) { + console.log( + 'you have to specify a known good version with `--good `', + ) + process.exit(1) + } + const { root, vitestPath, workspace } = await setupEnvironment() + const suitesToRun = getSuitesToRun(suites, root) + let isFirstRun = true + const { verify } = options + const runSuite = async () => { + try { + await buildVitest({ verify: isFirstRun && verify }) + for (const suite of suitesToRun) { + await run(suite, { + verify: !!(isFirstRun && verify), + skipGit: !isFirstRun, + root, + vitestPath, + vitestMajor: parseVitestMajor(vitestPath), + workspace, + }) + } + isFirstRun = false + return null + } catch (e) { + return e + } + } + await setupVitestRepo({ ...options, shallow: false }) + const initialError = await runSuite() + if (initialError) { + await bisectVitest(options.good, runSuite) + } else { + console.log(`no errors for starting commit, cannot bisect`) + } + }) +cli.help() +cli.parse() + +async function run(suite: string, options: RunOptions) { + const { test } = await import(`./tests/${suite}.ts`) + await test({ + ...options, + workspace: path.resolve(options.workspace, suite), + }) +} + +function getSuitesToRun(suites: string[], root: string) { + let suitesToRun: string[] = suites + const availableSuites: string[] = fs + .readdirSync(path.join(root, 'tests')) + .filter((f: string) => !f.startsWith('_') && f.endsWith('.ts')) + .map((f: string) => f.slice(0, -3)) + availableSuites.sort() + if (suitesToRun.length === 0) { + suitesToRun = availableSuites + } else { + const invalidSuites = suitesToRun.filter( + (x) => !x.startsWith('_') && !availableSuites.includes(x), + ) + if (invalidSuites.length) { + console.log(`invalid suite(s): ${invalidSuites.join(', ')}`) + console.log(`available suites: ${availableSuites.join(', ')}`) + process.exit(1) + } + } + return suitesToRun +} diff --git a/package.json b/package.json index 6434e82..afa298a 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,13 @@ "description": "Vitest Ecosystem CI", "scripts": { "prepare": "pnpm exec simple-git-hooks", - "lint": "echo TODO", + "lint": "eslint --ignore-path .gitignore '**/*.ts'", "lint:fix": "pnpm lint --fix", "format": "prettier --ignore-path .gitignore --check .", "format:fix": "pnpm format --write", - "test:self": "echo TODO", - "test": "echo TODO", - "bisect": "echo TODO" + "test:self": "tsx ecosystem-ci.ts _selftest", + "test": "tsx ecosystem-ci.ts", + "bisect": "tsx ecosystem-ci.ts bisect" }, "simple-git-hooks": { "pre-commit": "pnpm exec lint-staged --concurrent false" @@ -38,8 +38,16 @@ "url": "https://github.com/vitest-dev/vitest-ecosystem-ci/issues" }, "homepage": "https://github.com/vitest-dev/vitest-ecosystem-ci#readme", - "dependencies": {}, + "dependencies": { + "@actions/core": "^1.10.0", + "cac": "^6.7.14", + "execa": "^7.1.1", + "node-fetch": "^3.3.1" + }, "devDependencies": { + "@antfu/ni": "^0.21.4", + "@types/node": "^18.16.18", + "@types/semver": "^7.5.0", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "eslint": "^8.43.0", @@ -47,7 +55,9 @@ "eslint-plugin-n": "^16.0.0", "lint-staged": "^13.2.2", "prettier": "^2.8.8", + "semver": "^7.5.2", "simple-git-hooks": "^2.8.1", + "tsx": "^3.12.7", "typescript": "^5.1.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df8fed3..020b81d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,7 +4,30 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + '@actions/core': + specifier: ^1.10.0 + version: 1.10.0 + cac: + specifier: ^6.7.14 + version: 6.7.14 + execa: + specifier: ^7.1.1 + version: 7.1.1 + node-fetch: + specifier: ^3.3.1 + version: 3.3.1 + devDependencies: + '@antfu/ni': + specifier: ^0.21.4 + version: 0.21.4 + '@types/node': + specifier: ^18.16.18 + version: 18.16.18 + '@types/semver': + specifier: ^7.5.0 + version: 7.5.0 '@typescript-eslint/eslint-plugin': specifier: ^5.60.0 version: 5.60.0(@typescript-eslint/parser@5.60.0)(eslint@8.43.0)(typescript@5.1.3) @@ -26,15 +49,258 @@ devDependencies: prettier: specifier: ^2.8.8 version: 2.8.8 + semver: + specifier: ^7.5.2 + version: 7.5.2 simple-git-hooks: specifier: ^2.8.1 version: 2.8.1 + tsx: + specifier: ^3.12.7 + version: 3.12.7 typescript: specifier: ^5.1.3 version: 5.1.3 packages: + /@actions/core@1.10.0: + resolution: {integrity: sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==} + dependencies: + '@actions/http-client': 2.1.0 + uuid: 8.3.2 + dev: false + + /@actions/http-client@2.1.0: + resolution: {integrity: sha512-BonhODnXr3amchh4qkmjPMUO8mFi/zLaaCeCAJZqch8iQqyDnVIkySjB38VHAC8IJ+bnlgfOqlhpyCUZHlQsqw==} + dependencies: + tunnel: 0.0.6 + dev: false + + /@antfu/ni@0.21.4: + resolution: {integrity: sha512-O0Uv9LbLDSoEg26fnMDdDRiPwFJnQSoD4WnrflDwKCJm8Cx/0mV4cGxwBLXan5mGIrpK4Dd7vizf4rQm0QCEAA==} + hasBin: true + dev: true + + /@esbuild-kit/cjs-loader@2.4.2: + resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==} + dependencies: + '@esbuild-kit/core-utils': 3.1.0 + get-tsconfig: 4.6.0 + dev: true + + /@esbuild-kit/core-utils@3.1.0: + resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==} + dependencies: + esbuild: 0.17.19 + source-map-support: 0.5.21 + dev: true + + /@esbuild-kit/esm-loader@2.5.5: + resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==} + dependencies: + '@esbuild-kit/core-utils': 3.1.0 + get-tsconfig: 4.6.0 + dev: true + + /@esbuild/android-arm64@0.17.19: + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.17.19: + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.17.19: + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.17.19: + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.17.19: + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.17.19: + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.17.19: + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.17.19: + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.17.19: + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.17.19: + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.17.19: + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.17.19: + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.17.19: + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.17.19: + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.17.19: + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.17.19: + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.17.19: + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.17.19: + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.17.19: + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.17.19: + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.17.19: + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.17.19: + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.43.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -117,6 +383,10 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/node@18.16.18: + resolution: {integrity: sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw==} + dev: true + /@types/semver@7.5.0: resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} dev: true @@ -343,12 +613,21 @@ packages: fill-range: 7.0.1 dev: true + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: semver: 7.5.2 dev: true + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -426,7 +705,11 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true + + /data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + dev: false /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -470,6 +753,36 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true + /esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + dev: true + /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -628,7 +941,6 @@ packages: onetime: 6.0.0 signal-exit: 3.0.7 strip-final-newline: 3.0.0 - dev: true /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -659,6 +971,14 @@ packages: reusify: 1.0.4 dev: true + /fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.2.1 + dev: false + /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -693,10 +1013,25 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true + /formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + dependencies: + fetch-blob: 3.2.0 + dev: false + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} dev: true @@ -704,6 +1039,11 @@ packages: /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + + /get-tsconfig@4.6.0: + resolution: {integrity: sha512-lgbo68hHTQnFddybKbbs/RDRJnJT5YyGy2kQzVwbq+g67X73i+5MVTval34QxGkOe9X5Ujf1UYpCaphLyltjEg==} + dependencies: + resolve-pkg-maps: 1.0.0 dev: true /glob-parent@5.1.2: @@ -773,7 +1113,6 @@ packages: /human-signals@4.3.1: resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} engines: {node: '>=14.18.0'} - dev: true /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} @@ -850,11 +1189,9 @@ packages: /is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true /js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} @@ -956,7 +1293,6 @@ packages: /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -979,7 +1315,6 @@ packages: /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} - dev: true /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -999,6 +1334,20 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: false + + /node-fetch@3.3.1: + resolution: {integrity: sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + dev: false + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -1009,7 +1358,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: path-key: 4.0.0 - dev: true /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} @@ -1033,7 +1381,6 @@ packages: engines: {node: '>=12'} dependencies: mimic-fn: 4.0.0 - dev: true /optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} @@ -1088,12 +1435,10 @@ packages: /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - dev: true /path-key@4.0.0: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} - dev: true /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1140,6 +1485,10 @@ packages: engines: {node: '>=4'} dev: true + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve@1.22.2: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} hasBin: true @@ -1198,16 +1547,13 @@ packages: engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 - dev: true /shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - dev: true /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true /simple-git-hooks@2.8.1: resolution: {integrity: sha512-DYpcVR1AGtSfFUNzlBdHrQGPsOhuuEJ/FkmPOOlFysP60AHd3nsEpkGq/QEOdtUyT1Qhk7w9oLmFoMG+75BDog==} @@ -1246,6 +1592,18 @@ packages: is-fullwidth-code-point: 4.0.0 dev: true + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + /string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -1286,7 +1644,6 @@ packages: /strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} - dev: true /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -1338,6 +1695,22 @@ packages: typescript: 5.1.3 dev: true + /tsx@3.12.7: + resolution: {integrity: sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw==} + hasBin: true + dependencies: + '@esbuild-kit/cjs-loader': 2.4.2 + '@esbuild-kit/core-utils': 3.1.0 + '@esbuild-kit/esm-loader': 2.5.5 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1367,13 +1740,22 @@ packages: punycode: 2.3.0 dev: true + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + + /web-streams-polyfill@3.2.1: + resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} + engines: {node: '>= 8'} + dev: false + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true dependencies: isexe: 2.0.0 - dev: true /word-wrap@1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} diff --git a/tests/_selftest.ts b/tests/_selftest.ts new file mode 100644 index 0000000..4f05dbe --- /dev/null +++ b/tests/_selftest.ts @@ -0,0 +1,30 @@ +import path from 'path' +import fs from 'fs' +import { runInRepo } from '../utils' +import { RunOptions } from '../types' + +export async function test(options: RunOptions) { + await runInRepo({ + ...options, + repo: 'vitest-dev/vitest-ecosystem-ci', + build: async () => { + const dir = path.resolve(options.workspace, 'vitest-ecosystem-ci') + const pkgFile = path.join(dir, 'package.json') + const pkg = JSON.parse(await fs.promises.readFile(pkgFile, 'utf-8')) + if (pkg.name !== 'vitest-ecosystem-ci') { + throw new Error( + `invalid checkout, expected package.json with "name":"vitest-ecosystem-ci" in ${dir}`, + ) + } + pkg.scripts.selftestscript = + "[ -d ../../vitest/packages/vitest/dist ] || (echo 'vitest build failed' && exit 1)" + await fs.promises.writeFile( + pkgFile, + JSON.stringify(pkg, null, 2), + 'utf-8', + ) + }, + test: 'pnpm run selftestscript', + verify: false, + }) +} diff --git a/tests/vitest-sonar-reporter.ts b/tests/vitest-sonar-reporter.ts new file mode 100644 index 0000000..3eb8b8d --- /dev/null +++ b/tests/vitest-sonar-reporter.ts @@ -0,0 +1,11 @@ +import { runInRepo } from '../utils' +import { RunOptions } from '../types' + +export async function test(options: RunOptions) { + await runInRepo({ + ...options, + repo: 'AriPerkkio/vitest-sonar-reporter', + build: 'build', + test: 'test', + }) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1f7868b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "include": ["./**/*.ts"], + "exclude": ["**/node_modules/**"], + "compilerOptions": { + "target": "esnext", + "module": "nodenext", + "moduleResolution": "node", + "strict": true, + "declaration": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "esModuleInterop": true, + "useUnknownInCatchVariables": false, + "allowSyntheticDefaultImports": true, + "lib": ["esnext"], + "sourceMap": true + } +} diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..bc1e7bc --- /dev/null +++ b/types.d.ts @@ -0,0 +1,56 @@ +// eslint-disable-next-line n/no-unpublished-import +import type { Agent } from '@antfu/ni' +export interface EnvironmentData { + root: string + workspace: string + vitestPath: string + cwd: string + env: ProcessEnv +} + +export interface RunOptions { + workspace: string + root: string + vitestPath: string + vitestMajor: number + verify?: boolean + skipGit?: boolean + release?: string + agent?: Agent + build?: Task | Task[] + test?: Task | Task[] + beforeInstall?: Task | Task[] + beforeBuild?: Task | Task[] + beforeTest?: Task | Task[] +} + +type Task = string | (() => Promise) + +export interface CommandOptions { + suites?: string[] + repo?: string + branch?: string + tag?: string + commit?: string + release?: string + verify?: boolean + skipGit?: boolean +} + +export interface RepoOptions { + repo: string + dir?: string + branch?: string + tag?: string + commit?: string + shallow?: boolean + overrides?: Overrides +} + +export interface Overrides { + [key: string]: string | boolean +} + +export interface ProcessEnv { + [key: string]: string | undefined +} diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..caf2dc5 --- /dev/null +++ b/utils.ts @@ -0,0 +1,598 @@ +import path from 'path' +import fs from 'fs' +import { fileURLToPath, pathToFileURL } from 'url' +import { execaCommand } from 'execa' +import { + EnvironmentData, + Overrides, + ProcessEnv, + RepoOptions, + RunOptions, + Task, +} from './types' +//eslint-disable-next-line n/no-unpublished-import +import { detect, AGENTS, Agent, getCommand } from '@antfu/ni' +import actionsCore from '@actions/core' +// eslint-disable-next-line n/no-unpublished-import +import * as semver from 'semver' + +const isGitHubActions = !!process.env.GITHUB_ACTIONS + +let vitestPath: string +let cwd: string +let env: ProcessEnv + +const VITEST_SUB_PACKAGES = [ + 'browser', + 'coverage-istanbul', + 'expect', + 'snapshot', + 'ui', + 'web-worker', + 'coverage-c8', + 'coverage-v8', + 'runner', + 'spy', + 'utils', + 'ws-client', +] + +function cd(dir: string) { + cwd = path.resolve(cwd, dir) +} + +export async function $(literals: TemplateStringsArray, ...values: any[]) { + const cmd = literals.reduce( + (result, current, i) => + result + current + (values?.[i] != null ? `${values[i]}` : ''), + '', + ) + + if (isGitHubActions) { + actionsCore.startGroup(`${cwd} $> ${cmd}`) + } else { + console.log(`${cwd} $> ${cmd}`) + } + + const proc = execaCommand(cmd, { + env, + stdio: 'pipe', + cwd, + }) + proc.stdin && process.stdin.pipe(proc.stdin) + proc.stdout && proc.stdout.pipe(process.stdout) + proc.stderr && proc.stderr.pipe(process.stderr) + const result = await proc + + if (isGitHubActions) { + actionsCore.endGroup() + } + + return result.stdout +} + +export async function setupEnvironment(): Promise { + // @ts-expect-error import.meta + const root = dirnameFrom(import.meta.url) + const workspace = path.resolve(root, 'workspace') + vitestPath = path.resolve(workspace, 'vitest') + cwd = process.cwd() + env = { + ...process.env, + CI: 'true', + TURBO_FORCE: 'true', // disable turbo caching, ecosystem-ci modifies things and we don't want replays + YARN_ENABLE_IMMUTABLE_INSTALLS: 'false', // to avoid errors with mutated lockfile due to overrides + NODE_OPTIONS: '--max-old-space-size=6144', // GITHUB CI has 7GB max, stay below + ECOSYSTEM_CI: 'true', // flag for tests, can be used to conditionally skip irrelevant tests. + } + initWorkspace(workspace) + return { root, workspace, vitestPath, cwd, env } +} + +function initWorkspace(workspace: string) { + if (!fs.existsSync(workspace)) { + fs.mkdirSync(workspace, { recursive: true }) + } + const eslintrc = path.join(workspace, '.eslintrc.json') + if (!fs.existsSync(eslintrc)) { + fs.writeFileSync(eslintrc, '{"root":true}\n', 'utf-8') + } + const editorconfig = path.join(workspace, '.editorconfig') + if (!fs.existsSync(editorconfig)) { + fs.writeFileSync(editorconfig, 'root = true\n', 'utf-8') + } +} + +export async function setupRepo(options: RepoOptions) { + if (options.branch == null) { + options.branch = 'main' + } + if (options.shallow == null) { + options.shallow = true + } + + let { repo, commit, branch, tag, dir, shallow } = options + if (!dir) { + throw new Error('setupRepo must be called with options.dir') + } + if (!repo.includes(':')) { + repo = `https://github.com/${repo}.git` + } + + let needClone = true + if (fs.existsSync(dir)) { + const _cwd = cwd + cd(dir) + let currentClonedRepo: string | undefined + try { + currentClonedRepo = await $`git ls-remote --get-url` + } catch { + // when not a git repo + } + cd(_cwd) + + if (repo === currentClonedRepo) { + needClone = false + } else { + fs.rmSync(dir, { recursive: true, force: true }) + } + } + + if (needClone) { + await $`git -c advice.detachedHead=false clone ${ + shallow ? '--depth=1 --no-tags' : '' + } --branch ${tag || branch} ${repo} ${dir}` + } + cd(dir) + await $`git clean -fdxq` + await $`git fetch ${shallow ? '--depth=1 --no-tags' : '--tags'} origin ${ + tag ? `tag ${tag}` : `${commit || branch}` + }` + if (shallow) { + await $`git -c advice.detachedHead=false checkout ${ + tag ? `tags/${tag}` : `${commit || branch}` + }` + } else { + await $`git checkout ${branch}` + await $`git merge FETCH_HEAD` + if (tag || commit) { + await $`git reset --hard ${tag || commit}` + } + } +} + +function toCommand( + task: Task | Task[] | void, + agent: Agent, +): ((scripts: any) => Promise) | void { + return async (scripts: any) => { + const tasks = Array.isArray(task) ? task : [task] + for (const task of tasks) { + if (task == null || task === '') { + continue + } else if (typeof task === 'string') { + const scriptOrBin = task.trim().split(/\s+/)[0] + if (scripts?.[scriptOrBin] != null) { + const runTaskWithAgent = getCommand(agent, 'run', [task]) + await $`${runTaskWithAgent}` + } else { + await $`${task}` + } + } else if (typeof task === 'function') { + await task() + } else { + throw new Error( + `invalid task, expected string or function but got ${typeof task}: ${task}`, + ) + } + } + } +} + +export async function runInRepo(options: RunOptions & RepoOptions) { + if (options.verify == null) { + options.verify = true + } + if (options.skipGit == null) { + options.skipGit = false + } + if (options.branch == null) { + options.branch = 'main' + } + + const { + build, + test, + repo, + branch, + tag, + commit, + skipGit, + verify, + beforeInstall, + beforeBuild, + beforeTest, + } = options + + const dir = path.resolve( + options.workspace, + options.dir || repo.substring(repo.lastIndexOf('/') + 1), + ) + + if (!skipGit) { + await setupRepo({ repo, dir, branch, tag, commit }) + } else { + cd(dir) + } + if (options.agent == null) { + const detectedAgent = await detect({ cwd: dir, autoInstall: false }) + if (detectedAgent == null) { + throw new Error(`Failed to detect packagemanager in ${dir}`) + } + options.agent = detectedAgent + } + if (!AGENTS[options.agent]) { + throw new Error( + `Invalid agent ${options.agent}. Allowed values: ${Object.keys( + AGENTS, + ).join(', ')}`, + ) + } + const agent = options.agent + const beforeInstallCommand = toCommand(beforeInstall, agent) + const beforeBuildCommand = toCommand(beforeBuild, agent) + const beforeTestCommand = toCommand(beforeTest, agent) + const buildCommand = toCommand(build, agent) + const testCommand = toCommand(test, agent) + + const pkgFile = path.join(dir, 'package.json') + const pkg = JSON.parse(await fs.promises.readFile(pkgFile, 'utf-8')) + + await beforeInstallCommand?.(pkg.scripts) + + if (verify && test) { + const frozenInstall = getCommand(agent, 'frozen') + await $`${frozenInstall}` + await beforeBuildCommand?.(pkg.scripts) + await buildCommand?.(pkg.scripts) + await beforeTestCommand?.(pkg.scripts) + await testCommand?.(pkg.scripts) + } + let overrides = options.overrides || {} + if (options.release) { + if (overrides.vitest && overrides.vitest !== options.release) { + throw new Error( + `conflicting overrides.vitest=${overrides.vitest} and --release=${options.release} config. Use either one or the other`, + ) + } else { + overrides.vitest = options.release + } + } else { + overrides.vitest ||= `${options.vitestPath}/packages/vitest` + overrides['vite-node'] ||= `${options.vitestPath}/packages/vite-node` + + VITEST_SUB_PACKAGES.forEach((packageName) => { + overrides[ + `@vitest/${packageName}` + ] = `${options.vitestPath}/packages/${packageName}` + }) + + const localOverrides = await buildOverrides(pkg, options, overrides) + cd(dir) // buildOverrides changed dir, change it back + overrides = { + ...overrides, + ...localOverrides, + } + } + await applyPackageOverrides(dir, pkg, overrides) + await beforeBuildCommand?.(pkg.scripts) + await buildCommand?.(pkg.scripts) + if (test) { + await beforeTestCommand?.(pkg.scripts) + await testCommand?.(pkg.scripts) + } + return { dir } +} + +export async function setupVitestRepo(options: Partial) { + const repo = options.repo || 'vitest-dev/vitest' + await setupRepo({ + repo, + dir: vitestPath, + branch: 'main', + shallow: true, + ...options, + }) + + try { + const rootPackageJsonFile = path.join(vitestPath, 'package.json') + const rootPackageJson = JSON.parse( + await fs.promises.readFile(rootPackageJsonFile, 'utf-8'), + ) + const { name } = rootPackageJson + if (name !== '@vitest/monorepo') { + throw new Error( + `expected "name" field of ${repo}/package.json to indicate vitest monorepo, but got ${name}.`, + ) + } + const needsWrite = await overridePackageManagerVersion( + rootPackageJson, + 'pnpm', + ) + if (needsWrite) { + fs.writeFileSync( + rootPackageJsonFile, + JSON.stringify(rootPackageJson, null, 2), + 'utf-8', + ) + if (rootPackageJson.devDependencies?.pnpm) { + await $`pnpm install -Dw pnpm --lockfile-only` + } + } + } catch (e) { + throw new Error(`Failed to setup vitest repo`, { cause: e }) + } +} + +export async function getPermanentRef() { + cd(vitestPath) + try { + const ref = await $`git log -1 --pretty=format:%h` + return ref + } catch (e) { + console.warn(`Failed to obtain perm ref. ${e}`) + return undefined + } +} + +export async function buildVitest({ verify = false }) { + cd(vitestPath) + const frozenInstall = getCommand('pnpm', 'frozen') + const runBuild = getCommand('pnpm', 'run', ['build']) + const runTest = getCommand('pnpm', 'run', ['test:ci']) + await $`${frozenInstall}` + await $`${runBuild}` + if (verify) { + await $`${runTest}` + } +} + +export async function bisectVitest( + good: string, + runSuite: () => Promise, +) { + // sometimes vitest build modifies files in git, e.g. LICENSE.md + // this would stop bisect, so to reset those changes + const resetChanges = async () => $`git reset --hard HEAD` + + try { + cd(vitestPath) + await resetChanges() + await $`git bisect start` + await $`git bisect bad` + await $`git bisect good ${good}` + let bisecting = true + while (bisecting) { + const commitMsg = await $`git log -1 --format=%s` + const isNonCodeCommit = commitMsg.match(/^(?:release|docs)[:(]/) + if (isNonCodeCommit) { + await $`git bisect skip` + continue // see if next commit can be skipped too + } + const error = await runSuite() + cd(vitestPath) + await resetChanges() + const bisectOut = await $`git bisect ${error ? 'bad' : 'good'}` + bisecting = bisectOut.substring(0, 10).toLowerCase() === 'bisecting:' // as long as git prints 'bisecting: ' there are more revisions to test + } + } catch (e) { + console.log('error while bisecting', e) + } finally { + try { + cd(vitestPath) + await $`git bisect reset` + } catch (e) { + console.log('Error while resetting bisect', e) + } + } +} + +function isLocalOverride(v: string): boolean { + if (!v.includes('/') || v.startsWith('@')) { + // not path-like (either a version number or a package name) + return false + } + try { + return !!fs.lstatSync(v)?.isDirectory() + } catch (e) { + if (e.code !== 'ENOENT') { + throw e + } + return false + } +} + +/** + * utility to override packageManager version + * + * @param pkg parsed package.json + * @param pm package manager to override eg. `pnpm` + * @returns {boolean} true if pkg was updated, caller is responsible for writing it to disk + */ +async function overridePackageManagerVersion( + pkg: { [key: string]: any }, + pm: string, +): Promise { + const versionInUse = pkg.packageManager?.startsWith(`${pm}@`) + ? pkg.packageManager.substring(pm.length + 1) + : await $`${pm} --version` + let overrideWithVersion: string | null = null + if (pm === 'pnpm') { + if (semver.eq(versionInUse, '7.18.0')) { + // avoid bug with absolute overrides in pnpm 7.18.0 + overrideWithVersion = '7.18.1' + } + } + if (overrideWithVersion) { + console.warn( + `detected ${pm}@${versionInUse} used in ${pkg.name}, changing pkg.packageManager and pkg.engines.${pm} to enforce use of ${pm}@${overrideWithVersion}`, + ) + // corepack reads this and uses pnpm @ newVersion then + pkg.packageManager = `${pm}@${overrideWithVersion}` + if (!pkg.engines) { + pkg.engines = {} + } + pkg.engines[pm] = overrideWithVersion + + if (pkg.devDependencies?.[pm]) { + // if for some reason the pm is in devDependencies, that would be a local version that'd be preferred over our forced global + // so ensure it here too. + pkg.devDependencies[pm] = overrideWithVersion + } + + return true + } + return false +} + +export async function applyPackageOverrides( + dir: string, + pkg: any, + overrides: Overrides = {}, +) { + const useFileProtocol = (v: string) => + isLocalOverride(v) ? `file:${path.resolve(v)}` : v + // remove boolean flags + overrides = Object.fromEntries( + Object.entries(overrides) + //eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([key, value]) => typeof value === 'string') + .map(([key, value]) => [key, useFileProtocol(value as string)]), + ) + await $`git clean -fdxq` // remove current install + + const agent = await detect({ cwd: dir, autoInstall: false }) + if (!agent) { + throw new Error(`failed to detect packageManager in ${dir}`) + } + // Remove version from agent string: + // yarn@berry => yarn + // pnpm@6, pnpm@7 => pnpm + const pm = agent?.split('@')[0] + + await overridePackageManagerVersion(pkg, pm) + + if (pm === 'pnpm') { + if (!pkg.devDependencies) { + pkg.devDependencies = {} + } + pkg.devDependencies = { + ...pkg.devDependencies, + ...overrides, // overrides must be present in devDependencies or dependencies otherwise they may not work + } + if (!pkg.pnpm) { + pkg.pnpm = {} + } + pkg.pnpm.overrides = { + ...pkg.pnpm.overrides, + ...overrides, + } + } else if (pm === 'yarn') { + pkg.resolutions = { + ...pkg.resolutions, + ...overrides, + } + } else if (pm === 'npm') { + pkg.overrides = { + ...pkg.overrides, + ...overrides, + } + // npm does not allow overriding direct dependencies, force it by updating the blocks themselves + for (const [name, version] of Object.entries(overrides)) { + if (pkg.dependencies?.[name]) { + pkg.dependencies[name] = version + } + if (pkg.devDependencies?.[name]) { + pkg.devDependencies[name] = version + } + } + } else { + throw new Error(`unsupported package manager detected: ${pm}`) + } + const pkgFile = path.join(dir, 'package.json') + await fs.promises.writeFile(pkgFile, JSON.stringify(pkg, null, 2), 'utf-8') + + // use of `ni` command here could cause lockfile violation errors so fall back to native commands that avoid these + if (pm === 'pnpm') { + await $`pnpm install --prefer-frozen-lockfile --prefer-offline --strict-peer-dependencies false` + } else if (pm === 'yarn') { + await $`yarn install` + } else if (pm === 'npm') { + await $`npm install` + } +} + +export function dirnameFrom(url: string) { + return path.dirname(fileURLToPath(url)) +} + +export function parseVitestMajor(vitestPath: string): number { + const content = fs.readFileSync( + path.join(vitestPath, 'packages', 'vitest', 'package.json'), + 'utf-8', + ) + const pkg = JSON.parse(content) + return parseMajorVersion(pkg.version) +} + +export function parseMajorVersion(version: string) { + return parseInt(version.split('.', 1)[0], 10) +} + +async function buildOverrides( + pkg: any, + options: RunOptions, + repoOverrides: Overrides, +) { + const { root } = options + const buildsPath = path.join(root, 'builds') + const buildFiles: string[] = fs + .readdirSync(buildsPath) + .filter((f: string) => !f.startsWith('_') && f.endsWith('.ts')) + .map((f) => path.join(buildsPath, f)) + const buildDefinitions: { + packages: { [key: string]: string } + build: (options: RunOptions) => Promise<{ dir: string }> + dir?: string + }[] = await Promise.all(buildFiles.map((f) => import(pathToFileURL(f).href))) + const deps = new Set([ + ...Object.keys(pkg.dependencies ?? {}), + ...Object.keys(pkg.devDependencies ?? {}), + ...Object.keys(pkg.peerDependencies ?? {}), + ]) + + const needsOverride = (p: string) => + repoOverrides[p] === true || (deps.has(p) && repoOverrides[p] == null) + const buildsToRun = buildDefinitions.filter(({ packages }) => + Object.keys(packages).some(needsOverride), + ) + const overrides: Overrides = {} + for (const buildDef of buildsToRun) { + const { dir } = await buildDef.build({ + root: options.root, + workspace: options.workspace, + vitestPath: options.vitestPath, + vitestMajor: options.vitestMajor, + skipGit: options.skipGit, + release: options.release, + verify: options.verify, + // do not pass along scripts + }) + for (const [name, path] of Object.entries(buildDef.packages)) { + if (needsOverride(name)) { + overrides[name] = `${dir}/${path}` + } + } + } + return overrides +}