mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
Re-enable lastUpdated, add lastUpdated logic to API doc generator
This commit is contained in:
@@ -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'));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
Reference in New Issue
Block a user