Files
vercel__workflow/.github/scripts/generate-docs-data.js
Pranay Prakash 8a146a5bb1 docs: revamp World documentation pages (#763)
* docs: revamp World documentation pages with new structure and design

- Add dedicated world detail pages at /worlds/[id] with MDX-driven content
- Create WorldDetailHero, WorldDetailToc, WorldTestingPerformance components
- Add benchmark history charts and test summary links
- Rewrite local-world, postgres-world, and vercel-world MDX with cleaner structure
- Use h3 headings for configuration options instead of tables/accordions
- Add WorldDataProvider context for passing world data to MDX components
- Add example field to worlds-manifest for linking to example repos
- Update worlds index page design

* fix: update worlds page meta title and description

* feat(docs): improve globe backdrop responsiveness and positioning

- Globe now scales with container width up to 1200px max
- Increased opacity from 20% to 30% for better visibility
- Positioned globe 30% down to show north pole behind header
- Globe component now auto-sizes based on container width

* fix(docs): add light mode support for globe backdrop

* feat: improve world page metadata and add dynamic OG images

- Update title format to '{Name} World | Workflow DevKit'
- Update worlds index page title to 'Worlds | Workflow DevKit'
- Improve world descriptions in worlds-manifest.json for better SEO
- Add dynamic OG image generation at /worlds/[id]/og
- Add openGraph and twitter metadata to world detail pages

* fix: use opengraph-image.tsx file convention for dynamic OG images

- Replace route-based og/route.tsx with opengraph-image.tsx file convention
- Remove runtime = 'edge' to allow generateStaticParams for static generation
- Simplify page.tsx metadata (Next.js auto-detects opengraph-image.tsx)

* docs: improve worlds page UI and restructure deploying section

- Fix globe visibility in light/dark mode
- Fix tooltip positioning on mobile breakpoints
- Remove redundant Documentation button for community worlds
- Add missing pages to Foundations index (streaming, serialization, idempotency)
- Restructure deploying section with new index and building-a-world guide
- Restore original MDX content for official worlds (local, vercel, postgres)
- Auto-expand sidebar folders when child page is active

* docs: simplify worlds page footer sections

* docs: move last updated section under worlds grid

* fix(docs): fix broken links and skip typecheck for interface definitions

- Fix /docs/deploying/world link to /docs/deploying/building-a-world
- Fix /docs/deploying#what-are-worlds invalid anchor to /docs/deploying
- Add @skip-typecheck markers to interface definition code blocks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(docs): address PR review feedback

- benchmark-history/route.ts: Add error handling for JSON parsing,
  distinguish 404s from actual errors
- Globe.tsx: Fix hydration mismatch by deferring theme-dependent
  rendering until component is mounted
- sidebar.tsx: Improve path check robustness for active child detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:16:40 -08:00

343 lines
11 KiB
JavaScript

#!/usr/bin/env node
/**
* Generates JSON data files for the docs site from CI results.
* These files are published to GitHub Pages and consumed by the docs.
*
* Usage:
* node generate-docs-data.js --type e2e --results-dir ./e2e-results --output ./docs-data/e2e-results.json
* node generate-docs-data.js --type benchmarks --results-dir ./benchmark-results --output ./docs-data/benchmark-results.json
*/
const fs = require('fs');
const path = require('path');
// Parse command line arguments
const args = process.argv.slice(2);
let type = 'e2e';
let resultsDir = '.';
let outputFile = 'results.json';
let commit = process.env.GITHUB_SHA || null;
let branch = process.env.GITHUB_REF_NAME || 'main';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--type' && args[i + 1]) {
type = args[i + 1];
i++;
} else if (args[i] === '--results-dir' && args[i + 1]) {
resultsDir = args[i + 1];
i++;
} else if (args[i] === '--output' && args[i + 1]) {
outputFile = args[i + 1];
i++;
} else if (args[i] === '--commit' && args[i + 1]) {
commit = args[i + 1];
i++;
} else if (args[i] === '--branch' && args[i + 1]) {
branch = args[i + 1];
i++;
}
}
// World mapping from artifact/file names to world IDs
function extractWorldId(filename, fileType) {
const base = path.basename(filename, '.json');
if (fileType === 'e2e') {
// E2E files: e2e-{category}-{app}.json
// e2e-vercel-prod-nextjs-turbopack.json → vercel
// e2e-local-dev-nextjs-turbopack.json → local
// e2e-local-postgres-nextjs-turbopack.json → postgres
// e2e-community-turso.json → turso
if (base.startsWith('e2e-community-')) {
const rest = base.replace('e2e-community-', '');
// Remove -dev suffix if present (e2e-community-turso-dev.json)
return rest.replace(/-dev$/, '');
}
if (base.startsWith('e2e-vercel-prod-')) return 'vercel';
if (base.startsWith('e2e-local-dev-') || base.startsWith('e2e-local-prod-'))
return 'local';
if (base.startsWith('e2e-local-postgres-')) return 'postgres';
if (base.startsWith('e2e-windows-')) return 'local';
return null;
}
if (fileType === 'benchmarks') {
// Benchmark files: bench-results-{app}-{world}.json
// bench-results-nextjs-turbopack-local.json → local
if (base.startsWith('bench-results-')) {
const parts = base.replace('bench-results-', '').split('-');
return parts[parts.length - 1];
}
// Timing files: bench-timings-{app}-{world}.json
if (base.startsWith('bench-timings-')) {
const parts = base.replace('bench-timings-', '').split('-');
return parts[parts.length - 1];
}
return null;
}
return null;
}
// Find all result files recursively
function findResultFiles(dir, prefix) {
const files = [];
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...findResultFiles(fullPath, prefix));
} else if (
entry.name.startsWith(prefix) &&
entry.name.endsWith('.json')
) {
files.push(fullPath);
}
}
} catch (e) {
// Directory doesn't exist
}
return files;
}
// Parse vitest E2E results
function parseE2EResults(files) {
const worldResults = {};
for (const file of files) {
const worldId = extractWorldId(file, 'e2e');
if (!worldId) continue;
try {
const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
// Initialize world if needed
if (!worldResults[worldId]) {
worldResults[worldId] = {
status: 'pending',
total: 0,
passed: 0,
failed: 0,
skipped: 0,
frameworks: {},
};
}
// Parse vitest JSON format
if (content.testResults) {
for (const testFile of content.testResults) {
for (const assertion of testFile.assertionResults || []) {
worldResults[worldId].total++;
if (assertion.status === 'passed') {
worldResults[worldId].passed++;
} else if (assertion.status === 'failed') {
worldResults[worldId].failed++;
} else {
worldResults[worldId].skipped++;
}
}
}
}
// Extract framework from filename for detailed breakdown
const basename = path.basename(file, '.json');
const frameworkMatch = basename.match(
/-(nextjs-turbopack|nextjs-webpack|nitro|nuxt|sveltekit|vite|hono|express|fastify|astro)(?:-(canary|stable))?$/
);
if (frameworkMatch) {
const framework = frameworkMatch[1];
if (!worldResults[worldId].frameworks[framework]) {
worldResults[worldId].frameworks[framework] = {
total: 0,
passed: 0,
failed: 0,
};
}
if (content.testResults) {
for (const testFile of content.testResults) {
for (const assertion of testFile.assertionResults || []) {
worldResults[worldId].frameworks[framework].total++;
if (assertion.status === 'passed') {
worldResults[worldId].frameworks[framework].passed++;
} else if (assertion.status === 'failed') {
worldResults[worldId].frameworks[framework].failed++;
}
}
}
}
}
} catch (e) {
console.error(`Warning: Could not parse ${file}: ${e.message}`);
}
}
// Calculate status for each world
for (const [worldId, results] of Object.entries(worldResults)) {
if (results.total === 0) {
results.status = 'pending';
} else if (results.failed > 0) {
results.status = results.passed > 0 ? 'partial' : 'failing';
} else {
results.status = 'passing';
}
results.progress =
results.total > 0
? Math.round((results.passed / results.total) * 100)
: 0;
}
return worldResults;
}
// Parse benchmark results
function parseBenchmarkResults(files) {
const worldResults = {};
// Group files by world
const filesByWorld = {};
for (const file of files) {
const worldId = extractWorldId(file, 'benchmarks');
if (!worldId) continue;
if (!filesByWorld[worldId]) {
filesByWorld[worldId] = { results: [], timings: [] };
}
if (path.basename(file).startsWith('bench-results-')) {
filesByWorld[worldId].results.push(file);
} else if (path.basename(file).startsWith('bench-timings-')) {
filesByWorld[worldId].timings.push(file);
}
}
for (const [worldId, worldFiles] of Object.entries(filesByWorld)) {
worldResults[worldId] = {
status: 'pending',
metrics: {},
frameworks: {},
};
// Parse benchmark results
for (const file of worldFiles.results) {
try {
const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
// Extract framework from filename
const basename = path.basename(file, '.json');
const match = basename.match(/bench-results-(.+)-\w+$/);
const framework = match ? match[1] : 'unknown';
if (!worldResults[worldId].frameworks[framework]) {
worldResults[worldId].frameworks[framework] = {};
}
// Parse vitest bench format
for (const fileData of content.files || []) {
for (const group of fileData.groups || []) {
for (const bench of group.benchmarks || []) {
if (bench.mean !== undefined && bench.mean !== null) {
const metric = {
mean: bench.mean,
min: bench.min,
max: bench.max,
samples: bench.sampleCount,
};
worldResults[worldId].metrics[bench.name] = metric;
worldResults[worldId].frameworks[framework][bench.name] =
metric;
}
}
}
}
} catch (e) {
console.error(`Warning: Could not parse ${file}: ${e.message}`);
}
}
// Parse timing data for workflow execution times
for (const file of worldFiles.timings) {
try {
const content = JSON.parse(fs.readFileSync(file, 'utf-8'));
if (content.summary) {
for (const [benchName, timing] of Object.entries(content.summary)) {
if (worldResults[worldId].metrics[benchName]) {
// Store workflow time metrics separately from wall time
if (timing.avgExecutionTimeMs !== undefined) {
worldResults[worldId].metrics[benchName].workflowTime =
timing.avgExecutionTimeMs;
}
if (timing.minExecutionTimeMs !== undefined) {
worldResults[worldId].metrics[benchName].workflowMin =
timing.minExecutionTimeMs;
}
if (timing.maxExecutionTimeMs !== undefined) {
worldResults[worldId].metrics[benchName].workflowMax =
timing.maxExecutionTimeMs;
}
if (timing.avgFirstByteTimeMs !== undefined) {
worldResults[worldId].metrics[benchName].ttfb =
timing.avgFirstByteTimeMs;
}
if (timing.avgSlurpTimeMs !== undefined) {
worldResults[worldId].metrics[benchName].slurp =
timing.avgSlurpTimeMs;
}
}
}
}
} catch (e) {
console.error(`Warning: Could not parse ${file}: ${e.message}`);
}
}
// Set status
worldResults[worldId].status =
Object.keys(worldResults[worldId].metrics).length > 0
? 'measured'
: 'pending';
}
return worldResults;
}
// Main
const prefix = type === 'e2e' ? 'e2e-' : 'bench-';
const resultFiles = findResultFiles(resultsDir, prefix);
if (resultFiles.length === 0) {
console.log(`No ${type} result files found in ${resultsDir}`);
// Write empty results
const emptyOutput = {
lastUpdated: new Date().toISOString(),
commit,
branch,
type,
worlds: {},
};
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
fs.writeFileSync(outputFile, JSON.stringify(emptyOutput, null, 2));
console.log(`Wrote empty results to ${outputFile}`);
process.exit(0);
}
console.log(`Found ${resultFiles.length} ${type} result files`);
const worldResults =
type === 'e2e'
? parseE2EResults(resultFiles)
: parseBenchmarkResults(resultFiles);
const output = {
lastUpdated: new Date().toISOString(),
commit,
branch,
type,
worlds: worldResults,
};
// Ensure output directory exists
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
fs.writeFileSync(outputFile, JSON.stringify(output, null, 2));
console.log(`Wrote ${type} results to ${outputFile}`);
console.log(`Worlds with data: ${Object.keys(worldResults).join(', ')}`);