mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
* Add BufferTarget.onFinalize option for awaitable async action on finalize The `finalized` event fires synchronously and its return value is ignored, making it unsuitable for use cases where the muxer should wait (e.g. uploading the buffer to S3, R2, or other object stores that require a known Content-Length and therefore can't stream via StreamTarget). Adds a new `BufferTargetOptions` type with an `onFinalize` callback that the muxer awaits before resolving. Matches the existing callback pattern used by `HlsOutputFormatOptions.onSegment`, `onMaster`, etc. When used with PathedTarget, this provides proper backpressure: the next segment won't start being produced until the previous one has finished uploading, keeping memory bounded regardless of video length. - Adds `BufferTargetOptions` type, exported from the package root - `BufferTarget` constructor now accepts optional options (backward compatible) - `_finalize()` awaits `onFinalize` before emitting the `finalized` event - Adds runtime validation for non-function callbacks - Updates "Upload to a server" docs in writing-hls.md with S3 pattern - Documents `onFinalize` in writing-media-files.md BufferTarget section - Adds tests for callback invocation, async awaiting, backward compat, and validation * Add ConcurrentRunner, add OutputOptions.onFinalize, adjust docs accordingly, clean up tests * give me more control baby * forgot to tell about it * polish guide and js docs * vanilagy says remove this, docs will reveal it --------- Co-authored-by: Vanilagy <[email protected]>
219 lines
5.7 KiB
TypeScript
219 lines
5.7 KiB
TypeScript
import ts from 'typescript';
|
|
import * as fs from 'fs';
|
|
|
|
const checkDocblocks = (filePath: string) => {
|
|
const program = ts.createProgram([filePath], {});
|
|
const sourceFile = program.getSourceFile(filePath);
|
|
const checker = program.getTypeChecker();
|
|
|
|
if (!sourceFile) {
|
|
throw new Error(`Could not find source file: ${filePath}`);
|
|
}
|
|
|
|
const missingDocblocks: { name: string; kind: string; line: number; reason: string }[] = [];
|
|
|
|
const checkNode = (node: ts.Node) => {
|
|
if (
|
|
ts.isInterfaceDeclaration(node)
|
|
|| ts.isClassDeclaration(node)
|
|
|| ts.isMethodDeclaration(node)
|
|
|| ts.isGetAccessorDeclaration(node)
|
|
|| ts.isSetAccessorDeclaration(node)
|
|
|| ts.isPropertyDeclaration(node)
|
|
|| ts.isFunctionDeclaration(node)
|
|
|| ts.isTypeAliasDeclaration(node)
|
|
|| ts.isEnumDeclaration(node)
|
|
|| ts.isPropertySignature(node)
|
|
|| ts.isMethodSignature(node)
|
|
|| ts.isVariableStatement(node)
|
|
|| ts.isVariableDeclaration(node)
|
|
|| (ts.isParameter(node) && ts.isPropertyDeclaration(node.parent))
|
|
) {
|
|
let symbol: ts.Symbol | undefined;
|
|
|
|
try {
|
|
if (ts.isVariableStatement(node)) {
|
|
node.declarationList.declarations.forEach((declaration) => {
|
|
const declSymbol = checker.getSymbolAtLocation(declaration.name);
|
|
if (declSymbol) {
|
|
const docStatus = checkDocumentationContent(declSymbol, declaration);
|
|
if (docStatus.hasProblem) {
|
|
const name = declaration.name.getText(sourceFile);
|
|
const line = sourceFile.getLineAndCharacterOfPosition(declaration.getStart()).line + 1;
|
|
missingDocblocks.push({
|
|
name,
|
|
kind: 'variable',
|
|
line,
|
|
reason: docStatus.reason,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
} else if ('name' in node && node.name) {
|
|
symbol = checker.getSymbolAtLocation(node.name);
|
|
}
|
|
} catch {
|
|
symbol = undefined;
|
|
}
|
|
|
|
let name = 'anonymous';
|
|
const kind = ts.SyntaxKind[node.kind].replace(/Declaration|Statement/g, '').toLowerCase();
|
|
|
|
if ('name' in node && node.name) {
|
|
if (ts.isIdentifier(node.name)) {
|
|
name = node.name.text;
|
|
} else if ('getText' in node.name) {
|
|
name = node.name.getText(sourceFile);
|
|
}
|
|
}
|
|
|
|
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
|
|
if (!symbol) {
|
|
const jsDocNodes = ts.getJSDocCommentsAndTags(node);
|
|
if (jsDocNodes.length === 0) {
|
|
missingDocblocks.push({
|
|
name,
|
|
kind,
|
|
line,
|
|
reason: 'No docblock found',
|
|
});
|
|
} else {
|
|
const docStatus = checkJSDocContent(jsDocNodes);
|
|
if (docStatus.hasProblem) {
|
|
missingDocblocks.push({
|
|
name,
|
|
kind,
|
|
line,
|
|
reason: docStatus.reason,
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
const docStatus = checkDocumentationContent(symbol, node);
|
|
if (docStatus.hasProblem) {
|
|
missingDocblocks.push({
|
|
name,
|
|
kind,
|
|
line,
|
|
reason: docStatus.reason,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
ts.forEachChild(node, checkNode);
|
|
};
|
|
|
|
const checkDocumentationContent = (symbol: ts.Symbol, node: ts.Node) => {
|
|
const docComments = symbol.getDocumentationComment(checker);
|
|
if (docComments.length === 0) {
|
|
const jsDocNodes = ts.getJSDocCommentsAndTags(node);
|
|
if (jsDocNodes.length === 0) {
|
|
return { hasProblem: true, reason: 'No docblock found' };
|
|
}
|
|
return checkJSDocContent(jsDocNodes);
|
|
}
|
|
|
|
// Get the raw text of the comment
|
|
const docText = docComments.map(comment => comment.text).join('').trim();
|
|
|
|
// Remove all @tags with regex
|
|
const cleanedText = docText.replace(/@\S+/g, '').trim();
|
|
|
|
// If nothing remains after removing tags, it's just modifiers
|
|
if (cleanedText.length === 0) {
|
|
return { hasProblem: true, reason: 'Docblock contains only modifiers' };
|
|
}
|
|
|
|
return { hasProblem: false, reason: '' };
|
|
};
|
|
|
|
const checkJSDocContent = (jsDocNodes: readonly ts.Node[]) => {
|
|
if (jsDocNodes.length === 0) {
|
|
return { hasProblem: true, reason: 'No docblock found' };
|
|
}
|
|
|
|
for (const node of jsDocNodes) {
|
|
if (ts.isJSDoc(node)) {
|
|
let commentText = '';
|
|
|
|
if (typeof node.comment === 'string') {
|
|
commentText = node.comment;
|
|
} else if (Array.isArray(node.comment)) {
|
|
// Handle JSDoc comment parts (including @link tags)
|
|
commentText = node.comment
|
|
.map((part) => {
|
|
if (typeof part === 'string') {
|
|
return part;
|
|
} else if (part && typeof part === 'object' && 'text' in part) {
|
|
return part.text || '';
|
|
}
|
|
return '';
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
// Remove all @tags with regex
|
|
const cleanedText = commentText.replace(/@\S+/g, '').trim();
|
|
|
|
// If there's content after removing tags, it's a meaningful docblock
|
|
if (cleanedText.length > 0) {
|
|
return { hasProblem: false, reason: '' };
|
|
}
|
|
}
|
|
}
|
|
|
|
return { hasProblem: true, reason: 'Docblock contains only modifiers' };
|
|
};
|
|
|
|
if (sourceFile) {
|
|
checkNode(sourceFile);
|
|
}
|
|
|
|
return {
|
|
success: missingDocblocks.length === 0,
|
|
missingDocblocks,
|
|
};
|
|
};
|
|
|
|
const main = () => {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.length !== 1) {
|
|
console.error('Missing file argument.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const filePath = args[0]!;
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
console.error(`File not found: ${filePath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
const result = checkDocblocks(filePath);
|
|
|
|
if (result.success) {
|
|
console.log(`✅ All symbols in ${filePath} have meaningful docblocks.`);
|
|
} else {
|
|
console.log(
|
|
`❌ Found ${result.missingDocblocks.length} symbols with insufficient docblocks:`,
|
|
);
|
|
|
|
result.missingDocblocks.forEach((item) => {
|
|
console.log(` - ${item.kind} '${item.name}' at ${filePath}:${item.line}: ${item.reason}`);
|
|
});
|
|
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
main();
|