Re-enable lastUpdated, add lastUpdated logic to API doc generator

This commit is contained in:
Vanilagy
2026-07-22 20:25:37 +02:00
parent 634186fd5e
commit 7ff06c3f65
2 changed files with 55 additions and 4 deletions
+14 -2
View File
@@ -10,6 +10,7 @@ import apiRoutes from '../api/index.json';
import m3u8Grammar from './m3u8-grammar.json' with { type: 'json' }; import m3u8Grammar from './m3u8-grammar.json' with { type: 'json' };
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { execFileSync } from 'node:child_process';
const DESCRIPTION = 'A JavaScript library for reading, writing, and converting media files. Directly in the browser,' const DESCRIPTION = 'A JavaScript library for reading, writing, and converting media files. Directly in the browser,'
+ ' and faster than anybunny else.'; + ' and faster than anybunny else.';
@@ -27,8 +28,14 @@ export default withMermaid({
for (const entry of entries) { for (const entry of entries) {
const isDirectory = await fs.stat(path.join('./examples', entry)).then(stat => stat.isDirectory()); const isDirectory = await fs.stat(path.join('./examples', entry)).then(stat => stat.isDirectory());
if (isDirectory) { if (isDirectory) {
const gitDate = execFileSync(
'git',
['log', '-1', '--format=%cI', '--', path.join('./examples', entry)],
).toString().trim();
items.push({ items.push({
url: `/examples/${entry}/`, // With trailing slash url: `/examples/${entry}/`, // With trailing slash
lastmod: new Date(gitDate).toISOString(),
}); });
} }
} }
@@ -36,7 +43,7 @@ export default withMermaid({
return items; return items;
}, },
}, },
// lastUpdated: true, lastUpdated: true,
head: [ head: [
['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }], ['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }],
['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }], ['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }],
@@ -201,7 +208,7 @@ export default withMermaid({
tailwindcss() as any, tailwindcss() as any,
llmstxt({ llmstxt({
ignoreFiles: [ ignoreFiles: [
'api/*', 'api/!(index).md',
'examples.md', 'examples.md',
'llms.md', 'llms.md',
], ],
@@ -305,5 +312,10 @@ export default withMermaid({
for (const file of files) { for (const file of files) {
await fs.copyFile('./docs/api/' + file, './dist-docs/api/' + file); await fs.copyFile('./docs/api/' + file, './dist-docs/api/' + file);
} }
// The llms.txt generation leaves behind runs of empty lines, collapse them into one
const llmsTxtPath = './dist-docs/llms.txt';
const llmsTxt = await fs.readFile(llmsTxtPath, 'utf-8');
await fs.writeFile(llmsTxtPath, llmsTxt.replace(/\n{3,}/g, '\n\n'));
}, },
}); });
+41 -2
View File
@@ -15,6 +15,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { execFileSync } from 'child_process';
const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) => { const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false) => {
const program = ts.createProgram(entryFiles, { const program = ts.createProgram(entryFiles, {
@@ -629,6 +630,7 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
// Data structures for "Used by" feature // Data structures for "Used by" feature
const usedByReferences = new Map<string, Set<{ user: string; context: string; type: 'constructor' | 'method' | 'property' | 'extends' | 'type_param' | 'type_alias' | 'variable' | 'function' }>>(); const usedByReferences = new Map<string, Set<{ user: string; context: string; type: 'constructor' | 'method' | 'property' | 'extends' | 'type_param' | 'type_alias' | 'variable' | 'function' }>>();
const generatedDocs = new Map<string, string>(); const generatedDocs = new Map<string, string>();
const symbolSourceFiles = new Map<string, string>();
const addUsage = ( const addUsage = (
used: string, used: string,
@@ -678,6 +680,9 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
} }
} }
// Remember where the symbol is actually defined (reexports don't count), for lastModified dates
symbolSourceFiles.set(symbolName, targetDeclaration.getSourceFile().fileName);
const groupTag = ts.getJSDocTags(targetDeclaration).find(tag => tag.tagName.text === 'group'); const groupTag = ts.getJSDocTags(targetDeclaration).find(tag => tag.tagName.text === 'group');
if (!groupTag || typeof groupTag.comment !== 'string') { if (!groupTag || typeof groupTag.comment !== 'string') {
throw new Error(`Symbol '${symbolName}' is missing @group JSDoc tag`); throw new Error(`Symbol '${symbolName}' is missing @group JSDoc tag`);
@@ -1750,6 +1755,8 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
}); });
// Phase 3: Assemble final docs with "Used by" sections and write files // Phase 3: Assemble final docs with "Used by" sections and write files
let newestLastModified: string | null = null;
generatedDocs.forEach((markdown, symbolName) => { generatedDocs.forEach((markdown, symbolName) => {
const usages = usedByReferences.get(symbolName); const usages = usedByReferences.get(symbolName);
let usedByMarkdown = ''; let usedByMarkdown = '';
@@ -1821,8 +1828,16 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
} }
} }
const finalMarkdown = markdown.replace('<!-- USED_BY_SECTION -->', usedByMarkdown);
if (!dry) { if (!dry) {
const lastModified = getLastModifiedDate(symbolSourceFiles.get(symbolName)!);
if (!newestLastModified || new Date(lastModified) > new Date(newestLastModified)) {
newestLastModified = lastModified;
}
const finalMarkdown = addLastUpdatedToFrontmatter(
markdown.replace('<!-- USED_BY_SECTION -->', usedByMarkdown),
lastModified,
);
const outputPath = path.join(outputDir, `${symbolName}.md`); const outputPath = path.join(outputDir, `${symbolName}.md`);
fs.writeFileSync(outputPath, finalMarkdown); fs.writeFileSync(outputPath, finalMarkdown);
console.log(`Generated: ${outputPath}`); console.log(`Generated: ${outputPath}`);
@@ -1874,7 +1889,7 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
if (!dry) { if (!dry) {
const indexPath = path.join(outputDir, 'index.md'); const indexPath = path.join(outputDir, 'index.md');
fs.writeFileSync(indexPath, indexMarkdown); fs.writeFileSync(indexPath, addLastUpdatedToFrontmatter(indexMarkdown, newestLastModified!));
console.log(`Generated: ${indexPath}`); console.log(`Generated: ${indexPath}`);
} }
@@ -1901,6 +1916,30 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
} }
}; };
// Last-commit date of a source file, so generated pages can carry an honest lastModified date.
// Falls back to the filesystem mtime for files git doesn't know about yet.
const lastModifiedCache = new Map<string, string>();
const getLastModifiedDate = (filePath: string) => {
let date = lastModifiedCache.get(filePath);
if (date === undefined) {
const gitDate = execFileSync('git', ['log', '-1', '--format=%cI', '--', filePath]).toString().trim();
date = gitDate
? new Date(gitDate).toISOString()
: fs.statSync(filePath).mtime.toISOString();
lastModifiedCache.set(filePath, date);
}
return date;
};
// Unquoted ISO value so YAML parses it as a date -- VitePress only honors the lastUpdated
// frontmatter key (for both the page footer and the sitemap) when it's an actual Date
const addLastUpdatedToFrontmatter = (markdown: string, lastModified: string) => {
if (markdown.startsWith('---\n')) {
return markdown.replace('---\n', `---\nlastUpdated: ${lastModified}\n`);
}
return `---\nlastUpdated: ${lastModified}\n---\n\n${markdown}`;
};
// Shared helper for extracting a JSDoc description, handling inline tags via raw-source fallback. // Shared helper for extracting a JSDoc description, handling inline tags via raw-source fallback.
// - `tagHandling: 'stopAtFirst'` matches the behavior used by property-level descriptions: if any // - `tagHandling: 'stopAtFirst'` matches the behavior used by property-level descriptions: if any
// @-tag line is encountered, all subsequent lines are dropped (including any trailing description). // @-tag line is encountered, all subsequent lines are dropped (including any trailing description).