Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Custom Metrics Documentation
run-name: Documentation Check & Sync (${{ github.ref_name }})

on:
push:
branches:
- main
paths:
- 'dist/**'
- 'bin/**'
- '.github/workflows/docs.yml'
pull_request:
branches:
- main
paths:
- 'dist/**'
- 'bin/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

jobs:
validate:
name: Validate JSDoc Parity
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'

- name: Install Dependencies
run: npm ci

- name: Validate JSDoc vs Code
run: npm run validate:docs

sync:
name: Sync Documentation to har.fyi
needs: validate
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
steps:
- name: Checkout custom-metrics
uses: actions/checkout@v4
with:
path: custom-metrics

- name: Checkout har.fyi
uses: actions/checkout@v4
with:
repository: HTTPArchive/har.fyi
token: ${{ secrets.DOCS_SYNC_PAT || secrets.GITHUB_TOKEN }}
path: har.fyi

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: custom-metrics/package-lock.json

- name: Install Dependencies
run: |
cd custom-metrics
npm ci

- name: Generate MDX Documentation
run: |
cd custom-metrics
node bin/generate-docs.js --out ../har.fyi/src/content/docs/reference/custom-metrics

- name: Create Pull Request in har.fyi
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.DOCS_SYNC_PAT || secrets.GITHUB_TOKEN }}
path: har.fyi
branch: sync-custom-metrics
base: main
delete-branch: true
title: "docs: sync custom metrics reference from custom-metrics"
body: |
Automated documentation sync from [custom-metrics commit ${{ github.sha }}][commit].

Generated by `HTTPArchive/custom-metrics` workflow.

[commit]: https://github.com/HTTPArchive/custom-metrics/commit/${{ github.sha }}
commit-message: "docs: sync custom metrics from custom-metrics@${{ github.sha }}"


20 changes: 20 additions & 0 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,23 @@ jobs:
VALIDATE_EDITORCONFIG: true
VALIDATE_MARKDOWN: true
VALIDATE_YAML: true

validate-docs:
name: Validate JSDoc Parity
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"

- name: Install Dependencies
run: npm ci

- name: Validate JSDoc vs Code
run: npm run validate:docs

128 changes: 128 additions & 0 deletions bin/generate-docs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');
const {
TOP_LEVEL_METRICS,
normalizeType,
extractCustomTypeName,
cleanDescription
} = require('./lib/types.js');
const {
getPrimaryTypedef
} = require('./lib/jsdoc-parser.js');
const {
getAnnotatedMetricFiles
} = require('./lib/file-utils.js');
const { validateMetric } = require('./validate-docs.js');

/**
* Recursively renders schema properties down to basic types in markdown format.
*/
function renderProperties(properties, prefix = '', headingLevel = 3, typedefs = new Map(), visited = new Set()) {
let mdx = '';
const hashes = '#'.repeat(headingLevel);

for (const prop of properties) {
const rawType = prop.type || 'unknown';
const customTypeName = extractCustomTypeName(rawType, typedefs);
const isArray = rawType.includes('[]') || /Array</i.test(rawType);
const normalizedType = normalizeType(rawType);

const displayType = customTypeName
? (isArray ? 'array<object>' : 'object')
: normalizedType;

const fullPath = prefix ? `${prefix}.${prop.name}` : prop.name;

mdx += `${hashes} \`${fullPath}\`\n\n`;
mdx += `Type: \`${displayType}\`\n\n`;
mdx += `${cleanDescription(prop.description)}\n\n`;

if (customTypeName && typedefs.has(customTypeName) && !visited.has(customTypeName)) {
const nestedTypedef = typedefs.get(customTypeName);
const nextPrefix = isArray ? `${fullPath}[i]` : fullPath;
const nextVisited = new Set(visited).add(customTypeName);
mdx += renderProperties(nestedTypedef.properties, nextPrefix, headingLevel + 1, typedefs, nextVisited);
}
}

return mdx;
}

/**
* Generates Starlight-compliant MDX content from parsed JSDoc typedefs.
*/
function generateMDX(metricName, typedefs) {
const primaryTypedef = getPrimaryTypedef(metricName, typedefs);
const capitalizedName = metricName.charAt(0).toUpperCase() + metricName.slice(1);
const isTopLevel = TOP_LEVEL_METRICS.has(metricName);

const parentLink = isTopLevel
? `_Appears in: [\`custom_metrics\`](/reference/structs/custom-metrics/) struct_\\\n_As: [\`${metricName}\`](/reference/structs/custom-metrics/#${metricName})_`
: `_Appears in: [\`custom_metrics.other\`](/reference/custom-metrics/other/) struct_\\\n_As: [\`${metricName}\`](/reference/custom-metrics/other/#${metricName})_`;

let mdx = `---
title: ${capitalizedName} custom metric
description: Reference docs for the ${metricName} custom metric
---

${parentLink}

## Schema

`;

mdx += renderProperties(primaryTypedef.properties, '', 3, typedefs);

return mdx;
}

// CLI execution
if (require.main === module) {
const args = process.argv.slice(2);
let outDir = path.join(__dirname, '../../har.fyi/src/content/docs/reference/custom-metrics');
let explicitFiles = [];

for (let i = 0; i < args.length; i++) {
if (args[i] === '--out' && args[i + 1]) {
outDir = path.resolve(args[i + 1]);
i++;
} else if (!args[i].startsWith('--')) {
explicitFiles.push(path.resolve(args[i]));
}
}

const targetFiles = explicitFiles.length > 0 ? explicitFiles : getAnnotatedMetricFiles();

fs.mkdirSync(outDir, { recursive: true });

for (const file of targetFiles) {
const metricName = path.basename(file, '.js');
console.log(`Generating docs for ${metricName}...`);

const validation = validateMetric(file);
if (!validation.valid) {
console.error(`❌ Validation failed for ${file}. Docs generation aborted:`);
for (const err of validation.errors) {
console.error(` - ${err}`);
}
process.exit(1);
}

const isTopLevel = TOP_LEVEL_METRICS.has(metricName);
const targetDir = isTopLevel ? outDir : path.join(outDir, 'other');
fs.mkdirSync(targetDir, { recursive: true });

const mdxContent = generateMDX(metricName, validation.typedefs);
const outFile = path.join(targetDir, `${metricName}.mdx`);
fs.writeFileSync(outFile, mdxContent, 'utf8');
console.log(`✅ Generated ${outFile}\n`);
}
}

module.exports = {
generateMDX,
renderProperties
};
Loading
Loading