import assert from 'node:assert' import { spawnSync } from 'node:child_process' import { readFileSync, writeFileSync } from 'node:fs' import { findPackageJSON } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' import type { JsonTestResults } from 'vitest/reporters' import { unevals } from '../src/testing/package.ts' const writeOrCheckComparisonTable = () => { const packageStatsByCategory = computePackageStatsByCategory() const comparisonTable = generateComparisonTable(packageStatsByCategory) const readme = readFileSync(readmePath, `utf8`) const newContent = [ ``, comparisonTable, ``, ].join(`\n\n`) const newReadme = readme.replace( /[\s\S]*?/u, newContent, ) if (!process.argv.includes(`--check`)) { writeFileSync(readmePath, newReadme) console.log(`✅ Comparison table updated`) } else if (readme === newReadme) { console.log(`✅ Comparison table is up-to-date`) } else { console.error( `❌ Comparison table is outdated. Run \`pnpm generate-comparison-table\` to update it`, ) const tmpPath = join(tmpdir(), `readme-generated.md`) writeFileSync(tmpPath, newReadme) const { stdout } = spawnSync(`diff`, [`-u`, readmePath, tmpPath], { encoding: `utf8`, }) console.error(stdout) process.exit(1) } } type Stats = { passed: string[] failed: string[] } const computePackageStatsByCategory = (): Map> => { const packageStatsByCategory = new Map>() for (const pkg of packages) { const jsonOutput = JSON.parse( spawnSync(vitestBinPath, [`run`, `--reporter=json`, testPath], { cwd: rootDirectoryPath, env: { ...process.env, UNEVAL_COMPARISON: `true`, UNEVAL_PACKAGE: pkg, }, encoding: `utf8`, }).stdout, ) as JsonTestResults assert(jsonOutput.testResults.length === 1) for (const assertionResult of jsonOutput.testResults[0]!.assertionResults) { if ( assertionResult.ancestorTitles.length === 0 || (assertionResult.status !== `passed` && assertionResult.status !== `failed`) ) { continue } const category = assertionResult.ancestorTitles[0]! let statsByPackage = packageStatsByCategory.get(category) if (!statsByPackage) { statsByPackage = new Map() packageStatsByCategory.set(category, statsByPackage) } let packageStats = statsByPackage.get(pkg) if (!packageStats) { packageStats = { passed: [], failed: [] } statsByPackage.set(pkg, packageStats) } const testName = assertionResult.title.replace( /^uneval '(?.*)'/u, `$`, ) packageStats[assertionResult.status].push(testName) } } return packageStatsByCategory } const generateComparisonTable = ( packageStatsByCategory: Map>, ): string => { const passCount = (pkg: string): number => Array.from( packageStatsByCategory.values(), statsByPackage => statsByPackage.get(pkg)!, ).reduce((count, { passed }) => count + passed.length, 0) const sortedPackages = packages.toSorted( (package1, package2) => passCount(package2) - passCount(package1), ) const columns = sortedPackages.map( pkg => `${ pkg === `@tomer/uneval` ? `${noBreak(pkg)}` : packageLink(pkg) }
${packageBundleSizeBadge(pkg)}`, ) const lineNumbers = computeLineNumbers() const headerCells = [ `Category`, ...columns.map(col => `${col}`), ].join(``) const rows = Array.from( packageStatsByCategory, ([category, statsByPackage]) => { const lineNumber = lineNumbers.get(category) assert(lineNumber, category) const dataCells = sortedPackages .map( pkg => `${statsCell(statsByPackage.get(pkg)!, { lineNumbers })}`, ) .join(``) return `${githubCodeLink({ content: category, lineNumber, })}${dataCells}` }, ) return [``, `${headerCells}`, ...rows, `
`].join(`\n`) } const statsCell = ( { passed, failed }: Stats, { lineNumbers }: { lineNumbers: Map }, ): string => { const total = passed.length + failed.length const summary = `${emoji(passed.length, total)} ${passed.length}/${total}` if (total === 1) { return summary } const testLines = [ ...passed.map(name => { const lineNumber = lineNumbers.get(name) assert(lineNumber, name) return `✅ ${githubCodeLink({ content: name, lineNumber })}` }), ...failed.map(name => { const lineNumber = lineNumbers.get(name) assert(lineNumber, name) return `❌ ${githubCodeLink({ content: name, lineNumber })}` }), ].join(`
`) return `
${summary}${testLines}
` } const githubCodeLink = ({ content, lineNumber, }: { content: string lineNumber: number }): string => `${noBreak( escapeHtml(content), )}` const computeLineNumbers = (): Map => { const testFileLines = readFileSync(testPath, `utf8`).split(`\n`) const lineNumbers = new Map() for (const [index, line] of testFileLines.entries()) { const match = /^ {2}'?(?[\w/ ]+)'?: \[/iu.exec(line) ?? /name: `(?[^`]+)`,/iu.exec(line) if (!match) { continue } const name = match.groups!.name! if (lineNumbers.has(name)) { continue } const lineNumber = index + 1 lineNumbers.set(name, lineNumber) } return lineNumbers } const packageLink = (pkg: string): string => { const version = ( JSON.parse( readFileSync(findPackageJSON(pkg, import.meta.url)!, `utf8`), ) as Record ).version as string const link = `${noBreak( escapeHtml(pkg), )}@${noBreak(escapeHtml(version))}` return `${link}${pkg === `seroval` ? ` (sync)` : ``}` } const packageBundleSizeBadge = (pkg: string): string => `${pkg} gzip size` const escapeHtml = (string: string): string => string .replaceAll(`&`, `&`) .replaceAll(`<`, `<`) .replaceAll(`>`, `>`) const noBreak = (html: string): string => html .replaceAll(`-`, `\u2060-\u2060`) .replaceAll(`/`, `\u2060/\u2060`) .replaceAll(`.`, `\u2060.\u2060`) .replaceAll(` `, `\u00A0`) const emoji = (passed: number, total: number): string => { const percentage = passed / total if (percentage === 0) { return `❌` } if (percentage <= 0.25) { return `🔴` } if (percentage <= 0.5) { return `🟠` } if (percentage <= 0.75) { return `🟡` } if (percentage < 1) { return `🟢` } return `✅` } const packages = Object.keys(unevals) const rootDirectoryPath = join(import.meta.dirname, `..`) const vitestBinPath = join(rootDirectoryPath, `node_modules/.bin/vitest`) const testPath = join(rootDirectoryPath, `src/index.test.ts`) const readmePath = join(rootDirectoryPath, `readme.md`) writeOrCheckComparisonTable()