From a2a30c3e920d107aca3dcfaa7bfb511dd2179802 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:11:43 +0200 Subject: [PATCH 1/4] Add API doc generator, improve documentation across the board --- .gitignore | 1 + README.md | 1 + docs/.vitepress/config.mts | 97 +- docs/api-config.json | 22 + docs/index.md | 3 + package-lock.json | 15 + package.json | 3 +- packages/mp3-encoder/src/index.ts | 1 + scripts/check-docblocks.ts | 28 +- scripts/generate-api-docs.ts | 1445 +++++++++++++++++++++++++++++ src/codec.ts | 113 +-- src/conversion.ts | 82 +- src/custom-coder.ts | 55 +- src/encode.ts | 222 ++++- src/index.ts | 149 +-- src/input-format.ts | 43 + src/input-track.ts | 30 +- src/input.ts | 24 +- src/media-sink.ts | 31 +- src/media-source.ts | 64 +- src/misc.ts | 4 + src/output-format.ts | 50 +- src/output.ts | 12 + src/packet.ts | 9 +- src/sample.ts | 67 +- src/source.ts | 60 +- src/target.ts | 25 +- tsdoc.json | 13 + 28 files changed, 2281 insertions(+), 388 deletions(-) create mode 100644 docs/api-config.json create mode 100644 scripts/generate-api-docs.ts create mode 100644 tsdoc.json diff --git a/.gitignore b/.gitignore index 3c737af..355e7e1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ node_modules /dist-docs .DS_Store /docs/.vitepress/cache +/docs/api packages/mp3-encoder/dist \ No newline at end of file diff --git a/README.md b/README.md index 5218bec..53a5c3b 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,7 @@ npm run build # Production build with type definitions npm run check # Type checking npm run lint # ESLint +npm run docs:generate # Generates API docs npm run docs:dev # Start docs development server npm run dev # Start examples development server diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index bcb0330..723737b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -3,6 +3,8 @@ import footnote from 'markdown-it-footnote'; import tailwindcss from '@tailwindcss/vite'; import llmstxt from 'vitepress-plugin-llms'; import { HeadConfig } from 'vitepress'; +// @ts-expect-error This file gets generated once docs:generate is run +import apiRoutes from '../api/index.json'; const DESCRIPTION = 'A JavaScript library for reading, writing, and converting media files. Directly in the browser,' + ' and faster than anybunny else.'; @@ -32,56 +34,61 @@ export default withMermaid({ // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Guide', link: '/guide/introduction', activeMatch: '/guide' }, + { text: 'API', link: '/api', activeMatch: '/api' }, { text: 'Examples', link: '/examples', activeMatch: '/examples' }, { text: 'Sponsors', link: '/#sponsors', activeMatch: '/#sponsors' }, { text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license' }, ], - sidebar: [ - { - text: 'Getting started', - items: [ - { text: 'Introduction', link: '/guide/introduction' }, - { text: 'Installation', link: '/guide/installation' }, - { text: 'Quick start', link: '/guide/quick-start' }, - ], - }, - { - text: 'Reading', - items: [ - { text: 'Reading media files', link: '/guide/reading-media-files' }, - { text: 'Media sinks', link: '/guide/media-sinks' }, - { text: 'Input formats', link: '/guide/input-formats' }, - ], - }, - { - text: 'Writing', - items: [ - { text: 'Writing media files', link: '/guide/writing-media-files' }, - { text: 'Media sources', link: '/guide/media-sources' }, - { text: 'Output formats', link: '/guide/output-formats' }, - ], - }, - { - text: 'Conversion', - items: [ - { text: 'Converting media files', link: '/guide/converting-media-files' }, - ], - }, - { - text: 'Miscellaneous', - items: [ - { text: 'Packets & samples', link: '/guide/packets-and-samples' }, - { text: 'Supported formats & codecs', link: '/guide/supported-formats-and-codecs' }, - ], - }, - { - text: 'Extensions', - items: [ - { text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' }, - ], - }, - ], + sidebar: { + '/guide': [ + { + text: 'Getting started', + items: [ + { text: 'Introduction', link: '/guide/introduction' }, + { text: 'Installation', link: '/guide/installation' }, + { text: 'Quick start', link: '/guide/quick-start' }, + ], + }, + { + text: 'Reading', + items: [ + { text: 'Reading media files', link: '/guide/reading-media-files' }, + { text: 'Media sinks', link: '/guide/media-sinks' }, + { text: 'Input formats', link: '/guide/input-formats' }, + ], + }, + { + text: 'Writing', + items: [ + { text: 'Writing media files', link: '/guide/writing-media-files' }, + { text: 'Media sources', link: '/guide/media-sources' }, + { text: 'Output formats', link: '/guide/output-formats' }, + ], + }, + { + text: 'Conversion', + items: [ + { text: 'Converting media files', link: '/guide/converting-media-files' }, + ], + }, + { + text: 'Miscellaneous', + items: [ + { text: 'Packets & samples', link: '/guide/packets-and-samples' }, + { text: 'Supported formats & codecs', link: '/guide/supported-formats-and-codecs' }, + ], + }, + { + text: 'Extensions', + items: [ + { text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' }, + ], + }, + ], + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + '/api': apiRoutes, + }, socialLinks: [ { icon: 'github', link: 'https://github.com/Vanilagy/mediabunny' }, diff --git a/docs/api-config.json b/docs/api-config.json new file mode 100644 index 0000000..e160b55 --- /dev/null +++ b/docs/api-config.json @@ -0,0 +1,22 @@ +{ + "heading": "Mediabunny API reference", + "intro": "Here you can find detailed documentation for all classes, functions, constants and types exposed by Mediabunny's public API.", + + "Samples": "Raw, unencoded chunks of media data, such as video frames or sections of audio.", + "Packets": "Chunks of encoded media data.", + "Input files & tracks": "Read input files and their tracks; demuxer API.", + "Input formats": "Container formats that Mediabunny can read.", + "Input sources": "The sources that can provide data to an `Input`.", + "Output files": "Create and write new media files; muxer API.", + "Output formats": "Container formats that Mediabunny can write.", + "Output targets": "The targets where `Output` writes data to.", + "Media sinks": "Methods for extracting media data from input files.", + "Media sources": "Methods for adding media data to output files.", + "Conversion": "A simple API for converting and transforming media files.", + "Codecs": "Codecs understood by Mediabunny.", + "Encoding": "Encoder configuration and encodability checks.", + "Custom coders": "API for adding custom encoders and decoders.", + "Miscellaneous": "Whatever's left.", + + "@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny." +} diff --git a/docs/index.md b/docs/index.md index f35a333..050d8dd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,6 +14,9 @@ hero: - theme: brand text: Hop in link: /guide/introduction + - theme: alt + text: API + link: /api - theme: alt text: Examples link: /examples diff --git a/package-lock.json b/package-lock.json index 3bec6d7..74440a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9424,6 +9424,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 1d6512f..2514ca1 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,9 @@ "check": "tsc -p src --noEmit && tsc -p packages/mp3-encoder/src --noEmit && tsc -p scripts --noEmit && tsc -p tsconfig.vite.json --noEmit && rm tsconfig.vite.tsbuildinfo", "check-docblocks": "tsx scripts/check-docblocks.ts dist/mediabunny.d.ts", "docs:dev": "vitepress dev docs", - "docs:build": "vitepress build docs && npm run examples:build", + "docs:build": "npm run docs:generate && vitepress build docs && npm run examples:build", "docs:preview": "vitepress preview docs", + "docs:generate": "tsx scripts/generate-api-docs.ts src/index.ts packages/mp3-encoder/src/index.ts docs/api-config.json", "dev": "vite", "examples:build": "vite build", "fix-build-import-paths": "tsx scripts/add-import-extensions.ts", diff --git a/packages/mp3-encoder/src/index.ts b/packages/mp3-encoder/src/index.ts index 33e4a38..7319e2d 100644 --- a/packages/mp3-encoder/src/index.ts +++ b/packages/mp3-encoder/src/index.ts @@ -200,6 +200,7 @@ class Mp3Encoder extends CustomAudioEncoder { * } * ``` * + * @group \@mediabunny/mp3-encoder * @public */ export const registerMp3Encoder = () => { diff --git a/scripts/check-docblocks.ts b/scripts/check-docblocks.ts index 493d121..5589a9c 100644 --- a/scripts/check-docblocks.ts +++ b/scripts/check-docblocks.ts @@ -16,6 +16,7 @@ const checkDocblocks = (filePath: string) => { if ( ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node) + || ts.isConstructorDeclaration(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) @@ -60,7 +61,13 @@ const checkDocblocks = (filePath: string) => { let name = 'anonymous'; const kind = ts.SyntaxKind[node.kind].replace(/Declaration|Statement/g, '').toLowerCase(); - if ('name' in node && node.name) { + if (ts.isConstructorDeclaration(node)) { + // For constructors, use the parent class name + const parent = node.parent; + if (ts.isClassDeclaration(parent) && parent.name) { + name = parent.name.text; + } + } else if ('name' in node && node.name) { if (ts.isIdentifier(node.name)) { name = node.name.text; } else if ('getText' in node.name) { @@ -137,9 +144,22 @@ const checkDocblocks = (filePath: string) => { for (const node of jsDocNodes) { if (ts.isJSDoc(node)) { - const commentText = node.comment ?? ''; - if (typeof commentText !== 'string') { - throw new Error('Can\'t handle this yet!'); + 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 diff --git a/scripts/generate-api-docs.ts b/scripts/generate-api-docs.ts new file mode 100644 index 0000000..27af5a2 --- /dev/null +++ b/scripts/generate-api-docs.ts @@ -0,0 +1,1445 @@ +// This script has been 100% vibe-coded with Claude, meaning I literally haven't looked at any of the code. It's +// probably a mess, but it solves a one-off problem where only the output matters, and the output is indeed good, which +// is the point of a custom script for this: full, precise control. + +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @stylistic/max-len */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @stylistic/brace-style */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import * as ts from 'typescript'; +import * as fs from 'fs'; +import * as path from 'path'; + +const generateDocs = (entryFiles: string[], apiConfigFile: string) => { + const program = ts.createProgram(entryFiles, { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Node10, + allowJs: false, + declaration: true, + esModuleInterop: true, + skipLibCheck: true, + strict: true, + }); + + const sourceFiles = entryFiles.map((entryFile) => { + const sourceFile = program.getSourceFile(entryFile); + if (!sourceFile) { + throw new Error(`Could not find source file: ${entryFile}`); + } + return sourceFile; + }); + + const typeChecker = program.getTypeChecker(); + const outputDir = path.resolve(process.cwd(), 'docs/api'); + + // Load API config + const apiConfigPath = path.resolve(process.cwd(), apiConfigFile); + if (!fs.existsSync(apiConfigPath)) { + throw new Error(`API config file not found: ${apiConfigPath}`); + } + const apiConfig: Record = JSON.parse(fs.readFileSync(apiConfigPath, 'utf-8')); + + // Extract special fields + const headingText = apiConfig['heading'] || 'API Reference'; + const introText = apiConfig['intro']; + + // Create a copy without the special fields for group processing + const groupConfig = { ...apiConfig }; + delete groupConfig['heading']; + delete groupConfig['intro']; + + // Clear and recreate output directory + if (fs.existsSync(outputDir)) { + fs.rmSync(outputDir, { recursive: true }); + } + fs.mkdirSync(outputDir, { recursive: true }); + + // Collect all exported types for cross-referencing + const exportedTypes = new Set(); + const classHierarchy = new Map(); // Maps parent class to array of subclasses + const classInstances = new Map(); // Maps class name to array of instance variable names + + const collectExportedTypes = (module: ts.Symbol, visited = new Set()): void => { + if (visited.has(module)) return; + visited.add(module); + + const exports = typeChecker.getExportsOfModule(module); + exports.forEach((exportSymbol) => { + const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0]; + if (!declaration) return; + + // Collect classes, interfaces, types, enums, variables (only if @public) + if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration) + || ts.isTypeAliasDeclaration(declaration) || ts.isEnumDeclaration(declaration) + || ts.isVariableDeclaration(declaration)) { + const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public'); + if (hasPublicTag) { + exportedTypes.add(exportSymbol.getName()); + } + } + + // Follow reexports + else if (exportSymbol.flags & ts.SymbolFlags.Alias) { + const aliasedSymbol = typeChecker.getAliasedSymbol(exportSymbol); + const aliasedDeclaration = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]; + if (aliasedDeclaration) { + // Check if the aliased symbol has @public tag + const hasPublicTag = ts.getJSDocTags(aliasedDeclaration).some(tag => tag.tagName.text === 'public'); + if (hasPublicTag) { + exportedTypes.add(exportSymbol.getName()); + } + + // Also recursively collect from the source module + const sourceFile = aliasedDeclaration.getSourceFile(); + const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile); + if (moduleSymbol) { + collectExportedTypes(moduleSymbol, visited); + } + } + } + }); + }; + + // Get all exported symbols recursively + const getAllExportedSymbols = (module: ts.Symbol, visited = new Set()): ts.Symbol[] => { + if (visited.has(module)) return []; + visited.add(module); + + const exports = typeChecker.getExportsOfModule(module); + const symbols: ts.Symbol[] = []; + + exports.forEach((exportSymbol) => { + const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0]; + if (!declaration) return; + + // If it's a reexport, follow it recursively + if (exportSymbol.flags & ts.SymbolFlags.Alias) { + const aliasedSymbol = typeChecker.getAliasedSymbol(exportSymbol); + const aliasedDeclaration = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]; + if (aliasedDeclaration) { + const sourceFile = aliasedDeclaration.getSourceFile(); + const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile); + if (moduleSymbol) { + symbols.push(...getAllExportedSymbols(moduleSymbol, visited)); + } + } + } + // Otherwise, add any symbol with @public tag (we'll filter by type later) + else { + const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public'); + if (hasPublicTag) { + symbols.push(exportSymbol); + } + } + }); + + return symbols; + }; + + // Collect all exported types from all source files + const allModuleSymbols: ts.Symbol[] = []; + sourceFiles.forEach((sourceFile) => { + const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile); + if (moduleSymbol) { + allModuleSymbols.push(moduleSymbol); + collectExportedTypes(moduleSymbol); + } + }); + + // Helper to find all potential type references in a type string + const findAllTypeReferences = (typeString: string): string[] => { + // Match PascalCase identifiers that could be type names + const matches = typeString.match(/\b[A-Z][a-zA-Z0-9_]*\b/g) || []; + return [...new Set(matches)]; // Remove duplicates + }; + + // Helper to filter references to only exported types (excluding current type) + const filterToExportedTypes = (references: string[], currentTypeName?: string): string[] => { + return references.filter(ref => exportedTypes.has(ref) && ref !== currentTypeName); + }; + + // Helper to process {@link} tags in JSDoc comments + const processLinkTags = (text: string, currentTypeName?: string): string => { + // Replace {@link TypeName} with [TypeName](./TypeName.md) if TypeName is exported + // or just TypeName if not exported + // If TypeName is the current type, just use code formatting without link + return text.replace(/\{@link\s+([^}]+)\}/g, (_, typeName) => { + const cleanTypeName = typeName.trim(); + if (cleanTypeName === currentTypeName) { + return `\`${cleanTypeName}\``; + } + if (exportedTypes.has(cleanTypeName)) { + return `[\`${cleanTypeName}\`](./${cleanTypeName}.md)`; + } + return `\`${cleanTypeName}\``; + }); + }; + + // Helper to extract linked types from {@link} tags in text + const extractLinkedTypes = (text: string): string[] => { + if (!text) return []; + const linkMatches = text.match(/\{@link\s+([^}]+)\}/g) || []; + return linkMatches.map((match) => { + const typeName = match.replace(/\{@link\s+([^}]+)\}/, '$1').trim(); + return typeName; + }); + }; + + // Helper to format references with proper "and" and period + // Optionally filters out references that were already mentioned in @link tags + const formatReferences = (references: string[], linkedTypes: string[] = []): string => { + if (references.length === 0) return ''; + + // Filter out references that were already linked in the description + const filteredReferences = references.filter(ref => !linkedTypes.includes(ref)); + + if (filteredReferences.length === 0) return ''; + + const refLinks = filteredReferences.map(ref => `[\`${ref}\`](./${ref}.md)`); + const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }); + return `\n\nSee ${formatter.format(refLinks)}.`; + }; + + // Helper to format object types with proper indentation + const formatObjectType = (typeText: string): string => { + // Remove JSDoc comments but preserve original structure + let lines = typeText.split('\n'); + const result: string[] = []; + + // First remove all JSDoc comments from the entire text + const cleanText = typeText.replace(/\/\*\*[\s\S]*?\*\//g, ''); + lines = cleanText.split('\n'); + + for (const line of lines) { + // Skip empty lines + if (line.trim() === '') continue; + + result.push(line); + } + + return result.join('\n'); + }; + + // Helper to get better type string representation + const getTypeString = (type: ts.Type): string => { + // Handle array types specially + if (typeChecker.isArrayType(type)) { + const elementType = typeChecker.getTypeArguments(type as ts.TypeReference)[0]; + if (elementType) { + return `${getTypeString(elementType)}[]`; + } + } + + // Check if it's an array-like type by checking the symbol name + const typeString = typeChecker.typeToString(type); + if (typeString === 'Array' && type.symbol && type.symbol.getName() === 'Array') { + // Try to get type arguments from the type reference + if ((type as any).typeArguments && (type as any).typeArguments.length > 0) { + const elementType = (type as any).typeArguments[0]; + return `${getTypeString(elementType)}[]`; + } + // If we can't determine the element type, try looking at the declaration + if (type.symbol.declarations && type.symbol.declarations[0]) { + const declaration = type.symbol.declarations[0]; + if ( + ts.isTypeReferenceNode(declaration) + && declaration.typeArguments + && declaration.typeArguments.length > 0 + ) { + return `${declaration.typeArguments[0]!.getText()}[]`; + } + } + } + + return typeString; + }; + + // Helper to clean up optional parameter types + const cleanOptionalType = (type: string, isOptional: boolean) => { + let cleanedType = type; + if (isOptional) { + // Remove "| undefined" from union types for optional parameters + cleanedType = cleanedType.replace(/\s*\|\s*undefined$/, '').replace(/^undefined\s*\|\s*/, ''); + } + // Convert string literals from double quotes to single quotes + cleanedType = cleanedType.replace(/"([^"]*)"/g, '\'$1\''); + + // Format long union types with line breaks if they exceed 80 characters + // Only apply to top-level unions, not unions nested within intersections, object types, or generic types + if (cleanedType.includes(' | ') && cleanedType.length > 80 && !cleanedType.includes('&') && !cleanedType.includes('{') && !cleanedType.includes('<')) { + const unionMembers = cleanedType.split(' | '); + cleanedType = '\n\t| ' + unionMembers.join('\n\t| '); + } + + return cleanedType; + }; + + // Get all exported symbols from all modules + const allSymbols: ts.Symbol[] = []; + allModuleSymbols.forEach((moduleSymbol) => { + allSymbols.push(...getAllExportedSymbols(moduleSymbol)); + }); + const indexEntries: Array<{ name: string; type: string; group: string; order: number }> = []; + + // Create a map to track the order of symbols based on their appearance in the entry file + const symbolOrderMap = new Map(); + let orderIndex = 0; + + // Walk through all source files to establish order based on declaration/export order + const establishSymbolOrder = (node: ts.Node): void => { + if (ts.isExportDeclaration(node)) { + // Handle export declarations like "export { Foo } from './foo'" + if (node.exportClause && ts.isNamedExports(node.exportClause)) { + node.exportClause.elements.forEach((element) => { + const exportName = (element.propertyName || element.name).getText(); + if (!symbolOrderMap.has(exportName)) { + symbolOrderMap.set(exportName, orderIndex++); + } + }); + } + } else if (ts.isVariableStatement(node)) { + // Handle variable statements like "export const foo = ..." + if (node.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) { + node.declarationList.declarations.forEach((declaration) => { + const name = declaration.name.getText(); + if (name && !symbolOrderMap.has(name)) { + symbolOrderMap.set(name, orderIndex++); + } + }); + } + } else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isFunctionDeclaration(node)) { + // Handle direct declarations + if (node.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) { + const name = (node as any).name?.getText(); + if (name && !symbolOrderMap.has(name)) { + symbolOrderMap.set(name, orderIndex++); + } + } + } + + ts.forEachChild(node, establishSymbolOrder); + }; + + // Establish symbol order from all source files + sourceFiles.forEach((sourceFile) => { + establishSymbolOrder(sourceFile); + }); + + // Phase 1: Collect class hierarchy information + allSymbols.forEach((exportSymbol) => { + const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0]; + if (!declaration) return; + + const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public'); + if (!hasPublicTag) return; + + // Collect inheritance information for classes + if (ts.isClassDeclaration(declaration) && declaration.heritageClauses) { + const symbolName = declaration.name?.getText(); + if (!symbolName) return; + + const extendsClauseNode = declaration.heritageClauses.find( + clause => clause.token === ts.SyntaxKind.ExtendsKeyword, + ); + if (extendsClauseNode && extendsClauseNode.types[0]) { + const superClassName = extendsClauseNode.types[0].expression.getText(); + if (!classHierarchy.has(superClassName)) { + classHierarchy.set(superClassName, []); + } + classHierarchy.get(superClassName)!.push(symbolName); + } + } + }); + + // Collect class instances + allSymbols.forEach((exportSymbol) => { + const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0]; + if (!declaration) return; + + const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public'); + if (!hasPublicTag) return; + + if (ts.isVariableDeclaration(declaration)) { + const variableName = declaration.name.getText(); + let className: string | undefined; + // Check explicit type annotation + if (declaration.type && ts.isTypeReferenceNode(declaration.type)) { + className = declaration.type.typeName.getText(); + } + // Check initializer for constructor calls + else if (declaration.initializer && ts.isNewExpression(declaration.initializer)) { + className = declaration.initializer.expression.getText(); + } + if (className && exportedTypes.has(className)) { + if (!classInstances.has(className)) { + classInstances.set(className, []); + } + classInstances.get(className)!.push(variableName); + } + } + }); + + // Phase 2: Generate documentation for each symbol + allSymbols.forEach((exportSymbol) => { + const declaration = exportSymbol.valueDeclaration || exportSymbol.declarations?.[0]; + if (!declaration) return; + + // Check if it's a supported symbol type + const nodeKind = ts.SyntaxKind[declaration.kind]; + const symbolName = (declaration as any).name?.getText() || exportSymbol.getName(); + + // Only process symbols with @public tag + const hasPublicTag = ts.getJSDocTags(declaration).some(tag => tag.tagName.text === 'public'); + if (!hasPublicTag) return; + + // Check for @group tag (handle re-exports by looking at the original declaration) + let targetDeclaration = declaration; + if (exportSymbol.flags & ts.SymbolFlags.Alias) { + const aliasedSymbol = typeChecker.getAliasedSymbol(exportSymbol); + const aliasedDeclaration = aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]; + if (aliasedDeclaration) { + targetDeclaration = aliasedDeclaration; + } + } + + const groupTag = ts.getJSDocTags(targetDeclaration).find(tag => tag.tagName.text === 'group'); + if (!groupTag || typeof groupTag.comment !== 'string') { + throw new Error(`Symbol '${symbolName}' is missing @group JSDoc tag`); + } + const groupName = groupTag.comment.trim().replace(/\\(.)/g, '$1'); + + // Validate that the group exists in the API config + if (!Object.prototype.hasOwnProperty.call(groupConfig, groupName)) { + throw new Error(`Symbol '${symbolName}' has @group '${groupName}' which is not defined in API config`); + } + + // Check if it's a supported type + if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || ts.isVariableDeclaration(declaration)) { + // Supported types - continue processing + } else { + // Unsupported type - throw error with type info + throw new Error(`Unsupported symbol type: ${nodeKind} for symbol '${symbolName}'`); + } + + if (!declaration.name) return; + + // Handle variable declarations separately + if (ts.isVariableDeclaration(declaration)) { + const variableName = declaration.name.getText(); + + // Get variable description from JSDoc + const jsDocComment = ts.getJSDocCommentsAndTags(declaration)[0]; + let description = ''; + if (jsDocComment && ts.isJSDoc(jsDocComment)) { + const commentText = jsDocComment.comment; + if (typeof commentText === 'string') { + description = processLinkTags(commentText.trim(), variableName); + } + } + + // Check if it's a function type + const variableType = typeChecker.getTypeAtLocation(declaration); + const isFunctionType = variableType.getCallSignatures().length > 0; + + // Add to index + const order = symbolOrderMap.get(variableName); + if (order === undefined) { + throw new Error(`Symbol '${variableName}' not found in entry files export order`); + } + indexEntries.push({ name: variableName, type: isFunctionType ? 'Function' : 'Constant', group: groupName, order }); + + if (isFunctionType) { + // Handle function variables like methods + const signature = variableType.getCallSignatures()[0]; + if (signature) { + const parameters = signature.getParameters(); + const params = parameters.map((param) => { + const paramDecl = param.valueDeclaration; + if (paramDecl && ts.isParameter(paramDecl)) { + const paramName = param.getName(); + const hasQuestionToken = paramDecl.questionToken !== undefined; + const hasDefault = paramDecl.initializer !== undefined; + const isRest = paramDecl.dotDotDotToken !== undefined; + const rawParamType = paramDecl.type ? paramDecl.type.getText() : typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(param, paramDecl)); + const paramType = cleanOptionalType(rawParamType, hasQuestionToken); + + if (hasDefault) { + const defaultValue = paramDecl.initializer.getText(); + return `\t${isRest ? '...' : ''}${paramName}: ${paramType} = ${defaultValue}`; + } else { + return `\t${isRest ? '...' : ''}${paramName}${hasQuestionToken ? '?' : ''}: ${paramType}`; + } + } + return `\t${param.getName()}: unknown`; + }); + + const returnType = getTypeString(signature.getReturnType()); + const functionSig = params.length > 0 + ? `${variableName}(\n${params.join(',\n')},\n): ${returnType};` + : `${variableName}(): ${returnType};`; + + let markdown = `\n\n\n\n# ${variableName}\n\n\`\`\`ts\n${functionSig}\n\`\`\`${description ? `\n\n${description}` : ''}`; + + // Find referenced types in all parameters and return type + const allTypeStrings = params.map(p => p.replace(/\t.*?:\s*/, '')).concat([returnType]); + const allReferences = filterToExportedTypes([...new Set(allTypeStrings.flatMap(findAllTypeReferences))], variableName); + markdown += formatReferences(allReferences); + + const outputPath = path.join(outputDir, `${variableName}.md`); + fs.writeFileSync(outputPath, markdown); + console.log(`Generated: ${outputPath}`); + } + } else { + // Handle regular variables + let markdown = `\n\n\n\n# ${variableName}\n\n${description ? `${description}\n\n` : ''}`; + const variableValue = declaration.initializer ? declaration.initializer.getText() : 'undefined'; + const variableDefinition = `const ${variableName} = ${variableValue};`; + markdown += `\`\`\`ts\n${variableDefinition}\n\`\`\``; + + // Find referenced types in the variable value + const references = filterToExportedTypes(findAllTypeReferences(variableValue), variableName); + markdown += formatReferences(references); + + const outputPath = path.join(outputDir, `${variableName}.md`); + fs.writeFileSync(outputPath, markdown); + console.log(`Generated: ${outputPath}`); + } + + return; + } + + { + const className = declaration.name.text; + const isAbstract = ts.isClassDeclaration(declaration) && declaration.modifiers?.some(mod => mod.kind === ts.SyntaxKind.AbstractKeyword); + + // Add to index + const order = symbolOrderMap.get(className); + if (order === undefined) { + throw new Error(`Symbol '${className}' not found in entry files export order`); + } + if (isAbstract) { + indexEntries.push({ name: className, type: 'Abstract class', group: groupName, order }); + } else if (ts.isClassDeclaration(declaration)) { + indexEntries.push({ name: className, type: 'Class', group: groupName, order }); + } else if (ts.isTypeAliasDeclaration(declaration)) { + indexEntries.push({ name: className, type: 'Type', group: groupName, order }); + } else if (ts.isInterfaceDeclaration(declaration)) { + indexEntries.push({ name: className, type: 'Interface', group: groupName, order }); + } + + const properties: string[] = []; + const events: string[] = []; + const methods: string[] = []; + const staticMethods: string[] = []; + let constructor: string | null = null; + let extendsClause = ''; + let typeParameters: string | null = null; + + // Get class description from JSDoc (or from superclass if none) + let description = ''; + const jsDocComment = ts.getJSDocCommentsAndTags(declaration)[0]; + + if (jsDocComment && ts.isJSDoc(jsDocComment)) { + // First try to get the comment from the parsed JSDoc + const commentText = jsDocComment.comment; + if (typeof commentText === 'string' && commentText.trim()) { + description = processLinkTags(commentText.trim(), className); + } else { + // If no comment text, extract from raw source text + const sourceFile = declaration.getSourceFile(); + const sourceText = sourceFile.getFullText(); + const start = jsDocComment.getStart(); + const end = jsDocComment.getEnd(); + const rawJsDoc = sourceText.substring(start, end); + + // Extract the content between /** and */ + const match = rawJsDoc.match(/\/\*\*(.*?)\*\//s); + if (match && match[1]) { + const content = match[1] + .split('\n') + .map(line => line.replace(/^\s*\*\s?/, '')) // Remove leading * and spaces + .join('\n') + .trim(); + + // Filter out @tags but keep the description + const lines = content.split('\n'); + const descLines = lines.filter(line => !line.trim().startsWith('@')); + const rawDesc = descLines.join('\n').trim(); + + if (rawDesc) { + description = processLinkTags(rawDesc, className); + } + } + } + } + + // If no description, check superclass (only for classes/interfaces) + if (!description && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && declaration.heritageClauses) { + const classType = typeChecker.getTypeAtLocation(declaration); + const baseTypes = classType.getBaseTypes(); + if (baseTypes && baseTypes.length > 0) { + const baseSymbol = baseTypes[0]!.getSymbol(); + if (baseSymbol && baseSymbol.valueDeclaration) { + const baseJsDoc = ts.getJSDocCommentsAndTags(baseSymbol.valueDeclaration)[0]; + if (baseJsDoc && ts.isJSDoc(baseJsDoc) && typeof baseJsDoc.comment === 'string') { + description = processLinkTags(baseJsDoc.comment.trim(), className); + } + } + } + } + + // Check for type parameters (only for classes/interfaces) + if ((ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && declaration.typeParameters && declaration.typeParameters.length > 0) { + const typeParamStrings = declaration.typeParameters.map((tp) => { + const name = tp.name.text; + const constraint = tp.constraint ? ` extends ${tp.constraint.getText()}` : ''; + const defaultType = tp.default ? ` = ${tp.default.getText()}` : ''; + return `\t${name}${constraint}${defaultType}`; + }); + + const typeParamSig = `${className}<\n${typeParamStrings.join(',\n')},\n>`; + + // Get type parameter descriptions + const typeParamDocs: string[] = []; + const classJsDoc = ts.getJSDocCommentsAndTags(declaration)[0]; + if (classJsDoc && ts.isJSDoc(classJsDoc)) { + const templateTags = classJsDoc.tags?.filter((tag: any) => tag.tagName.text === 'template') || []; + templateTags.forEach((tag: any) => { + if (typeof tag.comment === 'string') { + const parts = tag.comment.trim().split(/\s+/); + const paramName = parts[0]; + const paramDesc = parts.slice(1).join(' ').replace(/^-\s*/, ''); + if (paramDesc) { + typeParamDocs.push(`- **${paramName}**: ${paramDesc}`); + } + } + }); + } + + typeParameters = `## Type parameters\n\n\`\`\`ts\n${typeParamSig}\n\`\`\``; + if (typeParamDocs.length > 0) { + typeParameters += `\n\n${typeParamDocs.join('\n')}`; + } + + // Find referenced types in type parameters for classes/interfaces + const typeParamRefs: string[] = []; + declaration.typeParameters.forEach((tp) => { + if (tp.constraint) { + typeParamRefs.push(...findAllTypeReferences(tp.constraint.getText())); + } + if (tp.default) { + typeParamRefs.push(...findAllTypeReferences(tp.default.getText())); + } + }); + const typeParamReferences = filterToExportedTypes([...new Set(typeParamRefs)], className); + const typeParamReferencesText = formatReferences(typeParamReferences); + if (typeParamReferencesText) { + typeParameters += typeParamReferencesText; + } + } + + // Check for extends clause (only for classes/interfaces) + if ((ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && declaration.heritageClauses) { + const extendsClauseNode = declaration.heritageClauses.find( + clause => clause.token === ts.SyntaxKind.ExtendsKeyword, + ); + if (extendsClauseNode && extendsClauseNode.types[0]) { + const superClassName = extendsClauseNode.types[0].expression.getText(); + extendsClause = `\n\n**Extends:** [\`${superClassName}\`](./${superClassName}.md)\n`; + } + } + + // Helper to get JSDoc description with superclass fallback (recursive) + const getDescriptionWithFallback = (member: ts.ClassElement | ts.TypeElement, memberName: string): string => { + const jsDoc = ts.getJSDocCommentsAndTags(member)[0]; + if (jsDoc && ts.isJSDoc(jsDoc)) { + // First try the parsed comment + if (typeof jsDoc.comment === 'string' && jsDoc.comment.trim()) { + return processLinkTags(jsDoc.comment.trim(), className); + } else { + // If no parsed comment, extract from raw source (same logic as class descriptions) + const sourceFile = member.getSourceFile(); + const sourceText = sourceFile.getFullText(); + const start = jsDoc.getStart(); + const end = jsDoc.getEnd(); + const rawJsDoc = sourceText.substring(start, end); + + const match = rawJsDoc.match(/\/\*\*(.*?)\*\//s); + if (match && match[1]) { + const content = match[1] + .split('\n') + .map(line => line.replace(/^\s*\*\s?/, '')) // Remove leading * and spaces + .join('\n') + .trim(); + + // Filter out @tags but keep the description + const lines = content.split('\n'); + const descLines = lines.filter(line => !line.trim().startsWith('@')); + const rawDesc = descLines.join('\n').trim(); + + if (rawDesc) { + return processLinkTags(rawDesc, className); + } + } + } + } + + // Recursively check superclass hierarchy for this member's documentation + const findInHierarchy = (currentDeclaration: ts.ClassDeclaration): string => { + const currentType = typeChecker.getTypeAtLocation(currentDeclaration); + const baseTypes = currentType.getBaseTypes(); + + if (baseTypes && baseTypes.length > 0) { + const baseSymbol = baseTypes[0]!.getSymbol(); + if (baseSymbol && baseSymbol.valueDeclaration && ts.isClassDeclaration(baseSymbol.valueDeclaration)) { + const baseMember = baseSymbol.valueDeclaration.members.find(m => + m.name && m.name.getText() === memberName, + ); + if (baseMember) { + const baseJsDoc = ts.getJSDocCommentsAndTags(baseMember)[0]; + if (baseJsDoc && ts.isJSDoc(baseJsDoc) && typeof baseJsDoc.comment === 'string') { + return processLinkTags(baseJsDoc.comment.trim(), className); + } + } + // Recursively check further up the hierarchy + return findInHierarchy(baseSymbol.valueDeclaration); + } + } + return ''; + }; + + if (ts.isClassDeclaration(declaration)) { + return findInHierarchy(declaration); + } + + return ''; + }; + + // Helper to extract linked types from a member's JSDoc + const getLinkedTypesFromMember = (member: ts.ClassElement | ts.TypeElement): string[] => { + const jsDoc = ts.getJSDocCommentsAndTags(member)[0]; + if (jsDoc && ts.isJSDoc(jsDoc)) { + // First try the parsed comment + if (typeof jsDoc.comment === 'string' && jsDoc.comment.trim()) { + return extractLinkedTypes(jsDoc.comment.trim()); + } else { + // If no parsed comment, extract from raw source + const sourceFile = member.getSourceFile(); + const sourceText = sourceFile.getFullText(); + const start = jsDoc.getStart(); + const end = jsDoc.getEnd(); + const rawJsDoc = sourceText.substring(start, end); + + const match = rawJsDoc.match(/\/\*\*(.*?)\*\//s); + if (match && match[1]) { + const content = match[1] + .split('\n') + .map(line => line.replace(/^\s*\*\s?/, '')) // Remove leading * and spaces + .join('\n') + .trim(); + + // Filter out @tags but keep the description + const lines = content.split('\n'); + const descLines = lines.filter(line => !line.trim().startsWith('@')); + const rawDesc = descLines.join('\n').trim(); + + return extractLinkedTypes(rawDesc); + } + } + } + return []; + }; + + // Helper to process members (both own and inherited) + const processMember = (member: ts.ClassElement | ts.TypeElement, _isInherited = false) => { + // Check if member has @internal in JSDoc + const hasInternalTag = ts.getJSDocTags(member).some(tag => tag.tagName.text === 'internal'); + if (hasInternalTag) return; + + if (ts.isConstructorDeclaration(member) && !isAbstract) { + // Skip private constructors + const isPrivate = member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword); + if (isPrivate) return; + + // Only process the first constructor we encounter to build the constructor section + // We'll collect all overloads separately + if (!constructor) { + // Collect all constructor overloads from the class + const constructorOverloads: ts.ConstructorDeclaration[] = []; + if (ts.isClassDeclaration(declaration)) { + declaration.members.forEach((m) => { + if (ts.isConstructorDeclaration(m) && !m.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword)) { + constructorOverloads.push(m); + } + }); + } + + // Build individual constructor blocks with their own descriptions + const constructorBlocks: string[] = []; + const allReferencedTypes: string[] = []; + + constructorOverloads.forEach((ctor) => { + // Skip the implementation (the one with a body) unless it's the only constructor + if (ctor.body && constructorOverloads.length > 1) { + // Process constructor parameters with visibility modifiers as properties (only from implementation) + ctor.parameters.forEach((param) => { + // Check if parameter has visibility modifier (public, private, protected, readonly) + const hasVisibilityModifier = param.modifiers?.some(mod => + mod.kind === ts.SyntaxKind.PublicKeyword + || mod.kind === ts.SyntaxKind.PrivateKeyword + || mod.kind === ts.SyntaxKind.ProtectedKeyword + || mod.kind === ts.SyntaxKind.ReadonlyKeyword, + ); + + if (!hasVisibilityModifier) return; + + // Skip private parameters + const isPrivate = param.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword); + if (isPrivate) return; + + const paramName = param.name.getText(); + const hasQuestionToken = param.questionToken !== undefined; + const isReadonly = param.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ReadonlyKeyword); + const rawParamType = param.type ? param.type.getText() : typeChecker.typeToString(typeChecker.getTypeAtLocation(param)); + const paramType = cleanOptionalType(rawParamType, hasQuestionToken); + + const propertyDef = `${isReadonly ? 'readonly ' : ''}${paramName}${hasQuestionToken ? '?' : ''}: ${paramType};`; + + // Get description from JSDoc comment on the parameter + const paramJsDoc = ts.getJSDocCommentsAndTags(param)[0]; + let desc = ''; + if (paramJsDoc && ts.isJSDoc(paramJsDoc) && typeof paramJsDoc.comment === 'string') { + desc = processLinkTags(paramJsDoc.comment.trim(), className); + } + + // Find referenced types + const references = filterToExportedTypes(findAllTypeReferences(paramType), className); + const referencesText = formatReferences(references); + + const propertyContent = `### \`${paramName}\`\n\n\`\`\`ts\n${propertyDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`; + properties.push(propertyContent); + }); + return; + } + + // Build parameter list for this overload + const params = ctor.parameters.map((param) => { + const paramName = param.name.getText(); + const hasQuestionToken = param.questionToken !== undefined; + const hasDefault = param.initializer !== undefined; + const isRest = param.dotDotDotToken !== undefined; + const rawParamType = param.type ? param.type.getText() : typeChecker.typeToString(typeChecker.getTypeAtLocation(param)); + const paramType = cleanOptionalType(rawParamType, hasQuestionToken); + + if (hasDefault) { + const defaultValue = param.initializer.getText(); + return `\t${isRest ? '...' : ''}${paramName}: ${paramType} = ${defaultValue}`; + } else { + return `\t${isRest ? '...' : ''}${paramName}${hasQuestionToken ? '?' : ''}: ${paramType}`; + } + }); + + const constructorSig = params.length > 0 + ? `constructor(\n${params.join(',\n')},\n): ${className};` + : `constructor(): ${className};`; + + // Collect referenced types from this overload + const ctorTypeStrings = params.map(p => p.replace(/\t.*?:\s*/, '')); + allReferencedTypes.push(...ctorTypeStrings.flatMap(findAllTypeReferences)); + + // Get description for this specific overload + const constructorDesc = getDescriptionWithFallback(ctor, 'constructor'); + const linkedTypes = getLinkedTypesFromMember(ctor); + + // Get parameter descriptions for this specific overload + const paramDocs: string[] = []; + const jsDoc = ts.getJSDocCommentsAndTags(ctor)[0]; + if (jsDoc && ts.isJSDoc(jsDoc)) { + const paramTags = jsDoc.tags?.filter((tag: any) => tag.tagName.text === 'param') || []; + paramTags.forEach((tag: any) => { + if (ts.isJSDocParameterTag(tag) && tag.name && typeof tag.comment === 'string') { + const paramName = tag.name.getText(); + const paramDesc = tag.comment.trim().replace(/^-\s*/, ''); + paramDocs.push(`- **${paramName}**: ${paramDesc}`); + } + }); + } + + // Build this constructor block + let constructorBlock = `\`\`\`ts\n${constructorSig}\n\`\`\``; + if (constructorDesc) { + constructorBlock += `\n\n${constructorDesc}`; + } + if (paramDocs.length > 0) { + constructorBlock += `\n\n**Parameters:**\n\n${paramDocs.join('\n')}`; + } + + // Find referenced types for this overload + const overloadReferences = filterToExportedTypes([...new Set(ctorTypeStrings.flatMap(findAllTypeReferences))], className); + constructorBlock += formatReferences(overloadReferences, linkedTypes); + + constructorBlocks.push(constructorBlock); + }); + + // Build the constructor section + const headingText = constructorBlocks.length > 1 ? 'Constructors' : 'Constructor'; + const separator = constructorBlocks.length > 1 ? '\n\n---\n\n' : '\n\n'; + constructor = `## ${headingText}\n\n${constructorBlocks.join(separator)}`; + } + } else if ((ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) && member.name) { + const name = member.name.getText(); + const isReadonly = member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ReadonlyKeyword); + const isOptional = member.questionToken !== undefined; + // Prefer explicit type annotation if available, otherwise use type checker + let rawType: string; + if (member.type) { + rawType = member.type.getText(); + // For optional properties, check if the type is a reference to an exported type + // If so, don't apply undefined removal logic to preserve the type reference + if (isOptional && exportedTypes.has(rawType)) { + // Keep the original type reference for exported types + } else { + rawType = cleanOptionalType(rawType, isOptional); + } + } else { + const memberType = typeChecker.getTypeAtLocation(member); + const typeName = memberType.getSymbol()?.getName(); + // If the type has a symbol name and it's in our exported types, use that instead of expanding + if (typeName && exportedTypes.has(typeName)) { + rawType = typeName; + } else { + rawType = getTypeString(memberType); + rawType = cleanOptionalType(rawType, isOptional); + } + } + const type = rawType; + const propertyDef = `${isReadonly ? 'readonly ' : ''}${name}${isOptional ? '?' : ''}: ${type};`; + + // Get description from JSDoc + const desc = getDescriptionWithFallback(member, name); + const linkedTypes = getLinkedTypesFromMember(member); + + // Find referenced types + const references = filterToExportedTypes(findAllTypeReferences(type), className); + const referencesText = formatReferences(references, linkedTypes); + + // Check if this is an event handler (starts with "on" and can be a function) + const isEventHandler = name.startsWith('on') && ( + type.includes('=>') + || type.includes('Function') + || type.includes('() =>') + || (type.includes('(') && type.includes(') =>')) + ); + + const inheritedBadge = ''; + const propertyContent = `### \`${name}\`${inheritedBadge}\n\n\`\`\`ts\n${propertyDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`; + + if (isEventHandler) { + events.push(propertyContent); + } else { + properties.push(propertyContent); + } + } else if (ts.isGetAccessorDeclaration(member) && member.name) { + const name = member.name.getText(); + // For getters, prefer the explicit return type annotation if available + let rawType: string; + if (member.type) { + rawType = member.type.getText(); + } else { + const memberType = typeChecker.getTypeAtLocation(member); + const typeName = memberType.getSymbol()?.getName(); + // If the type has a symbol name and it's in our exported types, use that instead of expanding + if (typeName && exportedTypes.has(typeName)) { + rawType = typeName; + } else { + rawType = getTypeString(memberType); + } + } + const type = cleanOptionalType(rawType, false); + const accessorDef = `get ${name}(): ${type};`; + + // Get description from JSDoc + const desc = getDescriptionWithFallback(member, name); + const linkedTypes = getLinkedTypesFromMember(member); + + // Find referenced types + const references = filterToExportedTypes(findAllTypeReferences(type), className); + const referencesText = formatReferences(references, linkedTypes); + + const inheritedBadge = ''; + properties.push(`### \`${name}\`${inheritedBadge}\n\n\`\`\`ts\n${accessorDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`); + } else if (ts.isSetAccessorDeclaration(member) && member.name) { + const name = member.name.getText(); + const param = member.parameters[0]; + const isOptional = param?.questionToken !== undefined; + const rawParamType = param?.type + ? typeChecker.typeToString(typeChecker.getTypeAtLocation(param)) + : 'any'; + const paramType = cleanOptionalType(rawParamType, isOptional); + const accessorDef = `set ${name}(value: ${paramType});`; + + // Get description from JSDoc + const desc = getDescriptionWithFallback(member, name); + const linkedTypes = getLinkedTypesFromMember(member); + + // Find referenced types + const references = filterToExportedTypes(findAllTypeReferences(paramType), className); + const referencesText = formatReferences(references, linkedTypes); + + const inheritedBadge = ''; + properties.push(`### \`${name}\`${inheritedBadge}\n\n\`\`\`ts\n${accessorDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`); + } else if ((ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) && member.name) { + const name = member.name.getText(); + const isStatic = member.modifiers?.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); + + // For method overloads, skip the implementation if it has no JSDoc + // (the overload signatures should have the documentation) + const methodJsDoc = ts.getJSDocCommentsAndTags(member)[0]; + const hasJsDoc = methodJsDoc && ts.isJSDoc(methodJsDoc) && methodJsDoc.comment; + + // If this is a method declaration (has a body) and has no JSDoc, check if there are overloads + if (ts.isMethodDeclaration(member) && member.body && !hasJsDoc && ts.isClassDeclaration(declaration)) { + // Check if there are other methods with the same name (overloads) + const sameNameMethods = declaration.members.filter(m => + (ts.isMethodDeclaration(m) || ts.isMethodSignature(m)) + && m.name && m.name.getText() === name, + ); + + // If there are multiple methods with the same name, this is likely an overloaded method + // Skip the implementation (the one with a body) if it has no JSDoc + if (sameNameMethods.length > 1) { + return; + } + } + + // Build parameter list with each on its own line + const params = member.parameters.map((param) => { + const paramName = param.name.getText(); + const hasQuestionToken = param.questionToken !== undefined; + const hasDefault = param.initializer !== undefined; + const isRest = param.dotDotDotToken !== undefined; + const rawParamType = param.type ? param.type.getText() : typeChecker.typeToString(typeChecker.getTypeAtLocation(param)); + const paramType = cleanOptionalType(rawParamType, hasQuestionToken); + + if (hasDefault) { + const defaultValue = param.initializer.getText(); + return `\t${isRest ? '...' : ''}${paramName}: ${paramType} = ${defaultValue}`; + } else { + return `\t${isRest ? '...' : ''}${paramName}${hasQuestionToken ? '?' : ''}: ${paramType}`; + } + }); + + // Get return type + const signature = typeChecker.getSignatureFromDeclaration(member); + const returnType = signature ? getTypeString(signature.getReturnType()) : 'void'; + + // Format method signature + const methodSig = params.length > 0 + ? `${isStatic ? 'static ' : ''}${name}(\n${params.join(',\n')},\n): ${returnType};` + : `${isStatic ? 'static ' : ''}${name}(): ${returnType};`; + + // Get method description from JSDoc + const desc = getDescriptionWithFallback(member, name); + const linkedTypes = getLinkedTypesFromMember(member); + + // Get parameter and return descriptions + const paramDocs: string[] = []; + let returnDoc = ''; + const jsDoc = ts.getJSDocCommentsAndTags(member)[0]; + if (jsDoc && ts.isJSDoc(jsDoc)) { + const paramTags = jsDoc.tags?.filter((tag: any) => tag.tagName.text === 'param') || []; + paramTags.forEach((tag: any) => { + if (ts.isJSDocParameterTag(tag) && tag.name && typeof tag.comment === 'string') { + const paramName = tag.name.getText(); + const paramDesc = tag.comment.trim().replace(/^-\s*/, ''); + paramDocs.push(`- **${paramName}**: ${paramDesc}`); + } + }); + + const returnTags = jsDoc.tags?.filter((tag: any) => tag.tagName.text === 'returns' || tag.tagName.text === 'return') || []; + if (returnTags.length > 0 && returnTags[0] && typeof returnTags[0].comment === 'string') { + returnDoc = returnTags[0].comment.trim().replace(/^-\s*/, ''); + } + } + + const inheritedBadge = ''; + let methodContent = `### \`${name}()\`${inheritedBadge}\n\n\`\`\`ts\n${methodSig}\n\`\`\``; + if (desc) { + methodContent += `\n\n${desc}`; + } + if (paramDocs.length > 0) { + methodContent += `\n\n**Parameters:**\n\n${paramDocs.join('\n')}`; + } + if (returnDoc) { + methodContent += `\n\n**Returns:** ${returnDoc}`; + } + + // Find referenced types in all parameters and return type + const allTypeStrings = params.map(p => p.replace(/\t.*?:\s*/, '')).concat([returnType]); + const allReferences = filterToExportedTypes([...new Set(allTypeStrings.flatMap(findAllTypeReferences))], className); + methodContent += formatReferences(allReferences, linkedTypes); + + if (isStatic) { + staticMethods.push(methodContent); + } else { + methods.push(methodContent); + } + } + }; + + // Track names of own members to avoid duplicates with inherited + const ownMemberNames = new Set(); + + // Process own members first + if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) { + declaration.members.forEach((member) => { + if (member.name) { + ownMemberNames.add(member.name.getText()); + } + processMember(member); + }); + } else if (ts.isTypeAliasDeclaration(declaration)) { + // For type aliases, check if it's a simple union type (like string literals) + const resolvedType = typeChecker.getTypeAtLocation(declaration); + + // If it's a primitive type or simple union, skip property processing + const isPrimitive = !!(resolvedType.flags & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean | ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.BooleanLiteral)); + const isSimpleUnion = resolvedType.isUnion() && resolvedType.types.every(t => + t.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.BooleanLiteral | ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean), + ); + + if (!isPrimitive && !isSimpleUnion) { + const typeProperties = typeChecker.getPropertiesOfType(resolvedType); + + typeProperties.forEach((prop) => { + // Create a synthetic property signature for each resolved property + const propName = prop.getName(); + const propType = typeChecker.getTypeOfSymbolAtLocation(prop, declaration); + const propTypeString = getTypeString(propType); + const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0; + + // Get JSDoc from the original declaration + let desc = ''; + const propDeclaration = prop.valueDeclaration || prop.declarations?.[0]; + if (propDeclaration) { + const jsDoc = ts.getJSDocCommentsAndTags(propDeclaration)[0]; + if (jsDoc && ts.isJSDoc(jsDoc) && typeof jsDoc.comment === 'string') { + desc = processLinkTags(jsDoc.comment.trim(), className); + } + } + + // Check if this property's original type annotation references an exported type + let cleanedType = propTypeString; + if (propDeclaration && ts.isPropertySignature(propDeclaration) && propDeclaration.type) { + const originalType = propDeclaration.type.getText(); + if (exportedTypes.has(originalType)) { + cleanedType = originalType; + } else { + cleanedType = cleanOptionalType(propTypeString, isOptional); + } + } else { + cleanedType = cleanOptionalType(propTypeString, isOptional); + } + + const propertyDef = `${propName}${isOptional ? '?' : ''}: ${cleanedType};`; + + // Find referenced types + const references = filterToExportedTypes(findAllTypeReferences(cleanedType), className); + const referencesText = formatReferences(references); + + // Check if this is an event handler (starts with "on" and can be a function) + const isEventHandler = propName.startsWith('on') && ( + cleanedType.includes('=>') + || cleanedType.includes('Function') + || cleanedType.includes('() =>') + || (cleanedType.includes('(') && cleanedType.includes(') =>')) + ); + + // Type alias properties are never inherited + const propertyContent = `### \`${propName}\`\n\n\`\`\`ts\n${propertyDef}\n\`\`\`${desc ? `\n\n${desc}` : ''}${referencesText}`; + + if (isEventHandler) { + events.push(propertyContent); + } else { + properties.push(propertyContent); + } + }); + } + } + + // Process inherited members (skip if overridden, only for classes) + if (ts.isClassDeclaration(declaration)) { + const classType = typeChecker.getTypeAtLocation(declaration); + const baseTypes = classType.getBaseTypes(); + if (baseTypes && baseTypes.length > 0) { + baseTypes.forEach((baseType) => { + const baseSymbol = baseType.getSymbol(); + if (baseSymbol && baseSymbol.valueDeclaration && ts.isClassDeclaration(baseSymbol.valueDeclaration)) { + baseSymbol.valueDeclaration.members.forEach((member) => { + // Skip if this member is overridden in the derived class + if (member.name && ownMemberNames.has(member.name.getText())) { + return; + } + // Never process inherited constructors - always use the derived class constructor + if (ts.isConstructorDeclaration(member)) { + return; + } + processMember(member, true); + }); + } + }); + } + } + + // Sort properties and methods alphabetically (but not for type aliases - keep source order) + if (!ts.isTypeAliasDeclaration(declaration)) { + properties.sort((a, b) => { + const nameA = a.match(/### (.+)/)?.[1] || ''; + const nameB = b.match(/### (.+)/)?.[1] || ''; + return nameA.localeCompare(nameB); + }); + } + + staticMethods.sort((a, b) => { + const nameA = a.match(/### (.+)/)?.[1] || ''; + const nameB = b.match(/### (.+)/)?.[1] || ''; + return nameA.localeCompare(nameB); + }); + + let markdown = ''; + + // Add VPBadge import and badge for all types + markdown += `\n\n`; + + if (isAbstract) { + markdown += `\n\n`; + } else if (ts.isClassDeclaration(declaration)) { + markdown += `\n\n`; + } else if (ts.isTypeAliasDeclaration(declaration)) { + markdown += `\n\n`; + } else if (ts.isInterfaceDeclaration(declaration)) { + markdown += `\n\n`; + } + + markdown += `# ${className}\n\n${description ? `${description}\n` : ''}${extendsClause}`; + + // Add subclasses section for classes that have subclasses + if (ts.isClassDeclaration(declaration) && classHierarchy.has(className)) { + const subclasses = classHierarchy.get(className)!; + // Sort by definition order instead of alphabetically + subclasses.sort((a, b) => { + const orderA = symbolOrderMap.get(a); + const orderB = symbolOrderMap.get(b); + if (orderA === undefined) throw new Error(`Symbol '${a}' not found in entry files export order`); + if (orderB === undefined) throw new Error(`Symbol '${b}' not found in entry files export order`); + return orderA - orderB; + }); + markdown += `\n## Subclasses\n\n`; + subclasses.forEach((sub) => { + markdown += `- [\`${sub}\`](./${sub}.md)\n`; + }); + } + + // Add instances section for classes that have instances + if (ts.isClassDeclaration(declaration) && classInstances.has(className)) { + const instances = classInstances.get(className)!; + // Sort by definition order instead of alphabetically + instances.sort((a, b) => { + const orderA = symbolOrderMap.get(a); + const orderB = symbolOrderMap.get(b); + if (orderA === undefined) throw new Error(`Symbol '${a}' not found in entry files export order`); + if (orderB === undefined) throw new Error(`Symbol '${b}' not found in entry files export order`); + return orderA - orderB; + }); + markdown += `\n## Instances\n\n`; + instances.forEach((instance) => { + markdown += `- [\`${instance}\`](./${instance}.md)\n`; + }); + } + + // Add type definition for type aliases + if (ts.isTypeAliasDeclaration(declaration) && declaration.type) { + const resolvedType = typeChecker.getTypeAtLocation(declaration); + const isPrimitive = !!(resolvedType.flags & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean | ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.BooleanLiteral)); + const isSimpleUnion = resolvedType.isUnion() && resolvedType.types.every(t => + t.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.BooleanLiteral | ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean), + ); + + // Build the type name with generic parameters if they exist + let typeName = className; + if (declaration.typeParameters && declaration.typeParameters.length > 0) { + const typeParamStrings = declaration.typeParameters.map((tp) => { + const name = tp.name.text; + const constraint = tp.constraint ? ` extends ${tp.constraint.getText()}` : ''; + const defaultType = tp.default ? ` = ${tp.default.getText()}` : ''; + return `${name}${constraint}${defaultType}`; + }); + typeName = `${className}<${typeParamStrings.join(', ')}>`; + } + + let typeText; + if (isPrimitive || isSimpleUnion) { + // For primitive types or simple unions, use resolved type string + const resolvedTypeString = typeChecker.typeToString(resolvedType); + if (resolvedTypeString === className) { + // If resolved type is just the alias name, use original text + typeText = declaration.type.getText(); + } else { + typeText = resolvedTypeString; + } + // Convert string literals from double quotes to single quotes + typeText = typeText.replace(/"([^"]*)"/g, '\'$1\''); + if (typeText.includes(' | ')) { + const unionMembers = typeText.split(' | '); + typeText = '\n\t| ' + unionMembers.join('\n\t| '); + } + } else { + // For complex types, use the original text + typeText = declaration.type.getText(); + // Format object types with proper line breaks + if (typeText.includes('{')) { + typeText = formatObjectType(typeText); + } + } + const typeDefinition = `type ${typeName} = ${typeText};`; + + // Find referenced types in the type definition and generic parameters + const allTypeRefs = findAllTypeReferences(typeText); + // Also find references in type parameters (extends clauses and default types) + if (declaration.typeParameters && declaration.typeParameters.length > 0) { + declaration.typeParameters.forEach((tp) => { + if (tp.constraint) { + allTypeRefs.push(...findAllTypeReferences(tp.constraint.getText())); + } + if (tp.default) { + allTypeRefs.push(...findAllTypeReferences(tp.default.getText())); + } + }); + } + const typeReferences = filterToExportedTypes([...new Set(allTypeRefs)], className); + const typeReferencesText = formatReferences(typeReferences); + + markdown += `\n\`\`\`ts\n${typeDefinition}\n\`\`\`${typeReferencesText}`; + } + + if (typeParameters) { + markdown += `\n${typeParameters}\n\n`; + } + + if (constructor) { + markdown += `\n${constructor}\n\n`; + } + + if (staticMethods.length > 0) { + markdown += `\n## Static methods\n\n${staticMethods.join('\n\n')}\n\n`; + } + + if (properties.length > 0) { + markdown += `\n## Properties\n\n${properties.join('\n\n')}\n\n`; + } + + if (events.length > 0) { + markdown += `\n## Events\n\n${events.join('\n\n')}\n\n`; + } + + if (methods.length > 0) { + markdown += `\n## Methods\n\n${methods.join('\n\n')}\n`; + } + const outputPath = path.join(outputDir, `${className}.md`); + fs.writeFileSync(outputPath, markdown); + console.log(`Generated: ${outputPath}`); + } + }); + + // Generate index.md with all exported symbols grouped by group + const entriesByGroup = new Map>(); + + indexEntries.forEach((entry) => { + if (!entriesByGroup.has(entry.group)) { + entriesByGroup.set(entry.group, []); + } + entriesByGroup.get(entry.group)!.push({ name: entry.name, type: entry.type, order: entry.order }); + }); + + // Sort groups according to API config order + const configGroups = Object.keys(groupConfig); + const sortedGroups = configGroups.filter(group => entriesByGroup.has(group)); + + // Check for groups in entries that aren't in config + const missingGroups = Array.from(entriesByGroup.keys()).filter(group => !configGroups.includes(group)); + if (missingGroups.length > 0) { + throw new Error(`Groups found in code but not in API config: ${missingGroups.join(', ')}`); + } + + let indexMarkdown = `# ${headingText}\n\n`; + + // Add intro text if provided + if (introText) { + indexMarkdown += `${introText}\n\n`; + } + + sortedGroups.forEach((group) => { + const entries = entriesByGroup.get(group)!; + // Sort entries by definition order + entries.sort((a, b) => a.order - b.order); + + indexMarkdown += `## ${group}\n\n`; + const groupDescription = groupConfig[group]; + if (groupDescription) { + indexMarkdown += `${groupDescription}\n\n`; + } + entries.forEach((entry) => { + indexMarkdown += `- [${entry.name}](./${entry.name}.md)\n`; + }); + indexMarkdown += '\n'; + }); + + const indexPath = path.join(outputDir, 'index.md'); + fs.writeFileSync(indexPath, indexMarkdown); + console.log(`Generated: ${indexPath}`); + + // Generate index.json with sidebar config structure + const sidebarConfig = sortedGroups.map((group) => { + const entries = entriesByGroup.get(group)!; + // Sort entries by definition order + entries.sort((a, b) => a.order - b.order); + + return { + text: group, + collapsed: true, + items: entries.map(entry => ({ + text: entry.name, + link: `/api/${entry.name}`, + })), + }; + }); + + const jsonPath = path.join(outputDir, 'index.json'); + fs.writeFileSync(jsonPath, JSON.stringify(sidebarConfig, null, 2)); + console.log(`Generated: ${jsonPath}`); +}; + +const main = () => { + const args = process.argv.slice(2); + if (args.length < 2) { + console.error('Usage: npm run generate-docs [entry-file2 ...] '); + console.error(' entry-files: One or more TypeScript entry files'); + console.error(' api-config-file: JSON config file defining groups'); + process.exit(1); + } + + // Last argument is the config file, everything else are entry files + const apiConfigFile = args[args.length - 1]!; + const entryFiles = args.slice(0, -1); + + generateDocs(entryFiles, apiConfigFile); +}; + +main(); diff --git a/src/codec.ts b/src/codec.ts index 8e4740e..119f7cb 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -28,6 +28,7 @@ import { SubtitleMetadata } from './subtitles'; /** * List of known video codecs, ordered by encoding preference. + * @group Codecs * @public */ export const VIDEO_CODECS = [ @@ -39,6 +40,7 @@ export const VIDEO_CODECS = [ ] as const; /** * List of known PCM (uncompressed) audio codecs, ordered by encoding preference. + * @group Codecs * @public */ export const PCM_AUDIO_CODECS = [ @@ -59,6 +61,7 @@ export const PCM_AUDIO_CODECS = [ ] as const; /** * List of known compressed audio codecs, ordered by encoding preference. + * @group Codecs * @public */ export const NON_PCM_AUDIO_CODECS = [ @@ -70,6 +73,7 @@ export const NON_PCM_AUDIO_CODECS = [ ] as const; /** * List of known audio codecs, ordered by encoding preference. + * @group Codecs * @public */ export const AUDIO_CODECS = [ @@ -78,6 +82,7 @@ export const AUDIO_CODECS = [ ] as const; /** * List of known subtitle codecs, ordered by encoding preference. + * @group Codecs * @public */ export const SUBTITLE_CODECS = [ @@ -86,22 +91,26 @@ export const SUBTITLE_CODECS = [ /** * Union type of known video codecs. + * @group Codecs * @public */ export type VideoCodec = typeof VIDEO_CODECS[number]; /** * Union type of known audio codecs. + * @group Codecs * @public */ export type AudioCodec = typeof AUDIO_CODECS[number]; export type PcmAudioCodec = typeof PCM_AUDIO_CODECS[number]; /** * Union type of known subtitle codecs. + * @group Codecs * @public */ export type SubtitleCodec = typeof SUBTITLE_CODECS[number]; /** * Union type of known media codecs. + * @group Codecs * @public */ export type MediaCodec = VideoCodec | AudioCodec | SubtitleCodec; @@ -726,110 +735,6 @@ export const getAudioEncoderConfigExtension = (codec: AudioCodec) => { return {}; }; -/** - * Represents a subjective media quality level. - * @public - */ -export class Quality { - /** @internal */ - _factor: number; - - /** @internal */ - constructor(factor: number) { - this._factor = factor; - } - - /** @internal */ - _toVideoBitrate(codec: VideoCodec, width: number, height: number) { - const pixels = width * height; - - const codecEfficiencyFactors = { - avc: 1.0, // H.264/AVC (baseline) - hevc: 0.6, // H.265/HEVC (~40% more efficient than AVC) - vp9: 0.6, // Similar to HEVC - av1: 0.4, // ~60% more efficient than AVC - vp8: 1.2, // Slightly less efficient than AVC - }; - - const referencePixels = 1920 * 1080; - const referenceBitrate = 3000000; - const scaleFactor = Math.pow(pixels / referencePixels, 0.95); // Slight non-linear scaling - const baseBitrate = referenceBitrate * scaleFactor; - - const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec]; - const finalBitrate = codecAdjustedBitrate * this._factor; - - return Math.ceil(finalBitrate / 1000) * 1000; - } - - /** @internal */ - _toAudioBitrate(codec: AudioCodec) { - if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec) || codec === 'flac') { - return undefined; - } - - const baseRates = { - aac: 128000, // 128kbps base for AAC - opus: 64000, // 64kbps base for Opus - mp3: 160000, // 160kbps base for MP3 - vorbis: 64000, // 64kbps base for Vorbis - }; - - const baseBitrate = baseRates[codec as keyof typeof baseRates]; - if (!baseBitrate) { - throw new Error(`Unhandled codec: ${codec}`); - } - - let finalBitrate = baseBitrate * this._factor; - - if (codec === 'aac') { - // AAC only works with specific bitrates, let's find the closest - const validRates = [96000, 128000, 160000, 192000]; - finalBitrate = validRates.reduce((prev, curr) => - Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev, - ); - } else if (codec === 'opus' || codec === 'vorbis') { - finalBitrate = Math.max(6000, finalBitrate); - } else if (codec === 'mp3') { - const validRates = [ - 8000, 16000, 24000, 32000, 40000, 48000, 64000, 80000, - 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, - ]; - finalBitrate = validRates.reduce((prev, curr) => - Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev, - ); - } - - return Math.round(finalBitrate / 1000) * 1000; - } -} - -/** - * Represents a very low media quality. - * @public - */ -export const QUALITY_VERY_LOW = new Quality(0.3); -/** - * Represents a low media quality. - * @public - */ -export const QUALITY_LOW = new Quality(0.6); -/** - * Represents a medium media quality. - * @public - */ -export const QUALITY_MEDIUM = new Quality(1); -/** - * Represents a high media quality. - * @public - */ -export const QUALITY_HIGH = new Quality(2); -/** - * Represents a very high media quality. - * @public - */ -export const QUALITY_VERY_HIGH = new Quality(4); - const VALID_VIDEO_CODEC_STRING_PREFIXES = ['avc1', 'avc3', 'hev1', 'hvc1', 'vp8', 'vp09', 'av01']; const AVC_CODEC_STRING_REGEX = /^(avc1|avc3)\.[0-9a-fA-F]{6}$/; const HEVC_CODEC_STRING_REGEX = /^(hev1|hvc1)\.(?:[ABC]?\d+)\.[0-9a-fA-F]{1,8}\.[LH]\d+(?:\.[0-9a-fA-F]{1,2}){0,6}$/; diff --git a/src/conversion.ts b/src/conversion.ts index 014a6bd..c3c1a0a 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -10,15 +10,14 @@ import { AUDIO_CODECS, AudioCodec, NON_PCM_AUDIO_CODECS, - Quality, - QUALITY_HIGH, VIDEO_CODECS, VideoCodec, } from './codec'; import { - AudioEncodingConfig, getEncodableAudioCodecs, getFirstEncodableVideoCodec, + Quality, + QUALITY_HIGH, VideoEncodingConfig, } from './encode'; import { Input } from './input'; @@ -51,6 +50,7 @@ import { AudioSample, VideoSample } from './sample'; /** * The options for media file conversion. + * @group Conversion * @public */ export type ConversionOptions = { @@ -62,8 +62,8 @@ export type ConversionOptions = { /** * Video-specific options. When passing an object, the same options are applied to all video tracks. When passing a * function, it will be invoked for each video track and is expected to return or resolve to the options - * for that specific track. The function is passed an instance of `InputVideoTrack` as well as a number `n`, which - * is the 1-based index of the track in the list of all video tracks. + * for that specific track. The function is passed an instance of {@link InputVideoTrack} as well as a number `n`, + * which is the 1-based index of the track in the list of all video tracks. */ video?: ConversionVideoOptions | ((track: InputVideoTrack, n: number) => MaybePromise); @@ -71,8 +71,8 @@ export type ConversionOptions = { /** * Audio-specific options. When passing an object, the same options are applied to all audio tracks. When passing a * function, it will be invoked for each audio track and is expected to return or resolve to the options - * for that specific track. The function is passed an instance of `InputAudioTrack` as well as a number `n`, which - * is the 1-based index of the track in the list of all audio tracks. + * for that specific track. The function is passed an instance of {@link InputAudioTrack} as well as a number `n`, + * which is the 1-based index of the track in the list of all audio tracks. */ audio?: ConversionAudioOptions | ((track: InputAudioTrack, n: number) => MaybePromise); @@ -88,10 +88,11 @@ export type ConversionOptions = { /** * Video-specific options. + * @group Conversion * @public */ export type ConversionVideoOptions = { - /** If true, all video tracks will be discarded and will not be present in the output. */ + /** If `true`, all video tracks will be discarded and will not be present in the output. */ discard?: boolean; /** * The desired width of the output video in pixels, defaulting to the video's natural display width. If height @@ -106,10 +107,10 @@ export type ConversionVideoOptions = { /** * The fitting algorithm in case both width and height are set, or if the input video changes its size over time. * - * - 'fill' will stretch the image to fill the entire box, potentially altering aspect ratio. - * - 'contain' will contain the entire image within the box while preserving aspect ratio. This may lead to + * - `'fill'` will stretch the image to fill the entire box, potentially altering aspect ratio. + * - `'contain'` will contain the entire image within the box while preserving aspect ratio. This may lead to * letterboxing. - * - 'cover' will scale the image until the entire box is filled, while preserving aspect ratio. + * - `'cover'` will scale the image until the entire box is filled, while preserving aspect ratio. */ fit?: 'fill' | 'contain' | 'cover'; /** @@ -125,17 +126,18 @@ export type ConversionVideoOptions = { /** The desired output video codec. */ codec?: VideoCodec; /** The desired bitrate of the output video. */ - bitrate?: VideoEncodingConfig['bitrate']; - /** When true, video will always be re-encoded instead of directly copying over the encoded samples. */ + bitrate?: number | Quality; + /** When `true`, video will always be re-encoded instead of directly copying over the encoded samples. */ forceTranscode?: boolean; }; /** * Audio-specific options. + * @group Conversion * @public */ export type ConversionAudioOptions = { - /** If true, all audio tracks will be discarded and will not be present in the output. */ + /** If `true`, all audio tracks will be discarded and will not be present in the output. */ discard?: boolean; /** The desired channel count of the output audio. */ numberOfChannels?: number; @@ -144,8 +146,8 @@ export type ConversionAudioOptions = { /** The desired output audio codec. */ codec?: AudioCodec; /** The desired bitrate of the output audio. */ - bitrate?: AudioEncodingConfig['bitrate']; - /** When true, audio will always be re-encoded instead of directly copying over the encoded samples. */ + bitrate?: number | Quality; + /** When `true`, audio will always be re-encoded instead of directly copying over the encoded samples. */ forceTranscode?: boolean; }; @@ -246,9 +248,41 @@ const validateAudioOptions = (audioOptions: ConversionAudioOptions | undefined) const FALLBACK_NUMBER_OF_CHANNELS = 2; const FALLBACK_SAMPLE_RATE = 48000; +/** + * An input track that was discarded (excluded) from a {@link Conversion} alongside the discard reason. + * @group Conversion + * @public + */ +export type DiscardedTrack = { + /** The track that was discarded. */ + track: InputTrack; + /** + * The reason for discarding the track. + * + * - `'discarded_by_user'`: You discarded this track by setting `discard: true`. + * - `'max_track_count_reached'`: The output had no more room for another track. + * - `'max_track_count_of_type_reached'`: The output had no more room for another track of this type, or the output + * doesn't support this track type at all. + * - `'unknown_source_codec'`: We don't know the codec of the input track and therefore don't know what to do + * with it. + * - `'undecodable_source_codec'`: The input track's codec is known, but we are unable to decode it. + * - `'no_encodable_target_codec'`: We can't find a codec that we are able to encode and that can be contained + * within the output format. This reason can be hit if the environment doesn't support the necessary encoders, or if + * you requested a codec that cannot be contained within the output format. + */ + reason: + | 'discarded_by_user' + | 'max_track_count_reached' + | 'max_track_count_of_type_reached' + | 'unknown_source_codec' + | 'undecodable_source_codec' + | 'no_encodable_target_codec'; +}; + /** * Represents a media file conversion process, used to convert one media file into another. In addition to conversion, * this class can be used to resize and rotate video, resample audio, drop tracks, or trim to a specific time range. + * @group Conversion * @public */ export class Conversion { @@ -298,7 +332,7 @@ export class Conversion { /** * A callback that is fired whenever the conversion progresses. Returns a number between 0 and 1, indicating the * completion of the conversion. Note that a progress of 1 doesn't necessarily mean the conversion is complete; - * the conversion is complete once `execute` resolves. + * the conversion is complete once `execute()` resolves. * * In order for progress to be computed, this property must be set before `execute` is called. */ @@ -311,18 +345,7 @@ export class Conversion { /** The list of tracks that are included in the output file. */ readonly utilizedTracks: InputTrack[] = []; /** The list of tracks from the input file that have been discarded, alongside the discard reason. */ - readonly discardedTracks: { - /** The track that was discarded. */ - track: InputTrack; - /** The reason for discarding the track. */ - reason: - | 'discarded_by_user' - | 'max_track_count_reached' - | 'max_track_count_of_type_reached' - | 'unknown_source_codec' - | 'undecodable_source_codec' - | 'no_encodable_target_codec'; - }[] = []; + readonly discardedTracks: DiscardedTrack[] = []; /** Initializes a new conversion process without starting the conversion. */ static async init(options: ConversionOptions) { @@ -332,6 +355,7 @@ export class Conversion { return conversion; } + /** Creates a new Conversion instance (duh). */ private constructor(options: ConversionOptions) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); diff --git a/src/custom-coder.ts b/src/custom-coder.ts index 4c3bccf..ddc422b 100644 --- a/src/custom-coder.ts +++ b/src/custom-coder.ts @@ -7,12 +7,14 @@ */ import { AudioCodec, VideoCodec } from './codec'; +import { MaybePromise } from './misc'; import { EncodedPacket } from './packet'; import { AudioSample, VideoSample } from './sample'; /** * Base class for custom video decoders. To add your own custom video decoder, extend this class, implement the - * abstract methods and static `supports` method, and register the decoder using `registerDecoder`. + * abstract methods and static `supports` method, and register the decoder using {@link registerDecoder}. + * @group Custom coders * @public */ export abstract class CustomVideoDecoder { @@ -23,25 +25,26 @@ export abstract class CustomVideoDecoder { /** The callback to call when a decoded VideoSample is available. */ readonly onSample!: (sample: VideoSample) => unknown; - /** Returns true iff the decoder can decode the given codec configuration. */ + /** Returns true if and only if the decoder can decode the given codec configuration. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars static supports(codec: VideoCodec, config: VideoDecoderConfig): boolean { return false; } /** Called after decoder creation; can be used for custom initialization logic. */ - abstract init(): Promise | void; + abstract init(): MaybePromise; /** Decodes the provided encoded packet. */ - abstract decode(packet: EncodedPacket): Promise | void; + abstract decode(packet: EncodedPacket): MaybePromise; /** Decodes all remaining packets and then resolves. */ - abstract flush(): Promise | void; + abstract flush(): MaybePromise; /** Called when the decoder is no longer needed and its resources can be freed. */ - abstract close(): Promise | void; + abstract close(): MaybePromise; } /** * Base class for custom audio decoders. To add your own custom audio decoder, extend this class, implement the - * abstract methods and static `supports` method, and register the decoder using `registerDecoder`. + * abstract methods and static `supports` method, and register the decoder using {@link registerDecoder}. + * @group Custom coders * @public */ export abstract class CustomAudioDecoder { @@ -52,25 +55,26 @@ export abstract class CustomAudioDecoder { /** The callback to call when a decoded AudioSample is available. */ readonly onSample!: (sample: AudioSample) => unknown; - /** Returns true iff the decoder can decode the given codec configuration. */ + /** Returns true if and only if the decoder can decode the given codec configuration. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars static supports(codec: AudioCodec, config: AudioDecoderConfig): boolean { return false; } /** Called after decoder creation; can be used for custom initialization logic. */ - abstract init(): Promise | void; + abstract init(): MaybePromise; /** Decodes the provided encoded packet. */ - abstract decode(packet: EncodedPacket): Promise | void; + abstract decode(packet: EncodedPacket): MaybePromise; /** Decodes all remaining packets and then resolves. */ - abstract flush(): Promise | void; + abstract flush(): MaybePromise; /** Called when the decoder is no longer needed and its resources can be freed. */ - abstract close(): Promise | void; + abstract close(): MaybePromise; } /** * Base class for custom video encoders. To add your own custom video encoder, extend this class, implement the - * abstract methods and static `supports` method, and register the encoder using `registerEncoder`. + * abstract methods and static `supports` method, and register the encoder using {@link registerEncoder}. + * @group Custom coders * @public */ export abstract class CustomVideoEncoder { @@ -81,25 +85,26 @@ export abstract class CustomVideoEncoder { /** The callback to call when an EncodedPacket is available. */ readonly onPacket!: (packet: EncodedPacket, meta?: EncodedVideoChunkMetadata) => unknown; - /** Returns true iff the encoder can encode the given codec configuration. */ + /** Returns true if and only if the encoder can encode the given codec configuration. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars static supports(codec: VideoCodec, config: VideoEncoderConfig): boolean { return false; } /** Called after encoder creation; can be used for custom initialization logic. */ - abstract init(): Promise | void; + abstract init(): MaybePromise; /** Encodes the provided video sample. */ - abstract encode(videoSample: VideoSample, options: VideoEncoderEncodeOptions): Promise | void; + abstract encode(videoSample: VideoSample, options: VideoEncoderEncodeOptions): MaybePromise; /** Encodes all remaining video samples and then resolves. */ - abstract flush(): Promise | void; + abstract flush(): MaybePromise; /** Called when the encoder is no longer needed and its resources can be freed. */ - abstract close(): Promise | void; + abstract close(): MaybePromise; } /** * Base class for custom audio encoders. To add your own custom audio encoder, extend this class, implement the - * abstract methods and static `supports` method, and register the encoder using `registerEncoder`. + * abstract methods and static `supports` method, and register the encoder using {@link registerEncoder}. + * @group Custom coders * @public */ export abstract class CustomAudioEncoder { @@ -110,20 +115,20 @@ export abstract class CustomAudioEncoder { /** The callback to call when an EncodedPacket is available. */ readonly onPacket!: (packet: EncodedPacket, meta?: EncodedAudioChunkMetadata) => unknown; - /** Returns true iff the encoder can encode the given codec configuration. */ + /** Returns true if and only if the encoder can encode the given codec configuration. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars static supports(codec: AudioCodec, config: AudioEncoderConfig): boolean { return false; } /** Called after encoder creation; can be used for custom initialization logic. */ - abstract init(): Promise | void; + abstract init(): MaybePromise; /** Encodes the provided audio sample. */ - abstract encode(audioSample: AudioSample): Promise | void; + abstract encode(audioSample: AudioSample): MaybePromise; /** Encodes all remaining audio samples and then resolves. */ - abstract flush(): Promise | void; + abstract flush(): MaybePromise; /** Called when the encoder is no longer needed and its resources can be freed. */ - abstract close(): Promise | void; + abstract close(): MaybePromise; } export const customVideoDecoders: typeof CustomVideoDecoder[] = []; @@ -134,6 +139,7 @@ export const customAudioEncoders: typeof CustomAudioEncoder[] = []; /** * Registers a custom video or audio decoder. Registered decoders will automatically be used for decoding whenever * possible. + * @group Custom coders * @public */ export const registerDecoder = (decoder: typeof CustomVideoDecoder | typeof CustomAudioDecoder) => { @@ -163,6 +169,7 @@ export const registerDecoder = (decoder: typeof CustomVideoDecoder | typeof Cust /** * Registers a custom video or audio encoder. Registered encoders will automatically be used for encoding whenever * possible. + * @group Custom coders * @public */ export const registerEncoder = (encoder: typeof CustomVideoEncoder | typeof CustomAudioEncoder) => { diff --git a/src/encode.ts b/src/encode.ts index dd596c5..0eb26da 100644 --- a/src/encode.ts +++ b/src/encode.ts @@ -16,7 +16,6 @@ import { inferCodecFromCodecString, MediaCodec, PCM_AUDIO_CODECS, - Quality, SUBTITLE_CODECS, SubtitleCodec, VIDEO_CODECS, @@ -27,13 +26,14 @@ import { EncodedPacket } from './packet'; /** * Configuration object that controls video encoding. Can be used to set codec, quality, and more. + * @group Encoding * @public */ export type VideoEncodingConfig = { /** The video codec that should be used for encoding the video samples (frames). */ codec: VideoCodec; /** - * The target bitrate for the encoded video, in bits per second. Alternatively, a subjective Quality can + * The target bitrate for the encoded video, in bits per second. Alternatively, a subjective {@link Quality} can * be provided. */ bitrate: number | Quality; @@ -44,14 +44,14 @@ export type VideoEncodingConfig = { */ keyFrameInterval?: number; /** - * Video frames may change size overtime. This field controls the behavior in case this happens. + * Video frames may change size over time. This field controls the behavior in case this happens. * - * - 'deny' (default) will throw an error, requiring all frames to have the exact same dimensions. - * - 'passThrough' will allow the change and directly pass the frame to the encoder. - * - 'fill' will stretch the image to fill the entire original box, potentially altering aspect ratio. - * - 'contain' will contain the entire image within the originalbox while preserving aspect ratio. This may lead to - * letterboxing. - * - 'cover' will scale the image until the entire original box is filled, while preserving aspect ratio. + * - `'deny'` (default) will throw an error, requiring all frames to have the exact same dimensions. + * - `'passThrough'` will allow the change and directly pass the frame to the encoder. + * - `'fill'` will stretch the image to fill the entire original box, potentially altering aspect ratio. + * - `'contain'` will contain the entire image within the original box while preserving aspect ratio. This may lead + * to letterboxing. + * - `'cover'` will scale the image until the entire original box is filled, while preserving aspect ratio. * * The "original box" refers to the dimensions of the first encoded frame. */ @@ -59,7 +59,10 @@ export type VideoEncodingConfig = { /** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */ onEncodedPacket?: (packet: EncodedPacket, meta: EncodedVideoChunkMetadata | undefined) => unknown; - /** Called when the internal encoder config, as used by the WebCodecs API, is created. */ + /** + * Called when the internal [encoder config](https://www.w3.org/TR/webcodecs/#video-encoder-config), as used by the + * WebCodecs API, is created. + */ onEncoderConfig?: (config: VideoEncoderConfig) => unknown; } & VideoEncodingAdditionalOptions; @@ -100,30 +103,33 @@ export const validateVideoEncodingConfig = (config: VideoEncodingConfig) => { /** * Additional options that control audio encoding. + * @group Encoding * @public */ export type VideoEncodingAdditionalOptions = { /** Configures the bitrate mode. */ bitrateMode?: 'constant' | 'variable'; /** The latency mode used by the encoder; controls the performance-quality tradeoff. */ - latencyMode?: VideoEncoderConfig['latencyMode']; + latencyMode?: 'quality' | 'realtime'; /** * The full codec string as specified in the WebCodecs Codec Registry. This string must match the codec * specified in `codec`. When not set, a fitting codec string will be constructed automatically by the library. */ fullCodecString?: string; - /** A hint that configures the hardware acceleration method of this codec. This is best left on 'no-preference'. */ - hardwareAcceleration?: VideoEncoderConfig['hardwareAcceleration']; + /** + * A hint that configures the hardware acceleration method of this codec. This is best left on `'no-preference'`. + */ + hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software'; /** * An encoding scalability mode identifier as defined by * [WebRTC-SVC](https://w3c.github.io/webrtc-svc/#scalabilitymodes*). */ - scalabilityMode?: VideoEncoderConfig['scalabilityMode']; + scalabilityMode?: string; /** * An encoding video content hint as defined by * [mst-content-hint](https://w3c.github.io/mst-content-hint/#video-content-hints). */ - contentHint?: VideoEncoderConfig['contentHint']; + contentHint?: string; }; export const validateVideoEncodingAdditionalOptions = (codec: VideoCodec, options: VideoEncodingAdditionalOptions) => { @@ -194,20 +200,24 @@ export const buildVideoEncoderConfig = (options: { /** * Configuration object that controls audio encoding. Can be used to set codec, quality, and more. + * @group Encoding * @public */ export type AudioEncodingConfig = { /** The audio codec that should be used for encoding the audio samples. */ codec: AudioCodec; /** - * The target bitrate for the encoded audio, in bits per second. Alternatively, a subjective Quality can + * The target bitrate for the encoded audio, in bits per second. Alternatively, a subjective {@link Quality} can * be provided. Required for compressed audio codecs, unused for PCM codecs. */ bitrate?: number | Quality; /** Called for each successfully encoded packet. Both the packet and the encoding metadata are passed. */ onEncodedPacket?: (packet: EncodedPacket, meta: EncodedAudioChunkMetadata | undefined) => unknown; - /** Called when the internal encoder config, as used by the WebCodecs API, is created. */ + /** + * Called when the internal [encoder config](https://www.w3.org/TR/webcodecs/#audio-encoder-config), as used by the + * WebCodecs API, is created. + */ onEncoderConfig?: (config: AudioEncoderConfig) => unknown; } & AudioEncodingAdditionalOptions; @@ -243,6 +253,7 @@ export const validateAudioEncodingConfig = (config: AudioEncodingConfig) => { /** * Additional options that control audio encoding. + * @group Encoding * @public */ export type AudioEncodingAdditionalOptions = { @@ -296,8 +307,119 @@ export const buildAudioEncoderConfig = (options: { }; }; +/** + * Represents a subjective media quality level. + * @group Encoding + * @public + */ +export class Quality { + /** @internal */ + _factor: number; + + /** @internal */ + constructor(factor: number) { + this._factor = factor; + } + + /** @internal */ + _toVideoBitrate(codec: VideoCodec, width: number, height: number) { + const pixels = width * height; + + const codecEfficiencyFactors = { + avc: 1.0, // H.264/AVC (baseline) + hevc: 0.6, // H.265/HEVC (~40% more efficient than AVC) + vp9: 0.6, // Similar to HEVC + av1: 0.4, // ~60% more efficient than AVC + vp8: 1.2, // Slightly less efficient than AVC + }; + + const referencePixels = 1920 * 1080; + const referenceBitrate = 3000000; + const scaleFactor = Math.pow(pixels / referencePixels, 0.95); // Slight non-linear scaling + const baseBitrate = referenceBitrate * scaleFactor; + + const codecAdjustedBitrate = baseBitrate * codecEfficiencyFactors[codec]; + const finalBitrate = codecAdjustedBitrate * this._factor; + + return Math.ceil(finalBitrate / 1000) * 1000; + } + + /** @internal */ + _toAudioBitrate(codec: AudioCodec) { + if ((PCM_AUDIO_CODECS as readonly string[]).includes(codec) || codec === 'flac') { + return undefined; + } + + const baseRates = { + aac: 128000, // 128kbps base for AAC + opus: 64000, // 64kbps base for Opus + mp3: 160000, // 160kbps base for MP3 + vorbis: 64000, // 64kbps base for Vorbis + }; + + const baseBitrate = baseRates[codec as keyof typeof baseRates]; + if (!baseBitrate) { + throw new Error(`Unhandled codec: ${codec}`); + } + + let finalBitrate = baseBitrate * this._factor; + + if (codec === 'aac') { + // AAC only works with specific bitrates, let's find the closest + const validRates = [96000, 128000, 160000, 192000]; + finalBitrate = validRates.reduce((prev, curr) => + Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev, + ); + } else if (codec === 'opus' || codec === 'vorbis') { + finalBitrate = Math.max(6000, finalBitrate); + } else if (codec === 'mp3') { + const validRates = [ + 8000, 16000, 24000, 32000, 40000, 48000, 64000, 80000, + 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, + ]; + finalBitrate = validRates.reduce((prev, curr) => + Math.abs(curr - finalBitrate) < Math.abs(prev - finalBitrate) ? curr : prev, + ); + } + + return Math.round(finalBitrate / 1000) * 1000; + } +} + +/** + * Represents a very low media quality. + * @group Encoding + * @public + */ +export const QUALITY_VERY_LOW = new Quality(0.3); +/** + * Represents a low media quality. + * @group Encoding + * @public + */ +export const QUALITY_LOW = new Quality(0.6); +/** + * Represents a medium media quality. + * @group Encoding + * @public + */ +export const QUALITY_MEDIUM = new Quality(1); +/** + * Represents a high media quality. + * @group Encoding + * @public + */ +export const QUALITY_HIGH = new Quality(2); +/** + * Represents a very high media quality. + * @group Encoding + * @public + */ +export const QUALITY_VERY_HIGH = new Quality(4); + /** * Checks if the browser is able to encode the given codec. + * @group Encoding * @public */ export const canEncode = (codec: MediaCodec) => { @@ -314,18 +436,24 @@ export const canEncode = (codec: MediaCodec) => { /** * Checks if the browser is able to encode the given video codec with the given parameters. + * @group Encoding * @public */ -export const canEncodeVideo = async (codec: VideoCodec, { - width = 1280, - height = 720, - bitrate = 1e6, - ...restOptions -}: { - width?: number; - height?: number; - bitrate?: number | Quality; -} & VideoEncodingAdditionalOptions = {}) => { +export const canEncodeVideo = async ( + codec: VideoCodec, + options: { + width?: number; + height?: number; + bitrate?: number | Quality; + } & VideoEncodingAdditionalOptions = {}, +) => { + const { + width = 1280, + height = 720, + bitrate = 1e6, + ...restOptions + } = options; + if (!VIDEO_CODECS.includes(codec)) { return false; } @@ -377,18 +505,24 @@ export const canEncodeVideo = async (codec: VideoCodec, { /** * Checks if the browser is able to encode the given audio codec with the given parameters. + * @group Encoding * @public */ -export const canEncodeAudio = async (codec: AudioCodec, { - numberOfChannels = 2, - sampleRate = 48000, - bitrate = 128e3, - ...restOptions -}: { - numberOfChannels?: number; - sampleRate?: number; - bitrate?: number | Quality; -} & AudioEncodingAdditionalOptions = {}) => { +export const canEncodeAudio = async ( + codec: AudioCodec, + options: { + numberOfChannels?: number; + sampleRate?: number; + bitrate?: number | Quality; + } & AudioEncodingAdditionalOptions = {}, +) => { + const { + numberOfChannels = 2, + sampleRate = 48000, + bitrate = 128e3, + ...restOptions + } = options; + if (!AUDIO_CODECS.includes(codec)) { return false; } @@ -442,6 +576,7 @@ export const canEncodeAudio = async (codec: AudioCodec, { /** * Checks if the browser is able to encode the given subtitle codec. + * @group Encoding * @public */ export const canEncodeSubtitles = async (codec: SubtitleCodec) => { @@ -454,6 +589,7 @@ export const canEncodeSubtitles = async (codec: SubtitleCodec) => { /** * Returns the list of all media codecs that can be encoded by the browser. + * @group Encoding * @public */ export const getEncodableCodecs = async (): Promise => { @@ -468,10 +604,11 @@ export const getEncodableCodecs = async (): Promise => { /** * Returns the list of all video codecs that can be encoded by the browser. + * @group Encoding * @public */ export const getEncodableVideoCodecs = async ( - checkedCodecs = VIDEO_CODECS as unknown as VideoCodec[], + checkedCodecs: VideoCodec[] = VIDEO_CODECS as unknown as VideoCodec[], options?: { width?: number; height?: number; @@ -484,10 +621,11 @@ export const getEncodableVideoCodecs = async ( /** * Returns the list of all audio codecs that can be encoded by the browser. + * @group Encoding * @public */ export const getEncodableAudioCodecs = async ( - checkedCodecs = AUDIO_CODECS as unknown as AudioCodec[], + checkedCodecs: AudioCodec[] = AUDIO_CODECS as unknown as AudioCodec[], options?: { numberOfChannels?: number; sampleRate?: number; @@ -500,10 +638,11 @@ export const getEncodableAudioCodecs = async ( /** * Returns the list of all subtitle codecs that can be encoded by the browser. + * @group Encoding * @public */ export const getEncodableSubtitleCodecs = async ( - checkedCodecs = SUBTITLE_CODECS as unknown as SubtitleCodec[], + checkedCodecs: SubtitleCodec[] = SUBTITLE_CODECS as unknown as SubtitleCodec[], ): Promise => { const bools = await Promise.all(checkedCodecs.map(canEncodeSubtitles)); return checkedCodecs.filter((_, i) => bools[i]); @@ -511,6 +650,7 @@ export const getEncodableSubtitleCodecs = async ( /** * Returns the first video codec from the given list that can be encoded by the browser. + * @group Encoding * @public */ export const getFirstEncodableVideoCodec = async ( @@ -532,6 +672,7 @@ export const getFirstEncodableVideoCodec = async ( /** * Returns the first audio codec from the given list that can be encoded by the browser. + * @group Encoding * @public */ export const getFirstEncodableAudioCodec = async ( @@ -553,6 +694,7 @@ export const getFirstEncodableAudioCodec = async ( /** * Returns the first subtitle codec from the given list that can be encoded by the browser. + * @group Encoding * @public */ export const getFirstEncodableSubtitleCodec = async ( diff --git a/src/index.ts b/src/index.ts index da1f4ce..9bd1991 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,61 +16,53 @@ export { VideoTrackMetadata, AudioTrackMetadata, SubtitleTrackMetadata, - TrackType, - ALL_TRACK_TYPES, } from './output'; export { OutputFormat, + AdtsOutputFormat, + AdtsOutputFormatOptions, IsobmffOutputFormat, - Mp4OutputFormat, - MovOutputFormat, IsobmffOutputFormatOptions, MkvOutputFormat, MkvOutputFormatOptions, - WebMOutputFormat, - WebMOutputFormatOptions, + MovOutputFormat, Mp3OutputFormat, Mp3OutputFormatOptions, - WavOutputFormat, - WavOutputFormatOptions, + Mp4OutputFormat, OggOutputFormat, OggOutputFormatOptions, - AdtsOutputFormat, - AdtsOutputFormatOptions, - TrackCountLimits, + WavOutputFormat, + WavOutputFormatOptions, + WebMOutputFormat, + WebMOutputFormatOptions, InclusiveIntegerRange, + TrackCountLimits, } from './output-format'; export { MediaSource, VideoSource, - EncodedVideoPacketSource, - VideoSampleSource, - CanvasSource, - MediaStreamVideoTrackSource, AudioSource, - EncodedAudioPacketSource, - AudioSampleSource, - AudioBufferSource, - MediaStreamAudioTrackSource, SubtitleSource, + AudioBufferSource, + AudioSampleSource, + CanvasSource, + EncodedAudioPacketSource, + EncodedVideoPacketSource, + MediaStreamAudioTrackSource, + MediaStreamVideoTrackSource, TextSubtitleSource, + VideoSampleSource, } from './media-source'; export { - VIDEO_CODECS, + MediaCodec, VideoCodec, + AudioCodec, + SubtitleCodec, + VIDEO_CODECS, + AUDIO_CODECS, PCM_AUDIO_CODECS, NON_PCM_AUDIO_CODECS, - AUDIO_CODECS, - AudioCodec, SUBTITLE_CODECS, - SubtitleCodec, - MediaCodec, - Quality, - QUALITY_VERY_LOW, - QUALITY_LOW, - QUALITY_MEDIUM, - QUALITY_HIGH, - QUALITY_VERY_HIGH, } from './codec'; export { VideoEncodingConfig, @@ -88,69 +80,110 @@ export { getFirstEncodableVideoCodec, getFirstEncodableAudioCodec, getFirstEncodableSubtitleCodec, + Quality, + QUALITY_VERY_LOW, + QUALITY_LOW, + QUALITY_MEDIUM, + QUALITY_HIGH, + QUALITY_VERY_HIGH, } from './encode'; -export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions, NullTarget } from './target'; -export { Rotation, AnyIterable, SetRequired, MaybePromise } from './misc'; +export { + Target, + BufferTarget, + NullTarget, + StreamTarget, + StreamTargetOptions, + StreamTargetChunk, +} from './target'; +export { + AnyIterable, + MaybePromise, + Rotation, + SetRequired, +} from './misc'; +export { + TrackType, + ALL_TRACK_TYPES, +} from './output'; export { Source, - BufferSource, BlobSource, BlobSourceOptions, - UrlSource, - UrlSourceOptions, + BufferSource, FilePathSource, FilePathSourceOptions, StreamSource, StreamSourceOptions, ReadableStreamSource, ReadableStreamSourceOptions, + UrlSource, + UrlSourceOptions, } from './source'; export { InputFormat, + AdtsInputFormat, IsobmffInputFormat, - Mp4InputFormat, - QuickTimeInputFormat, MatroskaInputFormat, - WebMInputFormat, Mp3InputFormat, - WaveInputFormat, + Mp4InputFormat, OggInputFormat, + QuickTimeInputFormat, + WaveInputFormat, + WebMInputFormat, ALL_FORMATS, - MP4, - QTFF, + ADTS, MATROSKA, - WEBM, MP3, - WAVE, + MP4, OGG, + QTFF, + WAVE, + WEBM, } from './input-format'; -export { Input, InputOptions } from './input'; -export { InputTrack, InputVideoTrack, InputAudioTrack, PacketStats } from './input-track'; -export { EncodedPacket, PacketType } from './packet'; export { - VideoSample, - VideoSampleInit, + Input, + InputOptions, +} from './input'; +export { + InputTrack, + InputVideoTrack, + InputAudioTrack, + PacketStats, +} from './input-track'; +export { + EncodedPacket, + PacketType, +} from './packet'; +export { AudioSample, AudioSampleInit, AudioSampleCopyToOptions, + VideoSample, + VideoSampleInit, } from './sample'; export { - PacketRetrievalOptions, - EncodedPacketSink, - BaseMediaSampleSink, - VideoSampleSink, - CanvasSinkOptions, - CanvasSink, - WrappedCanvas, - AudioSampleSink, AudioBufferSink, + AudioSampleSink, + BaseMediaSampleSink, + CanvasSink, + CanvasSinkOptions, + EncodedPacketSink, + PacketRetrievalOptions, + VideoSampleSink, WrappedAudioBuffer, + WrappedCanvas, } from './media-sink'; -export { Conversion, ConversionOptions, ConversionVideoOptions, ConversionAudioOptions } from './conversion'; +export { + Conversion, + ConversionOptions, + ConversionVideoOptions, + ConversionAudioOptions, + DiscardedTrack, +} from './conversion'; export { CustomVideoDecoder, - CustomAudioDecoder, CustomVideoEncoder, + CustomAudioDecoder, CustomAudioEncoder, registerDecoder, registerEncoder, diff --git a/src/input-format.ts b/src/input-format.ts index 1d4f17b..72f2ffc 100644 --- a/src/input-format.ts +++ b/src/input-format.ts @@ -31,6 +31,7 @@ import { readAscii } from './reader'; /** * Base class representing an input media file format. + * @group Input formats * @public */ export abstract class InputFormat { @@ -48,6 +49,7 @@ export abstract class InputFormat { /** * Format representing files compatible with the ISO base media file format (ISOBMFF), like MP4 or MOV files. + * @group Input formats * @public */ export abstract class IsobmffInputFormat extends InputFormat { @@ -75,6 +77,10 @@ export abstract class IsobmffInputFormat extends InputFormat { /** * MPEG-4 Part 14 (MP4) file format. + * + * Do not instantiate this class; use the {@link MP4} singleton instead. + * + * @group Input formats * @public */ export class Mp4InputFormat extends IsobmffInputFormat { @@ -95,6 +101,10 @@ export class Mp4InputFormat extends IsobmffInputFormat { /** * QuickTime File Format (QTFF), often called MOV. + * + * Do not instantiate this class; use the {@link QTFF} singleton instead. + * + * @group Input formats * @public */ export class QuickTimeInputFormat extends IsobmffInputFormat { @@ -115,6 +125,10 @@ export class QuickTimeInputFormat extends IsobmffInputFormat { /** * Matroska file format. + * + * Do not instantiate this class; use the {@link MATROSKA} singleton instead. + * + * @group Input formats * @public */ export class MatroskaInputFormat extends InputFormat { @@ -211,6 +225,10 @@ export class MatroskaInputFormat extends InputFormat { /** * WebM file format, based on Matroska. + * + * Do not instantiate this class; use the {@link WEBM} singleton instead. + * + * @group Input formats * @public */ export class WebMInputFormat extends MatroskaInputFormat { @@ -230,6 +248,10 @@ export class WebMInputFormat extends MatroskaInputFormat { /** * MP3 file format. + * + * Do not instantiate this class; use the {@link MP3} singleton instead. + * + * @group Input formats * @public */ export class Mp3InputFormat extends InputFormat { @@ -294,6 +316,10 @@ export class Mp3InputFormat extends InputFormat { /** * WAVE file format, based on RIFF. + * + * Do not instantiate this class; use the {@link WAVE} singleton instead. + * + * @group Input formats * @public */ export class WaveInputFormat extends InputFormat { @@ -330,6 +356,10 @@ export class WaveInputFormat extends InputFormat { /** * Ogg file format. + * + * Do not instantiate this class; use the {@link OGG} singleton instead. + * + * @group Input formats * @public */ export class OggInputFormat extends InputFormat { @@ -358,6 +388,10 @@ export class OggInputFormat extends InputFormat { /** * ADTS file format. + * + * Do not instantiate this class; use the {@link ADTS} singleton instead. + * + * @group Input formats * @public */ export class AdtsInputFormat extends InputFormat { @@ -402,41 +436,49 @@ export class AdtsInputFormat extends InputFormat { /** * MP4 input format singleton. + * @group Input formats * @public */ export const MP4 = new Mp4InputFormat(); /** * QuickTime File Format input format singleton. + * @group Input formats * @public */ export const QTFF = new QuickTimeInputFormat(); /** * Matroska input format singleton. + * @group Input formats * @public */ export const MATROSKA = new MatroskaInputFormat(); /** * WebM input format singleton. + * @group Input formats * @public */ export const WEBM = new WebMInputFormat(); /** * MP3 input format singleton. + * @group Input formats * @public */ export const MP3 = new Mp3InputFormat(); /** * WAVE input format singleton. + * @group Input formats * @public */ export const WAVE = new WaveInputFormat(); /** * Ogg input format singleton. + * @group Input formats * @public */ export const OGG = new OggInputFormat(); /** * ADTS input format singleton. + * @group Input formats * @public */ export const ADTS = new AdtsInputFormat(); @@ -444,6 +486,7 @@ export const ADTS = new AdtsInputFormat(); /** * List of all input format singletons. If you don't need to support all input formats, you should specify the * formats individually for better tree shaking. + * @group Input formats * @public */ export const ALL_FORMATS: InputFormat[] = [MP4, QTFF, MATROSKA, WEBM, WAVE, OGG, MP3, ADTS]; diff --git a/src/input-track.ts b/src/input-track.ts index bb5ec42..b093b0d 100644 --- a/src/input-track.ts +++ b/src/input-track.ts @@ -16,6 +16,7 @@ import { EncodedPacket, PacketType } from './packet'; /** * Contains aggregate statistics about the encoded packets of a track. + * @group Input files & tracks * @public */ export type PacketStats = { @@ -46,6 +47,7 @@ export interface InputTrackBacking { /** * Represents a media track in an input file. + * @group Input files & tracks * @public */ export abstract class InputTrack { @@ -71,12 +73,12 @@ export abstract class InputTrack { */ abstract determinePacketType(packet: EncodedPacket): Promise; - /** Returns true iff this track is a video track. */ + /** Returns true if and only if this track is a video track. */ isVideoTrack(): this is InputVideoTrack { return this instanceof InputVideoTrack; } - /** Returns true iff this track is an audio track. */ + /** Returns true if and only if this track is an audio track. */ isAudioTrack(): this is InputAudioTrack { return this instanceof InputAudioTrack; } @@ -92,17 +94,19 @@ export abstract class InputTrack { * * This field can be used to determine the codec of a track in case Mediabunny doesn't know that codec. * - * - For ISOBMFF files, this field returns the name of the Sample Description Box (e.g. 'avc1'). - * - For Matroska files, this field returns the value of the CodecID element. - * - For WAVE files, this field returns the value of the format tag in the 'fmt ' chunk. - * - For ADTS files, this field contains the MPEG-4 Audio Object Type. + * - For ISOBMFF files, this field returns the name of the Sample Description Box (e.g. `'avc1'`). + * - For Matroska files, this field returns the value of the `CodecID` element. + * - For WAVE files, this field returns the value of the format tag in the `'fmt '` chunk. + * - For ADTS files, this field contains the `MPEG-4 Audio Object Type`. * - In all other cases, this field is `null`. */ get internalCodecId() { return this._backing.getInternalCodecId(); } - /** The ISO 639-2/T language code for this track. If the language is unknown, this field is 'und' (undetermined). */ + /** + * The ISO 639-2/T language code for this track. If the language is unknown, this field is `'und'` (undetermined). + */ get languageCode() { return this._backing.getLanguageCode(); } @@ -189,6 +193,7 @@ export interface InputVideoTrackBacking extends InputTrackBacking { /** * Represents a video track in an input file. + * @group Input files & tracks * @public */ export class InputVideoTrack extends InputTrack { @@ -206,7 +211,7 @@ export class InputVideoTrack extends InputTrack { return 'video'; } - get codec() { + get codec(): VideoCodec | null { return this._backing.getCodec(); } @@ -252,8 +257,8 @@ export class InputVideoTrack extends InputTrack { } /** - * Returns the decoder configuration for decoding the track's packets using a VideoDecoder. Returns null if the - * track's codec is unknown. + * Returns the [decoder configuration](https://www.w3.org/TR/webcodecs/#video-decoder-config) for decoding the + * track's packets using a VideoDecoder. Returns null if the track's codec is unknown. */ getDecoderConfig() { return this._backing.getDecoderConfig(); @@ -315,6 +320,7 @@ export interface InputAudioTrackBacking extends InputTrackBacking { /** * Represents an audio track in an input file. + * @group Input files & tracks * @public */ export class InputAudioTrack extends InputTrack { @@ -347,8 +353,8 @@ export class InputAudioTrack extends InputTrack { } /** - * Returns the decoder configuration for decoding the track's packets using an AudioDecoder. Returns null if the - * track's codec is unknown. + * Returns the [decoder configuration](https://www.w3.org/TR/webcodecs/#audio-decoder-config) for decoding the + * track's packets using an AudioDecoder. Returns null if the track's codec is unknown. */ getDecoderConfig() { return this._backing.getDecoderConfig(); diff --git a/src/input.ts b/src/input.ts index 1136214..4d815e5 100644 --- a/src/input.ts +++ b/src/input.ts @@ -14,6 +14,7 @@ import { Source } from './source'; /** * The options for creating an Input object. + * @group Input files & tracks * @public */ export type InputOptions = { @@ -25,6 +26,7 @@ export type InputOptions = { /** * Represents an input media file. This is the root object from which all media read operations start. + * @group Input files & tracks * @public */ export class Input { @@ -39,6 +41,10 @@ export class Input { /** @internal */ _reader: Reader; + /** + * Creates a new input file from the specified options. No reading operations will be performed until methods are + * called on this instance. + */ constructor(options: InputOptions) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); @@ -81,9 +87,9 @@ export class Input { } /** - * Returns the format of the input file. You can compare this result directly to the InputFormat singletons or use - * `instanceof` checks for subset-aware logic (for example, `format instanceof MatroskaInputFormat` is true for - * both MKV and WebM). + * Returns the format of the input file. You can compare this result directly to the {@link InputFormat} singletons + * or use `instanceof` checks for subset-aware logic (for example, `format instanceof MatroskaInputFormat` is true + * for both MKV and WebM). */ async getFormat() { await this._getDemuxer(); @@ -112,18 +118,18 @@ export class Input { return tracks.filter(x => x.isVideoTrack()); } - /** Returns the primary video track of this input file, or null if there are no video tracks. */ - async getPrimaryVideoTrack() { - const tracks = await this.getTracks(); - return tracks.find(x => x.isVideoTrack()) ?? null; - } - /** Returns the list of all audio tracks of this input file. */ async getAudioTracks() { const tracks = await this.getTracks(); return tracks.filter(x => x.isAudioTrack()); } + /** Returns the primary video track of this input file, or null if there are no video tracks. */ + async getPrimaryVideoTrack() { + const tracks = await this.getTracks(); + return tracks.find(x => x.isVideoTrack()) ?? null; + } + /** Returns the primary audio track of this input file, or null if there are no audio tracks. */ async getPrimaryAudioTrack() { const tracks = await this.getTracks(); diff --git a/src/media-sink.ts b/src/media-sink.ts index b419e46..643f259 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -32,11 +32,12 @@ import { AudioSample, VideoSample } from './sample'; /** * Additional options for controlling packet retrieval. + * @group Media sinks * @public */ export type PacketRetrievalOptions = { /** - * When set to true, only packet metadata (like timestamp) will be retrieved - the actual packet data will not + * When set to `true`, only packet metadata (like timestamp) will be retrieved - the actual packet data will not * be loaded. */ metadataOnly?: boolean; @@ -97,12 +98,14 @@ const maybeFixPacketType = ( /** * Sink for retrieving encoded packets from an input track. + * @group Media sinks * @public */ export class EncodedPacketSink { /** @internal */ _track: InputTrack; + /** Creates a new {@link EncodedPacketSink} for the given {@link InputTrack}. */ constructor(track: InputTrack) { if (!(track instanceof InputTrack)) { throw new TypeError('track must be an InputTrack.'); @@ -342,6 +345,7 @@ abstract class DecoderWrapper< /** * Base class for decoded media sample sinks. + * @group Media sinks * @public */ export abstract class BaseMediaSampleSink< @@ -894,12 +898,14 @@ class VideoDecoderWrapper extends DecoderWrapper { /** * A sink that retrieves decoded video samples (video frames) from a video track. + * @group Media sinks * @public */ export class VideoSampleSink extends BaseMediaSampleSink { /** @internal */ _videoTrack: InputVideoTrack; + /** Creates a new {@link VideoSampleSink} for the given {@link InputVideoTrack}. */ constructor(videoTrack: InputVideoTrack) { if (!(videoTrack instanceof InputVideoTrack)) { throw new TypeError('videoTrack must be an InputVideoTrack.'); @@ -978,6 +984,7 @@ export class VideoSampleSink extends BaseMediaSampleSink { /** * A canvas with additional timing information (timestamp & duration). + * @group Media sinks * @public */ export type WrappedCanvas = { @@ -991,6 +998,7 @@ export type WrappedCanvas = { /** * Options for constructing a CanvasSink. + * @group Media sinks * @public */ export type CanvasSinkOptions = { @@ -1007,10 +1015,10 @@ export type CanvasSinkOptions = { /** * The fitting algorithm in case both width and height are set. * - * - 'fill' will stretch the image to fill the entire box, potentially altering aspect ratio. - * - 'contain' will contain the entire image within the box while preserving aspect ratio. This may lead to + * - `'fill'` will stretch the image to fill the entire box, potentially altering aspect ratio. + * - `'contain'` will contain the entire image within the box while preserving aspect ratio. This may lead to * letterboxing. - * - 'cover' will scale the image until the entire box is filled, while preserving aspect ratio. + * - `'cover'` will scale the image until the entire box is filled, while preserving aspect ratio. */ fit?: 'fill' | 'contain' | 'cover'; /** @@ -1032,7 +1040,9 @@ export type CanvasSinkOptions = { * directly retrieving frames, as it comes with common preprocessing steps such as resizing or applying rotation * metadata. * - * This sink will yield HTMLCanvasElements when in a DOM context, and OffscreenCanvases otherwise. + * This sink will yield `HTMLCanvasElement`s when in a DOM context, and `OffscreenCanvas`es otherwise. + * + * @group Media sinks * @public */ export class CanvasSink { @@ -1053,6 +1063,7 @@ export class CanvasSink { /** @internal */ _nextCanvasIndex = 0; + /** Creates a new {@link CanvasSink} for the given {@link InputVideoTrack}. */ constructor(videoTrack: InputVideoTrack, options: CanvasSinkOptions = {}) { if (!(videoTrack instanceof InputVideoTrack)) { throw new TypeError('videoTrack must be an InputVideoTrack.'); @@ -1509,12 +1520,14 @@ class PcmAudioDecoderWrapper extends DecoderWrapper { /** * Sink for retrieving decoded audio samples from an audio track. + * @group Media sinks * @public */ export class AudioSampleSink extends BaseMediaSampleSink { /** @internal */ _audioTrack: InputAudioTrack; + /** Creates a new {@link AudioSampleSink} for the given {@link InputAudioTrack}. */ constructor(audioTrack: InputAudioTrack) { if (!(audioTrack instanceof InputAudioTrack)) { throw new TypeError('audioTrack must be an InputAudioTrack.'); @@ -1595,6 +1608,7 @@ export class AudioSampleSink extends BaseMediaSampleSink { /** * An AudioBuffer with additional timing information (timestamp & duration). + * @group Media sinks * @public */ export type WrappedAudioBuffer = { @@ -1607,14 +1621,17 @@ export type WrappedAudioBuffer = { }; /** - * A sink that retrieves decoded audio samples from an audio track and converts them to AudioBuffers. This is often - * more useful than directly retrieving audio samples, as AudioBuffers can be directly used with the Web Audio API. + * A sink that retrieves decoded audio samples from an audio track and converts them to `AudioBuffer` instances. This is + * often more useful than directly retrieving audio samples, as audio buffers can be directly used with the + * Web Audio API. + * @group Media sinks * @public */ export class AudioBufferSink { /** @internal */ _audioSampleSink: AudioSampleSink; + /** Creates a new {@link AudioBufferSink} for the given {@link InputAudioTrack}. */ constructor(audioTrack: InputAudioTrack) { if (!(audioTrack instanceof InputAudioTrack)) { throw new TypeError('audioTrack must be an InputAudioTrack.'); diff --git a/src/media-source.ts b/src/media-source.ts index d8730e1..89b5485 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -41,6 +41,7 @@ import { /** * Base class for media sources. Media sources are used to add media samples to an output file. + * @group Media sources * @public */ export abstract class MediaSource { @@ -131,6 +132,7 @@ export abstract class MediaSource { /** * Base class for video sources - sources for video tracks. + * @group Media sources * @public */ export abstract class VideoSource extends MediaSource { @@ -139,6 +141,7 @@ export abstract class VideoSource extends MediaSource { /** @internal */ _codec: VideoCodec; + /** Internal constructor. */ constructor(codec: VideoCodec) { super(); @@ -152,9 +155,11 @@ export abstract class VideoSource extends MediaSource { /** * The most basic video source; can be used to directly pipe encoded packets into the output file. + * @group Media sources * @public */ export class EncodedVideoPacketSource extends VideoSource { + /** Creates a new {@link EncodedVideoPacketSource} whose packets are encoded using `codec`. */ constructor(codec: VideoCodec) { super(codec); } @@ -458,12 +463,17 @@ class VideoEncoderWrapper { /** * This source can be used to add raw, unencoded video samples (frames) to an output video track. These frames will * automatically be encoded and then piped into the output. + * @group Media sources * @public */ export class VideoSampleSource extends VideoSource { /** @internal */ private _encoder: VideoEncoderWrapper; + /** + * Creates a new {@link VideoSampleSource} whose samples are encoded according to the specified + * {@link VideoEncodingConfig}. + */ constructor(encodingConfig: VideoEncodingConfig) { validateVideoEncodingConfig(encodingConfig); @@ -493,7 +503,8 @@ export class VideoSampleSource extends VideoSource { /** * This source can be used to add video frames to the output track from a fixed canvas element. Since canvases are often - * used for rendering, this source provides a convenient wrapper around VideoSampleSource. + * used for rendering, this source provides a convenient wrapper around {@link VideoSampleSource}. + * @group Media sources * @public */ export class CanvasSource extends VideoSource { @@ -502,6 +513,10 @@ export class CanvasSource extends VideoSource { /** @internal */ private _canvas: HTMLCanvasElement | OffscreenCanvas; + /** + * Creates a new {@link CanvasSource} from a canvas element or `OffscreenCanvas` whose samples are encoded + * according to the specified {@link VideoEncodingConfig}. + */ constructor(canvas: HTMLCanvasElement | OffscreenCanvas, encodingConfig: VideoEncodingConfig) { if ( !(typeof HTMLCanvasElement !== 'undefined' && canvas instanceof HTMLCanvasElement) @@ -544,10 +559,12 @@ export class CanvasSource extends VideoSource { } /** - * Video source that encodes the frames of a MediaStreamVideoTrack and pipes them into the output. This is useful for - * capturing live or real-time data such as webcams or screen captures. Frames will automatically start being captured - * once the connected Output is started, and will keep being captured until the Output is finalized or this source - * is closed. + * Video source that encodes the frames of a + * [`MediaStreamVideoTrack`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) and pipes them into the + * output. This is useful for capturing live or real-time data such as webcams or screen captures. Frames will + * automatically start being captured once the connected {@link Output} is started, and will keep being captured until + * the {@link Output} is finalized or this source is closed. + * @group Media sources * @public */ export class MediaStreamVideoTrackSource extends VideoSource { @@ -572,6 +589,11 @@ export class MediaStreamVideoTrackSource extends VideoSource { return this._promiseWithResolvers.promise; } + /** + * Creates a new {@link MediaStreamVideoTrackSource} from a + * [`MediaStreamVideoTrack`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack), which will pull + * video samples from the stream in real time and encode them according to {@link VideoEncodingConfig}. + */ constructor(track: MediaStreamVideoTrack, encodingConfig: VideoEncodingConfig) { if (!(track instanceof MediaStreamTrack) || track.kind !== 'video') { throw new TypeError('track must be a video MediaStreamTrack.'); @@ -726,6 +748,7 @@ export class MediaStreamVideoTrackSource extends VideoSource { /** * Base class for audio sources - sources for audio tracks. + * @group Media sources * @public */ export abstract class AudioSource extends MediaSource { @@ -734,6 +757,7 @@ export abstract class AudioSource extends MediaSource { /** @internal */ _codec: AudioCodec; + /** Internal constructor. */ constructor(codec: AudioCodec) { super(); @@ -747,9 +771,11 @@ export abstract class AudioSource extends MediaSource { /** * The most basic audio source; can be used to directly pipe encoded packets into the output file. + * @group Media sources * @public */ export class EncodedAudioPacketSource extends AudioSource { + /** Creates a new {@link EncodedAudioPacketSource} whose packets are encoded using `codec`. */ constructor(codec: AudioCodec) { super(codec); } @@ -1171,12 +1197,17 @@ class AudioEncoderWrapper { /** * This source can be used to add raw, unencoded audio samples to an output audio track. These samples will * automatically be encoded and then piped into the output. + * @group Media sources * @public */ export class AudioSampleSource extends AudioSource { /** @internal */ private _encoder: AudioEncoderWrapper; + /** + * Creates a new {@link AudioSampleSource} whose samples are encoded according to the specified + * {@link AudioEncodingConfig}. + */ constructor(encodingConfig: AudioEncodingConfig) { validateAudioEncodingConfig(encodingConfig); @@ -1207,6 +1238,7 @@ export class AudioSampleSource extends AudioSource { /** * This source can be used to add audio data from an AudioBuffer to the output track. This is useful when working with * the Web Audio API. + * @group Media sources * @public */ export class AudioBufferSource extends AudioSource { @@ -1215,6 +1247,10 @@ export class AudioBufferSource extends AudioSource { /** @internal */ private _accumulatedTime = 0; + /** + * Creates a new {@link AudioBufferSource} whose `AudioBuffer` instances are encoded according to the specified + * {@link AudioEncodingConfig}. + */ constructor(encodingConfig: AudioEncodingConfig) { validateAudioEncodingConfig(encodingConfig); @@ -1250,10 +1286,12 @@ export class AudioBufferSource extends AudioSource { } /** - * Audio source that encodes the data of a MediaStreamAudioTrack and pipes it into the output. This is useful for - * capturing live or real-time audio such as microphones or audio from other media elements. Audio will automatically - * start being captured once the connected Output is started, and will keep being captured until the Output is - * finalized or this source is closed. + * Audio source that encodes the data of a + * [`MediaStreamAudioTrack`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) and pipes it into the + * output. This is useful for capturing live or real-time audio such as microphones or audio from other media elements. + * Audio will automatically start being captured once the connected {@link Output} is started, and will keep being + * captured until the {@link Output} is finalized or this source is closed. + * @group Media sources * @public */ export class MediaStreamAudioTrackSource extends AudioSource { @@ -1278,6 +1316,10 @@ export class MediaStreamAudioTrackSource extends AudioSource { return this._promiseWithResolvers.promise; } + /** + * Creates a new {@link MediaStreamAudioTrackSource} from a `MediaStreamAudioTrack`, which will pull audio samples + * from the stream in real time and encode them according to {@link AudioEncodingConfig}. + */ constructor(track: MediaStreamAudioTrack, encodingConfig: AudioEncodingConfig) { if (!(track instanceof MediaStreamTrack) || track.kind !== 'audio') { throw new TypeError('track must be an audio MediaStreamTrack.'); @@ -1572,6 +1614,7 @@ const sendMessageToMediaStreamTrackProcessorWorker = ( /** * Base class for subtitle sources - sources for subtitle tracks. + * @group Media sources * @public */ export abstract class SubtitleSource extends MediaSource { @@ -1580,6 +1623,7 @@ export abstract class SubtitleSource extends MediaSource { /** @internal */ _codec: SubtitleCodec; + /** Internal constructor. */ constructor(codec: SubtitleCodec) { super(); @@ -1593,12 +1637,14 @@ export abstract class SubtitleSource extends MediaSource { /** * This source can be used to add subtitles from a subtitle text file. + * @group Media sources * @public */ export class TextSubtitleSource extends SubtitleSource { /** @internal */ private _parser: SubtitleParser; + /** Creates a new {@link TextSubtitleSource} where added text chunks are in the specified `codec`. */ constructor(codec: SubtitleCodec) { super(codec); diff --git a/src/misc.ts b/src/misc.ts index 7d2b1c8..99aac7a 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -14,6 +14,7 @@ export function assert(x: unknown): asserts x { /** * Represents a clockwise rotation in degrees. + * @group Miscellaneous * @public */ export type Rotation = 0 | 90 | 180 | 270; @@ -355,6 +356,7 @@ export const findLastIndex = (arr: T[], predicate: (x: T) => boolean) => { /** * Sync or async iterable. + * @group Miscellaneous * @public */ export type AnyIterable = @@ -501,6 +503,7 @@ export const SECOND_TO_MICROSECOND_FACTOR = 1e6 * (1 + Number.EPSILON); /** * Sets all keys K of T to be required. + * @group Miscellaneous * @public */ export type SetRequired = T & Required>; @@ -630,6 +633,7 @@ export const isSafari = () => { /** * T or a promise that resolves to T. + * @group Miscellaneous * @public */ export type MaybePromise = T | Promise; diff --git a/src/output-format.ts b/src/output-format.ts index 0f5e228..c1a1a17 100644 --- a/src/output-format.ts +++ b/src/output-format.ts @@ -29,6 +29,7 @@ import { WaveMuxer } from './wave/wave-muxer'; /** * Specifies an inclusive range of integers. + * @group Miscellaneous * @public */ export type InclusiveIntegerRange = { @@ -40,6 +41,7 @@ export type InclusiveIntegerRange = { /** * Specifies the number of tracks (for each track type and in total) that an output format supports. + * @group Output formats * @public */ export type TrackCountLimits = { @@ -51,6 +53,7 @@ export type TrackCountLimits = { /** * Base class representing an output media file format. + * @group Output formats * @public */ export abstract class OutputFormat { @@ -97,6 +100,7 @@ export abstract class OutputFormat { /** * ISOBMFF-specific output options. + * @group Output formats * @public */ export type IsobmffOutputFormatOptions = { @@ -168,12 +172,14 @@ export type IsobmffOutputFormatOptions = { /** * Format representing files compatible with the ISO base media file format (ISOBMFF), like MP4 or MOV files. + * @group Output formats * @public */ export abstract class IsobmffOutputFormat extends OutputFormat { /** @internal */ _options: IsobmffOutputFormatOptions; + /** Internal constructor. */ constructor(options: IsobmffOutputFormatOptions = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); @@ -225,10 +231,16 @@ export abstract class IsobmffOutputFormat extends OutputFormat { } /** - * MPEG-4 Part 14 (MP4) file format. Supports all codecs except PCM audio codecs. + * MPEG-4 Part 14 (MP4) file format. Supports most codecs. + * @group Output formats * @public */ export class Mp4OutputFormat extends IsobmffOutputFormat { + /** Creates a new {@link Mp4OutputFormat} configured with the specified `options`. */ + constructor(options?: IsobmffOutputFormatOptions) { + super(options); + } + /** @internal */ get _name() { return 'MP4'; @@ -273,9 +285,15 @@ export class Mp4OutputFormat extends IsobmffOutputFormat { /** * QuickTime File Format (QTFF), often called MOV. Supports all video and audio codecs, but not subtitle codecs. + * @group Output formats * @public */ export class MovOutputFormat extends IsobmffOutputFormat { + /** Creates a new {@link MovOutputFormat} configured with the specified `options`. */ + constructor(options?: IsobmffOutputFormatOptions) { + super(options); + } + /** @internal */ get _name() { return 'MOV'; @@ -308,6 +326,7 @@ export class MovOutputFormat extends IsobmffOutputFormat { /** * Matroska-specific output options. + * @group Output formats * @public */ export type MkvOutputFormatOptions = { @@ -353,12 +372,14 @@ export type MkvOutputFormatOptions = { /** * Matroska file format. + * @group Output formats * @public */ export class MkvOutputFormat extends OutputFormat { /** @internal */ _options: MkvOutputFormatOptions; + /** Creates a new {@link MkvOutputFormat} configured with the specified `options`. */ constructor(options: MkvOutputFormatOptions = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); @@ -431,15 +452,22 @@ export class MkvOutputFormat extends OutputFormat { /** * WebM-specific output options. + * @group Output formats * @public */ export type WebMOutputFormatOptions = MkvOutputFormatOptions; /** * WebM file format, based on Matroska. + * @group Output formats * @public */ export class WebMOutputFormat extends MkvOutputFormat { + /** Creates a new {@link WebMOutputFormat} configured with the specified `options`. */ + constructor(options?: MkvOutputFormatOptions) { + super(options); + } + override getSupportedCodecs(): MediaCodec[] { return [ ...VIDEO_CODECS.filter(codec => ['vp8', 'vp9', 'av1'].includes(codec)), @@ -473,12 +501,13 @@ export class WebMOutputFormat extends MkvOutputFormat { /** * MP3-specific output options. + * @group Output formats * @public */ export type Mp3OutputFormatOptions = { /** * Controls whether the Xing header, which contains additional metadata as well as an index, is written to the start - * of the MP3 file. When disabled, the writing process becomes append-only. Defaults to true. + * of the MP3 file. When disabled, the writing process becomes append-only. Defaults to `true`. */ xingHeader?: boolean; @@ -493,12 +522,14 @@ export type Mp3OutputFormatOptions = { /** * MP3 file format. + * @group Output formats * @public */ export class Mp3OutputFormat extends OutputFormat { /** @internal */ _options: Mp3OutputFormatOptions; + /** Creates a new {@link Mp3OutputFormat} configured with the specified `options`. */ constructor(options: Mp3OutputFormatOptions = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); @@ -553,12 +584,13 @@ export class Mp3OutputFormat extends OutputFormat { /** * WAVE-specific output options. + * @group Output formats * @public */ export type WavOutputFormatOptions = { /** - * When enabled, an RF64 file be written, allowing for file sizes to exceed 4 GiB, which is otherwise not possible - * for regular WAVE files. + * When enabled, an RF64 file will be written, allowing for file sizes to exceed 4 GiB, which is otherwise not + * possible for regular WAVE files. */ large?: boolean; @@ -571,12 +603,14 @@ export type WavOutputFormatOptions = { /** * WAVE file format, based on RIFF. + * @group Output formats * @public */ export class WavOutputFormat extends OutputFormat { /** @internal */ _options: WavOutputFormatOptions; + /** Creates a new {@link WavOutputFormat} configured with the specified `options`. */ constructor(options: WavOutputFormatOptions = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); @@ -635,6 +669,7 @@ export class WavOutputFormat extends OutputFormat { /** * Ogg-specific output options. + * @group Output formats * @public */ export type OggOutputFormatOptions = { @@ -643,19 +678,21 @@ export type OggOutputFormatOptions = { * * @param data - The raw bytes. * @param position - The byte offset of the data in the file. - * @param source - The media source backing the page's logical bitstream (track). + * @param source - The {@link MediaSource} backing the page's logical bitstream (track). */ onPage?: (data: Uint8Array, position: number, source: MediaSource) => unknown; }; /** * Ogg file format. + * @group Output formats * @public */ export class OggOutputFormat extends OutputFormat { /** @internal */ _options: OggOutputFormatOptions; + /** Creates a new {@link OggOutputFormat} configured with the specified `options`. */ constructor(options: OggOutputFormatOptions = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); @@ -709,6 +746,7 @@ export class OggOutputFormat extends OutputFormat { /** * ADTS-specific output options. + * @group Output formats * @public */ export type AdtsOutputFormatOptions = { @@ -723,12 +761,14 @@ export type AdtsOutputFormatOptions = { /** * ADTS file format. + * @group Output formats * @public */ export class AdtsOutputFormat extends OutputFormat { /** @internal */ _options: AdtsOutputFormatOptions; + /** Creates a new {@link AdtsOutputFormat} configured with the specified `options`. */ constructor(options: AdtsOutputFormatOptions = {}) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); diff --git a/src/output.ts b/src/output.ts index 92c0d0f..24e71d7 100644 --- a/src/output.ts +++ b/src/output.ts @@ -15,6 +15,7 @@ import { Writer } from './writer'; /** * The options for creating an Output object. + * @group Output files * @public */ export type OutputOptions< @@ -29,11 +30,13 @@ export type OutputOptions< /** * List of all track types. + * @group Miscellaneous * @public */ export const ALL_TRACK_TYPES = ['video', 'audio', 'subtitle'] as const; /** * Union type of all track types. + * @group Miscellaneous * @public */ export type TrackType = typeof ALL_TRACK_TYPES[number]; @@ -62,6 +65,7 @@ export type OutputSubtitleTrack = OutputTrack & { type: 'subtitle' }; /** * Base track metadata, applicable to all tracks. + * @group Output files * @public */ export type BaseTrackMetadata = { @@ -73,6 +77,7 @@ export type BaseTrackMetadata = { /** * Additional metadata for video tracks. + * @group Output files * @public */ export type VideoTrackMetadata = BaseTrackMetadata & { @@ -87,11 +92,13 @@ export type VideoTrackMetadata = BaseTrackMetadata & { }; /** * Additional metadata for audio tracks. + * @group Output files * @public */ export type AudioTrackMetadata = BaseTrackMetadata & {}; /** * Additional metadata for subtitle tracks. + * @group Output files * @public */ export type SubtitleTrackMetadata = BaseTrackMetadata & {}; @@ -110,6 +117,7 @@ const validateBaseTrackMetadata = (metadata: BaseTrackMetadata) => { /** * Main class orchestrating the creation of a new media file. + * @group Output files * @public */ export class Output< @@ -138,6 +146,10 @@ export class Output< /** @internal */ _mutex = new AsyncMutex(); + /** + * Creates a new instance of {@link Output} which can then be used to create a new media file according to the + * specified {@link OutputOptions}. + */ constructor(options: OutputOptions) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); diff --git a/src/packet.ts b/src/packet.ts index ddbdc56..5221267 100644 --- a/src/packet.ts +++ b/src/packet.ts @@ -13,13 +13,17 @@ export const PLACEHOLDER_DATA = new Uint8Array(0); /** * The type of a packet. Key packets can be decoded without previous packets, while delta packets depend on previous * packets. + * @group Packets * @public */ export type PacketType = 'key' | 'delta'; /** - * Represents an encoded chunk of media. Mainly used as an expressive wrapper around WebCodecs API's EncodedVideoChunk - * and EncodedAudioChunk, but can also be used standalone. + * Represents an encoded chunk of media. Mainly used as an expressive wrapper around WebCodecs API's + * [`EncodedVideoChunk`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedVideoChunk) and + * [`EncodedAudioChunk`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedAudioChunk), but can also be used + * standalone. + * @group Packets * @public */ export class EncodedPacket { @@ -29,6 +33,7 @@ export class EncodedPacket { */ readonly byteLength: number; + /** Creates a new {@link EncodedPacket} from raw bytes and timing information. */ constructor( /** The encoded data of this packet. */ public readonly data: Uint8Array, diff --git a/src/sample.ts b/src/sample.ts index b59b332..5f407cd 100644 --- a/src/sample.ts +++ b/src/sample.ts @@ -19,10 +19,14 @@ import { /** * Metadata used for VideoSample initialization. + * @group Samples * @public */ export type VideoSampleInit = { - /** The internal pixel format in which the frame is stored. */ + /** + * The internal pixel format in which the frame is stored. + * [See pixel formats](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame/format) + */ format?: VideoPixelFormat; /** The width of the frame in pixels. */ codedWidth?: number; @@ -40,7 +44,8 @@ export type VideoSampleInit = { /** * Represents a raw, unencoded video sample (frame). Mainly used as an expressive wrapper around WebCodecs API's - * VideoFrame, but can also be used standalone. + * [`VideoFrame`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame), but can also be used standalone. + * @group Samples * @public */ export class VideoSample { @@ -49,7 +54,10 @@ export class VideoSample { /** @internal */ _closed: boolean = false; - /** The internal pixel format in which the frame is stored. */ + /** + * The internal pixel format in which the frame is stored. + * [See pixel formats](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame/format) + */ readonly format!: VideoPixelFormat | null; /** The width of the frame in pixels. */ readonly codedWidth!: number; @@ -87,8 +95,24 @@ export class VideoSample { return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration); } + /** + * Creates a new {@link VideoSample} from a + * [`VideoFrame`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame). This is essentially a near zero-cost + * wrapper around `VideoFrame`. The sample's metadata is optionally refined using the data specified in `init`. + */ constructor(data: VideoFrame, init?: VideoSampleInit); + /** + * Creates a new {@link VideoSample} from a + * [`CanvasImageSource`](https://udn.realityripple.com/docs/Web/API/CanvasImageSource), similar to the + * [`VideoFrame`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame) constructor. When `VideoFrame` is + * available, this is simply a wrapper around its constructor. If not, it will copy the source's image data to an + * internal canvas for later use. + */ constructor(data: CanvasImageSource, init: SetRequired); + /** + * Creates a new {@link VideoSample} from raw pixel data specified in `data`. Additional metadata must be provided + * in `init`. + */ constructor( data: BufferSource, init: SetRequired @@ -544,10 +568,10 @@ export class VideoSample { /** * Controls the fitting algorithm. * - * - 'fill' will stretch the image to fill the entire box, potentially altering aspect ratio. - * - 'contain' will contain the entire image within the box while preserving aspect ratio. This may lead to + * - `'fill'` will stretch the image to fill the entire box, potentially altering aspect ratio. + * - `'contain'` will contain the entire image within the box while preserving aspect ratio. This may lead to * letterboxing. - * - 'cover' will scale the image until the entire box is filled, while preserving aspect ratio. + * - `'cover'` will scale the image until the entire box is filled, while preserving aspect ratio. */ fit: 'fill' | 'contain' | 'cover'; /** A way to override rotation. Defaults to the rotation of the sample. */ @@ -596,7 +620,8 @@ export class VideoSample { } /** - * Converts this video sample to a CanvasImageSource for drawing to a canvas. + * Converts this video sample to a + * [`CanvasImageSource`](https://udn.realityripple.com/docs/Web/API/CanvasImageSource) for drawing to a canvas. * * You must use the value returned by this method immediately, as any VideoFrame created internally will * automatically be closed in the next microtask. @@ -660,12 +685,15 @@ const AUDIO_SAMPLE_FORMATS = new Set( /** * Metadata used for AudioSample initialization. + * @group Samples * @public */ export type AudioSampleInit = { /** The audio data for this sample. */ data: AllowSharedBufferSource; - /** The audio sample format. */ + /** + * The audio sample format. [See sample formats](https://developer.mozilla.org/en-US/docs/Web/API/AudioData/format) + */ format: AudioSampleFormat; /** The number of audio channels. */ numberOfChannels: number; @@ -677,6 +705,7 @@ export type AudioSampleInit = { /** * Options used for copying audio sample data. + * @group Samples * @public */ export type AudioSampleCopyToOptions = { @@ -684,7 +713,10 @@ export type AudioSampleCopyToOptions = { * The index identifying the plane to copy from. This must be 0 if using a non-planar (interleaved) output format. */ planeIndex: number; - /** The output format for the destination data. Defaults to the AudioSample's format. */ + /** + * The output format for the destination data. Defaults to the AudioSample's format. + * [See sample formats](https://developer.mozilla.org/en-US/docs/Web/API/AudioData/format) + */ format?: AudioSampleFormat; /** An offset into the source plane data indicating which frame to begin copying from. Defaults to 0. */ frameOffset?: number; @@ -696,8 +728,9 @@ export type AudioSampleCopyToOptions = { }; /** - * Represents a raw, unencoded audio sample. Mainly used as an expressive wrapper around WebCodecs API's AudioData, - * but can also be used standalone. + * Represents a raw, unencoded audio sample. Mainly used as an expressive wrapper around WebCodecs API's + * [`AudioData`](https://developer.mozilla.org/en-US/docs/Web/API/AudioData), but can also be used standalone. + * @group Samples * @public */ export class AudioSample { @@ -706,7 +739,10 @@ export class AudioSample { /** @internal */ _closed: boolean = false; - /** The audio sample format. */ + /** + * The audio sample format. + * [See sample formats](https://developer.mozilla.org/en-US/docs/Web/API/AudioData/format) + */ readonly format: AudioSampleFormat; /** The audio sample rate in hertz. */ readonly sampleRate: number; @@ -716,7 +752,7 @@ export class AudioSample { readonly numberOfFrames: number; /** The number of audio channels. */ readonly numberOfChannels: number; - /** The timestamp of the sample in seconds. */ + /** The duration of the sample in seconds. */ readonly duration: number; /** * The presentation timestamp of the sample in seconds. May be negative. Samples with negative end timestamps should @@ -734,6 +770,11 @@ export class AudioSample { return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration); } + /** + * Creates a new {@link AudioSample}, either from an existing + * [`AudioData`](https://developer.mozilla.org/en-US/docs/Web/API/AudioData) or from raw bytes specified in + * {@link AudioSampleInit}. + */ constructor(init: AudioData | AudioSampleInit) { if (isAudioData(init)) { if (init.format === null) { diff --git a/src/source.ts b/src/source.ts index 1b3d41a..ae24aaf 100644 --- a/src/source.ts +++ b/src/source.ts @@ -16,6 +16,7 @@ import { promiseWithResolvers, retriedFetch, toDataView, + toUint8Array, } from './misc'; export type ReadResult = { @@ -27,6 +28,7 @@ export type ReadResult = { /** * The source base class, representing a resource from which bytes can be read. + * @group Input sources * @public */ export abstract class Source { @@ -69,6 +71,7 @@ export abstract class Source { /** * A source backed by an ArrayBuffer or ArrayBufferView, with the entire file held in memory. + * @group Input sources * @public */ export class BufferSource extends Source { @@ -79,15 +82,16 @@ export class BufferSource extends Source { /** @internal */ _onreadCalled = false; - constructor(buffer: ArrayBuffer | Uint8Array) { - if (!(buffer instanceof ArrayBuffer) && !(buffer instanceof Uint8Array)) { - throw new TypeError('buffer must be an ArrayBuffer or Uint8Array.'); + /** Creates a new {@link BufferSource} backed the specified `ArrayBuffer` or `ArrayBufferView`. */ + constructor(buffer: ArrayBuffer | ArrayBufferView) { + if (!(buffer instanceof ArrayBuffer) && !ArrayBuffer.isView(buffer)) { + throw new TypeError('buffer must be an ArrayBuffer or ArrayBufferView.'); } super(); - this._bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); - this._view = toDataView(this._bytes); + this._bytes = toUint8Array(buffer); + this._view = toDataView(buffer); } /** @internal */ @@ -112,7 +116,8 @@ export class BufferSource extends Source { } /** - * Options for BlobSource. + * Options for {@link BlobSource}. + * @group Input sources * @public */ export type BlobSourceOptions = { @@ -121,7 +126,10 @@ export type BlobSourceOptions = { }; /** - * A source backed by a Blob. Since Files are also Blobs, this is the source to use when reading files off the disk. + * A source backed by a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). Since a + * [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) is also a `Blob`, this is the source to use when + * reading files off the disk. + * @group Input sources * @public */ export class BlobSource extends Source { @@ -130,6 +138,10 @@ export class BlobSource extends Source { /** @internal */ _orchestrator: ReadOrchestrator; + /** + * Creates a new {@link BlobSource} backed by the specified + * [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). + */ constructor(blob: Blob, options: BlobSourceOptions = {}) { if (!(blob instanceof Blob)) { throw new TypeError('blob must be a Blob.'); @@ -203,13 +215,14 @@ export class BlobSource extends Source { const URL_SOURCE_MIN_LOAD_AMOUNT = 0.5 * 2 ** 20; // 0.5 MiB /** - * Options for UrlSource. + * Options for {@link UrlSource}. + * @group Input sources * @public */ export type UrlSourceOptions = { /** - * The RequestInit used by the Fetch API. Can be used to further control the requests, such as setting - * custom headers. + * The [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) used by the Fetch API. Can be + * used to further control the requests, such as setting custom headers. */ requestInit?: RequestInit; @@ -228,6 +241,7 @@ export type UrlSourceOptions = { /** * A source backed by a URL. This is useful for reading data from the network. Requests will be made using an optimized * reading and prefetching pattern to minimize request count and latency. + * @group Input sources * @public */ export class UrlSource extends Source { @@ -245,6 +259,7 @@ export class UrlSource extends Source { abortController: AbortController; }>(); + /** Creates a new {@link UrlSource} backed by the resource at the specified URL. */ constructor( url: string | URL, options: UrlSourceOptions = {}, @@ -476,7 +491,8 @@ export class UrlSource extends Source { } /** - * Options for FilePathSource. + * Options for {@link FilePathSource}. + * @group Input sources * @public */ export type FilePathSourceOptions = { @@ -486,12 +502,14 @@ export type FilePathSourceOptions = { /** * A source backed by a path to a file. Intended for server-side usage in Node, Bun, or Deno. + * @group Input sources * @public */ export class FilePathSource extends Source { /** @internal */ _streamSource: StreamSource; + /** Creates a new {@link FilePathSource} backed by the file at the specified file path. */ constructor(filePath: string, options: BlobSourceOptions = {}) { if (typeof filePath !== 'string') { throw new TypeError('filePath must be a string.'); @@ -544,7 +562,8 @@ export class FilePathSource extends Source { } /** - * Options for defining a StreamSource. + * Options for defining a {@link StreamSource}. + * @group Input sources * @public */ export type StreamSourceOptions = { @@ -564,7 +583,7 @@ export type StreamSourceOptions = { maxCacheSize?: number; /** - * Specifies the prefetch profile that the reader should use with this source. A prefetch propfile specifies the + * Specifies the prefetch profile that the reader should use with this source. A prefetch profile specifies the * pattern with which bytes outside of the requested range are preloaded to reduce latency for future reads. * * - `'none'` (default): No prefetching; only the data needed in the moment is requested. @@ -579,6 +598,7 @@ export type StreamSourceOptions = { /** * A general-purpose, callback-driven source that can get its data from anywhere. + * @group Input sources * @public */ export class StreamSource extends Source { @@ -587,6 +607,7 @@ export class StreamSource extends Source { /** @internal */ _orchestrator: ReadOrchestrator; + /** Creates a new {@link StreamSource} whose behavior is specified by `options`. */ constructor(options: StreamSourceOptions) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); @@ -718,7 +739,8 @@ type ReadableStreamSourcePendingSlice = { }; /** - * Options for ReadableStreamSource. + * Options for {@link ReadableStreamSource}. + * @group Input sources * @public */ export type ReadableStreamSourceOptions = { @@ -727,16 +749,17 @@ export type ReadableStreamSourceOptions = { }; /** - * A source backed by a `ReadableStream` of `Uint8Array`, representing an append-only byte stream of unknown - * length. This is the source to use for incrementally streaming in input files that are still being constructed and - * whose size we don't yet know, like for example the output chunks of - * [MediaRecorder](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder). + * A source backed by a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) of + * `Uint8Array`, representing an append-only byte stream of unknown length. This is the source to use for incrementally + * streaming in input files that are still being constructed and whose size we don't yet know, like for example the + * output chunks of [MediaRecorder](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder). * * This source is *unsized*, meaning calls to `.getSize()` will throw and readers are more limited due to the * lack of random file access. You should only use this source with sequential access patterns, such as reading all * packets from start to end. This source does not work well with random access patterns unless you increase its * max cache size. * + * @group Input sources * @public */ export class ReadableStreamSource extends Source { @@ -761,6 +784,7 @@ export class ReadableStreamSource extends Source { /** @internal */ _pulling = false; + /** Creates a new {@link ReadableStreamSource} backed by the specified `ReadableStream`. */ constructor(stream: ReadableStream, options: ReadableStreamSourceOptions = {}) { if (!(stream instanceof ReadableStream)) { throw new TypeError('stream must be a ReadableStream.'); diff --git a/src/target.ts b/src/target.ts index 03366bd..3893be7 100644 --- a/src/target.ts +++ b/src/target.ts @@ -11,6 +11,7 @@ import { Output } from './output'; /** * Base class for targets, specifying where output files are written. + * @group Output targets * @public */ export abstract class Target { @@ -32,10 +33,11 @@ export abstract class Target { /** * A target that writes data directly into an ArrayBuffer in memory. Great for performance, but not suitable for very * large files. The buffer will be available once the output has been finalized. + * @group Output targets * @public */ export class BufferTarget extends Target { - /** Stores the final output buffer. Until the output is finalized, this will be null. */ + /** Stores the final output buffer. Until the output is finalized, this will be `null`. */ buffer: ArrayBuffer | null = null; /** @internal */ @@ -45,7 +47,8 @@ export class BufferTarget extends Target { } /** - * A data chunk for StreamTarget. + * A data chunk for {@link StreamTarget}. + * @group Output targets * @public */ export type StreamTargetChunk = { @@ -58,7 +61,8 @@ export type StreamTargetChunk = { }; /** - * Options for StreamTarget. + * Options for {@link StreamTarget}. + * @group Output targets * @public */ export type StreamTargetOptions = { @@ -73,9 +77,12 @@ export type StreamTargetOptions = { }; /** - * This target writes data to a WritableStream, making it a general-purpose target for writing data anywhere. It is - * also compatible with FileSystemWritableFileStream for use with the File System Access API. The WritableStream can - * also apply backpressure, which will propagate to the output and throttle the encoders. + * This target writes data to a [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream), + * making it a general-purpose target for writing data anywhere. It is also compatible with + * [`FileSystemWritableFileStream`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream) for + * use with the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API). The + * `WritableStream` can also apply backpressure, which will propagate to the output and throttle the encoders. + * @group Output targets * @public */ export class StreamTarget extends Target { @@ -84,6 +91,7 @@ export class StreamTarget extends Target { /** @internal */ _options: StreamTargetOptions; + /** Creates a new {@link StreamTarget} which writes to the specified `writable`. */ constructor( writable: WritableStream, options: StreamTargetOptions = {}, @@ -114,8 +122,9 @@ export class StreamTarget extends Target { } /** - * This target just discards all incoming data. It is useful for when you need an `Output` but extract data from it - * differently, for example through format-specific callbacks (`onMoof`, `onMdat`, ...) or encoder events. + * This target just discards all incoming data. It is useful for when you need an {@link Output} but extract data from + * it differently, for example through format-specific callbacks (`onMoof`, `onMdat`, ...) or encoder events. + * @group Output targets * @public */ export class NullTarget extends Target { diff --git a/tsdoc.json b/tsdoc.json new file mode 100644 index 0000000..e0118bc --- /dev/null +++ b/tsdoc.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], + "tagDefinitions": [ + { + "tagName": "@group", + "syntaxKind": "block" + } + ], + "supportForTags": { + "@group": true + } +} \ No newline at end of file From f13a2e6732fd0354a42c7e6e4ff74cf7617a962d Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Wed, 3 Sep 2025 18:13:42 +0200 Subject: [PATCH 2/4] Merge pull request #86 from JonnyBurger/url-source-allow-reading-small-files Do not make UrlSource fail if the size of the file is smaller than 512kb (+ add a Vitest browser mode test) --- package-lock.json | 2757 +++++++++++++++++++- package.json | 8 +- test/browser/url-source-short-file.test.ts | 19 + test/{ => node}/read-mp4.test.ts | 6 +- test/public/frames.webm | Bin 0 -> 67304 bytes test/{files => public}/video.mp4 | Bin test/vitest.config.ts | 10 + 7 files changed, 2792 insertions(+), 8 deletions(-) create mode 100644 test/browser/url-source-short-file.test.ts rename test/{ => node}/read-mp4.test.ts (84%) create mode 100644 test/public/frames.webm rename test/{files => public}/video.mp4 (100%) create mode 100644 test/vitest.config.ts diff --git a/package-lock.json b/package-lock.json index 74440a1..0ebc148 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@tailwindcss/vite": "^4.1.7", "@types/markdown-it-footnote": "^3.0.4", "@types/node": "^22.13.10", + "@vitest/browser": "3.2.4", "esbuild": "^0.25.1", "esbuild-plugin-external-global": "^1.0.1", "eslint": "^9.22.0", @@ -39,7 +40,8 @@ "vitepress": "^1.6.3", "vitepress-plugin-llms": "^1.5.1", "vitepress-plugin-mermaid": "^2.0.17", - "vitest": "3.2.4" + "vitest": "3.2.4", + "webdriverio": "9.19.2" }, "funding": { "type": "individual", @@ -324,6 +326,21 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -360,6 +377,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", @@ -1230,6 +1257,109 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1413,6 +1543,79 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@promptbook/utils": { + "version": "0.69.5", + "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", + "integrity": "sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "CC-BY-4.0", + "dependencies": { + "spacetrim": "0.11.59" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.8.tgz", + "integrity": "sha512-f02QYEnBDE0p8cteNoPYHHjbDuwyfbe4cCIVlNi8/MRicIxFW4w4CfgU0LNgWEID6s06P+hRJ1qjpBLMhPRCiQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.37.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz", @@ -2173,6 +2376,47 @@ "vite": "^5.2.0 || ^6" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/argparse": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", @@ -2180,6 +2424,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", @@ -2594,6 +2845,13 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2615,6 +2873,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.26.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", @@ -2855,6 +3141,42 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/browser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.4.tgz", + "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/user-event": "^14.6.1", + "@vitest/mocker": "3.2.4", + "@vitest/utils": "3.2.4", + "magic-string": "^0.30.17", + "sirv": "^3.0.1", + "tinyrainbow": "^2.0.0", + "ws": "^8.18.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "3.2.4", + "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -3231,6 +3553,187 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@wdio/config": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.19.2.tgz", + "integrity": "sha512-OVCzPQxav0QDk5rktQ6LYARZ5ueUuJXIqTXUpS3A9Jt6PF+ZUI5sbO/y+z+qHQXqDq+LkscmFsmkzgnoHzHcfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.18.0", + "@wdio/types": "9.19.2", + "@wdio/utils": "9.19.2", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/logger/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@wdio/protocols": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.16.2.tgz", + "integrity": "sha512-h3k97/lzmyw5MowqceAuY3HX/wGJojXHkiPXA3WlhGPCaa2h4+GovV2nJtRvknCKsE7UHA1xB5SWeI8MzloBew==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/repl": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", + "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/repl/node_modules/@types/node": { + "version": "20.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", + "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@wdio/types": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.19.2.tgz", + "integrity": "sha512-fBI7ljL+YcPXSXUhdk2+zVuz7IYP1aDMTq1eVmMme9GY0y67t0dCNPOt6xkCAEdL5dOcV6D2L1r6Cf/M2ifTvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/types/node_modules/@types/node": { + "version": "20.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", + "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@wdio/utils": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.19.2.tgz", + "integrity": "sha512-caimJiTsxDUfXn/gRAzcYTO3RydSl7XzD+QpjfWZYJjzr8a2XfNnj+Vdmr8gG4BSkiVHirW9mFCZeQp2eTD7rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.19.2", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.73", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.73.tgz", + "integrity": "sha512-I2UP8/rdQE5hTtVVL08B7P8XuwXiKuuMUPjNuFOVL/9b+8IsExR9S5jz2H58u0rJjU4M1BikLgqEMG8gZJZVBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3252,6 +3755,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -3361,6 +3874,44 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -3371,6 +3922,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3381,6 +3942,33 @@ "node": ">=12" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -3398,6 +3986,114 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.1.tgz", + "integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.2.2.tgz", + "integrity": "sha512-5vn+bdnlCYMwETIm1FqQXDP6TYPbxr2uJd88ve40kr4oPbiTZJVrTNzqA3/4sfWZeWKuQR/RkboBt7qEEDtfMA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/birpc": { "version": "0.2.19", "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", @@ -3437,6 +4133,41 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/byte-size": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-9.0.1.tgz", @@ -3707,6 +4438,23 @@ "dev": true, "license": "ISC" }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3735,6 +4483,13 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cose-base": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", @@ -3744,6 +4499,33 @@ "layout-base": "^1.0.0" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3774,6 +4556,19 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-shorthand-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", + "integrity": "sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "dev": true + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -4301,6 +5096,16 @@ "lodash-es": "^4.17.21" } }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -4325,6 +5130,19 @@ } } }, + "node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -4355,6 +5173,31 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -4398,6 +5241,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -4471,6 +5321,110 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/edge-paths": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", + "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/which": "^2.0.1", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/shirshak55" + } + }, + "node_modules/edgedriver": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.1.2.tgz", + "integrity": "sha512-UvFqd/IR81iPyWMcxXbUNi+xKWR7JjfoHjfuwjqsj9UHQKn80RpQmS0jf+U25IPi+gKVPcpOSKm0XkqgGMq4zQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.1.3", + "@zip.js/zip.js": "^2.7.53", + "decamelize": "^6.0.0", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^5.0.8", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^3.3.2", + "which": "^5.0.0" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/edgedriver/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/edgedriver/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/edgedriver/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/edgedriver/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4485,6 +5439,30 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -4601,6 +5579,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.22.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", @@ -4815,6 +5815,26 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -4851,12 +5871,40 @@ "node": ">=0.10.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -4899,6 +5947,25 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -4923,6 +5990,40 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5010,6 +6111,23 @@ "tabbable": "^6.2.0" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -5019,6 +6137,19 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -5058,6 +6189,85 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/geckodriver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-5.0.0.tgz", + "integrity": "sha512-vn7TtQ3b9VMJtVXsyWtQQl1fyBVFhQy7UvJF96kPuuJ0or5THH496AD3eUyaDD11+EqCxH9t6V+EP9soZQk4YQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.1.3", + "@zip.js/zip.js": "^2.7.53", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^3.3.2", + "tar-fs": "^3.0.6", + "which": "^5.0.0" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/geckodriver/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/geckodriver/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/geckodriver/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/geckodriver/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5068,6 +6278,35 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-tsconfig": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", @@ -5081,6 +6320,42 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5093,6 +6368,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -5113,6 +6414,13 @@ "dev": true, "license": "ISC" }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true, + "license": "MIT" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -5233,6 +6541,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/htmlfy": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.8.1.tgz", + "integrity": "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==", + "dev": true, + "license": "MIT" + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -5261,6 +6576,34 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5273,6 +6616,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5282,6 +6646,13 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5309,6 +6680,17 @@ "node": ">=8" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5318,6 +6700,13 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -5327,6 +6716,16 @@ "node": ">=12" } }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -5406,6 +6805,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-what": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", @@ -5419,12 +6831,35 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -5447,9 +6882,7 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -5503,6 +6936,52 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/juice": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/juice/-/juice-8.1.0.tgz", @@ -5600,6 +7079,52 @@ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", "dev": true }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5613,6 +7138,16 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", @@ -5869,6 +7404,28 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/locate-app": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", + "integrity": "sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@promptbook/utils": "0.69.5", + "type-fest": "4.26.0", + "userhome": "1.0.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5896,12 +7453,47 @@ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "dev": true }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true, + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -5948,6 +7540,16 @@ "node": ">=10" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -6833,6 +8435,16 @@ "pathe": "^2.0.1" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6864,6 +8476,37 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -6891,6 +8534,16 @@ "dev": true, "optional": true }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -6903,6 +8556,16 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/oniguruma-to-es": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", @@ -6962,6 +8625,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/package-manager-detector": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz", @@ -6971,6 +8675,13 @@ "quansync": "^0.2.7" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6999,6 +8710,45 @@ "parse5": "^6.0.1" } }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-data-parser": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", @@ -7029,6 +8779,30 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -7055,6 +8829,13 @@ "node": ">= 14.16" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -7262,6 +9043,61 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/property-information": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", @@ -7273,6 +9109,54 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7298,6 +9182,13 @@ } ] }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true, + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7334,6 +9225,63 @@ "node": ">=0.10.0" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", @@ -7485,6 +9433,33 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -7503,6 +9478,13 @@ "dev": true, "license": "MIT" }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "dev": true, + "license": "MIT" + }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", @@ -7591,6 +9573,57 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "dev": true }, + "node_modules/safaridriver": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.0.tgz", + "integrity": "sha512-J92IFbskyo7OYB3Dt4aTdyhag1GlInrfbPCmMteb7aBK7PwlnGz1HI0+oyNN97j7pV9DqUAVoVgkNRMrfY47mQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz", + "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7635,6 +9668,42 @@ "node": ">=10" } }, + "node_modules/serialize-error": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-12.0.0.tgz", + "integrity": "sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.31.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7680,6 +9749,34 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slick": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", @@ -7689,6 +9786,47 @@ "node": "*" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7719,6 +9857,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spacetrim": { + "version": "0.11.59", + "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.59.tgz", + "integrity": "sha512-lLYsktklSRKprreOm7NXReW8YiX2VBjbgmXYEziOoGf/qsJqAEACaDvoTtUOycwjpaSh+bT8eu0KrJn7UNxiCg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "Apache-2.0" + }, "node_modules/speakingurl": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", @@ -7752,6 +9907,16 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7773,6 +9938,30 @@ "dev": true, "license": "MIT" }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -7798,6 +9987,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -7826,6 +10031,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", @@ -7868,6 +10087,19 @@ "dev": true, "license": "MIT" }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/stylis": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", @@ -7957,6 +10189,33 @@ "node": ">=18" } }, + "node_modules/tar-fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -7967,6 +10226,16 @@ "node": ">=18" } }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -8075,6 +10344,16 @@ "dev": true, "license": "MIT" }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -8163,6 +10442,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", @@ -8206,6 +10498,16 @@ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "dev": true }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -8341,6 +10643,30 @@ "punycode": "^2.1.0" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/userhome": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", + "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", @@ -9261,6 +11587,34 @@ } } }, + "node_modules/wait-port": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wait-port/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/web-resource-inliner": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", @@ -9317,12 +11671,323 @@ "url": "https://github.com/fb55/htmlparser2?sponsor=1" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webdriver": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.19.2.tgz", + "integrity": "sha512-kw6dSwNzimU8/CkGVlM36pqWHZ7BhCwV4/d8fu6rpIYGeQbPwcNc4M90TfJuzYMA7Au3NdrwT/EVQgVLQ9Ju8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.19.2", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.16.2", + "@wdio/types": "9.19.2", + "@wdio/utils": "9.19.2", + "deepmerge-ts": "^7.0.3", + "https-proxy-agent": "^7.0.6", + "undici": "^6.21.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriver/node_modules/@types/node": { + "version": "20.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", + "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/webdriverio": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.19.2.tgz", + "integrity": "sha512-xP/9odQ9tt2pEuMgo0Oobklhu1lObgL1KmejZeyxVStwnrSTbFmn1AAqPq5pfXizUsyv2PR5+id9frrarx/c4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.19.2", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.16.2", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.19.2", + "@wdio/utils": "9.19.2", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.8.1", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^12.0.0", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.19.2" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": ">=22.x || <=24.x" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/webdriverio/node_modules/@types/node": { + "version": "20.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", + "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/webdriverio/node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/webdriverio/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/webdriverio/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/webdriverio/node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/webdriverio/node_modules/undici": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", + "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -9398,6 +12063,54 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xmldom-sre": { "version": "0.1.31", "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz", @@ -9468,6 +12181,27 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yauzl/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -9480,6 +12214,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 2514ca1..8b6e2df 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "build": "./build.sh", "watch": "tsx scripts/bundle.ts --watch", "lint": "eslint .", - "test": "vitest --run", + "test-node": "cd test && vitest node --run", + "test-web": "cd test && vitest browser --run --browser", + "test": "npm run test-node && npm run test-web", "check": "tsc -p src --noEmit && tsc -p packages/mp3-encoder/src --noEmit && tsc -p scripts --noEmit && tsc -p tsconfig.vite.json --noEmit && rm tsconfig.vite.tsbuildinfo", "check-docblocks": "tsx scripts/check-docblocks.ts dist/mediabunny.d.ts", "docs:dev": "vitepress dev docs", @@ -83,7 +85,9 @@ "vitepress": "^1.6.3", "vitepress-plugin-llms": "^1.5.1", "vitepress-plugin-mermaid": "^2.0.17", - "vitest": "3.2.4" + "vitest": "3.2.4", + "@vitest/browser": "3.2.4", + "webdriverio": "9.19.2" }, "keywords": [ "media", diff --git a/test/browser/url-source-short-file.test.ts b/test/browser/url-source-short-file.test.ts new file mode 100644 index 0000000..d9d003a --- /dev/null +++ b/test/browser/url-source-short-file.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from 'vitest'; +import { UrlSource } from '../../src/source.js'; +import { ALL_FORMATS } from '../../src/input-format.js'; +import { Input } from '../../src/input.js'; + +test('Should be able to load a very small video file via URL (<512 kB)', async () => { + const source = new UrlSource('/frames.webm'); + const input = new Input({ + source, + formats: ALL_FORMATS, + }); + const primaryVideoTrack = await input.getPrimaryVideoTrack(); + if (!primaryVideoTrack) { + throw new Error('No video track found'); + }; + + const duration = await primaryVideoTrack.computeDuration(); + expect(duration).toBeCloseTo(3.33333); +}); diff --git a/test/read-mp4.test.ts b/test/node/read-mp4.test.ts similarity index 84% rename from test/read-mp4.test.ts rename to test/node/read-mp4.test.ts index b5d0407..1332cc5 100644 --- a/test/read-mp4.test.ts +++ b/test/node/read-mp4.test.ts @@ -1,11 +1,13 @@ import { expect, test } from 'vitest'; -import { ALL_FORMATS, MP4, EncodedPacketSink, Input, FilePathSource } from '../src/index.js'; +import path from 'node:path'; +import { ALL_FORMATS, MP4, EncodedPacketSink, Input, FilePathSource } from '../../src/index.js'; const __dirname = new URL('.', import.meta.url).pathname; test('Should be able to get packets from a .MP4 file', async () => { + const filePath = path.join(__dirname, '..', 'public/video.mp4'); const input = new Input({ - source: new FilePathSource(`${__dirname}/files/video.mp4`), + source: new FilePathSource(filePath), formats: ALL_FORMATS, }); diff --git a/test/public/frames.webm b/test/public/frames.webm new file mode 100644 index 0000000000000000000000000000000000000000..7b7c0095dc668fa54c4bdcc6bb34bf7269e89931 GIT binary patch literal 67304 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

3k^*R#;0Z>tx=W zQXJ|?oR#h>lRd)<9Tq)rR2%dj+{U<4)Wq=1Q8Zu|FjQk(;WF22s4jv7RO{uyEm(M`1RS}O+!&Iaiiy!>*zjI85YF@u0x<8_Im0s^)5$6N`7{<}` z32tj``HTT=H9NqWWD<}c5%YT)k4E`7l)!l%XxfJjY!qgwy zjq~sNj1pK2gXu<2HAXs{+pP~61a(8Dl{z9KPd0Q>K;isgtdQha}TQg+$CC``=Qg@Rv$6!9~3{Y7wiOBVO=~f#=@~#)9$^dO`f1fQ6WQ96$pYt8qL2HzkfFUCi5BO=vDp5j zLgm?z)gU?uyKOS<^~BukM8M{m%fm9yZ0XIdd)>t)<3MO3+4JnY=e#+Q4!4&45HB+$ zY;U*L&-Sa~5ZNpvO?;-@ICGcf5D|*tt@e|Ex)Kf;uq>-41!hkL)xm zxya?vK8$R)Sf<3Y{XJhg>BDsI-yPBS%!a%Rl%eUpLDFkLq#HGXX^Dxunm{}IlC zGr8K3Wk^R78!Gfybj1GB_UNCb$eXO|BHjI zE6ff($aeX@S*1B=7itSzZGx5H?Go{=pGn#+&0#L6=A^gxWq4&4ggb4hR&F~X>7VOm(9d&L^@813-p zpO2`A{^SI>Y!c7Rvo6t;iDk_Wb~T!+;kge*xcnskk+@w;J*=Ni7sQ~bQARY}RmML( z`L5MTrrm&(1S@MFvIC_D=8hsEiuACL69mVF*CUx+(tf{ z2<$ghDq=4t)+K;6iKeM@I&3MOl62H_?j-{L4$JN=(%9Q8|b&pY09?GJOZbK zxmigFWLX+rGOlOv>6csPbbpP1e~G952D1L#dA@ds(w6;e5T*@*$iR9&(Hp`KH1RT4 zcAxSf_Y?Hmp$h`d5BJUD{ZP9Khul)w)95%E5i7<-s;kc;)dq z*(4p6GvpTWYJ$w5Z-k0vy!V~|rRC(%?ZNq{dLoGE>pE4LIWqBt@3!!Ybj`tUagfX|gyqgr}eG#-y4b_1(>UuO+q&C$3(i`?=fZ3Md{ zUYFlt6-A_QIn>oag3r@~BOEQG9?j}laypT?DF7HloNoF=5I*mYR=d^9&A0!%I z6LGrPj;pi1+yyTDM$Qi#!mKUVi`#Cx=49N%^qQ|Q@$qK4rG0FmoeaMyeGiA1=99jx zJIMxtHSN`5*^*~q zGA#RPzA-!MD!MMy>BDO@rc&Zu2bK|G9&T8-{)@6^(}HN9m;Gb%5(vu>3%M7mxN#u> zmNqRKTp#GFsNJ>|o2W2Cw4P*R_L^=I2Pj_k=1tP8+=M~mZ9s)FLTRfA)Cq$LD5ILZ z*&TxNEFz7W46=(0tC#g%OmT7bCjb_%_`uD($A$qDI)CDfO_QF^?Y_40ipmoMElu8{4e8W%+O z$Rp3jR1W8T+8dxz*qYG&fVwi?Rt6D4r5=Fg_XJFZJZaSu^Jk@%JaXzOfqy8g6OdTE z@kaOA7HUP%mukiVIm%o2b2^q#k?>*woZ#QcUh`*R8h^b{I11yVOYqy(2>>Sz^hR4s z(srg8#1OZu+wo8{7HeMl`I2lw;ox?%|7uspPGd8?=`{HElJPFBAPfA>4FD+mUli1k zWgR)?1SP6W(lIa@9j~DB{dm7uumB5T9FvFqEk+ih1EgxOOqDu8krmH#(|I4#%pdLHLY#xxivl~Y`k9+=~BC$%gvP69_EtX!N=BH z_@KQt`~`J&f6TWA!c{j&oHg)C6Wu$CYmgp&ey%-r$GW_59QGxR^r%Sm^}kTZ}3l564@LYt@b%_3BGyr)_UY~0RH;|hRVm0$oVcFzn^ttn9kiS8=a&AJ^DC{Mnc$2ddz*bf$J|pZRm>k80Q>*%aRgVc40ykJ zb9^eT=jY%>AxHJZjej3G+cxqEuGlRP#sBnk&P&X$f{Wh)e;V-f^D!^$TW>??;^XX< zI>gT+ZXHmPfYDE7L$Aicq>u^w5}Ko zmc*Rpb55Ekxau|lbFfn)KkA)yO4yp|frR&wR+R;X0aI__M8s^#1kIZv&4p7*5s=M2 zNxzRX^anjz!v%#A=1cx)EV$v^Rvg@T{rGS;SB(X$C;nURAgCQ3=&BoPxL>h}hjvCu z8pw-rla+aVMC`WQn)f+1re~C+lw`_1b(!P-$Z$T433Ya8qqL~;i`}00RqsM5{Fi(& zl1_e^NTucZE@kdmS}+7i)txye{nNu+1pt zY7G=NfB?!V{L-XcH3{aPyWSba^^9!&b)i&(2_V`(>|BVxvYig;i=aivNH)1s63RFu zR;KVbq3n&tvUD)YK0<>nMMd&QA449aKXJ<}_mFp6WkOKZL~)rMGeh(i1V}i?TkPzZ76@59vRFW# zI|*DrJfrbHoFY!MB<8K7FmPb4-*eHh^(Pt)q7(ronDm|(V$gKU@CYDF_Pxy2UD;^9 zvco#agxcizmXz%Y8?vlFSVDBr0KO=kn(xIwuDiL_ASL~@qXw6Uro0*b2}Hl_oM1;f zAS!>FGycf36M9VZbxy`ws1!P$Q;}*ggR3j(H8Xnp!2b9Y&Ni@`ts~mJc?Pb0htMu} zzIZp{?SUsYo~>pxT_0Iy8Qu4x+mp0wGA6<{HS0C%C*P0LjH;Ciuwk)KokD%`rMG#O zte|5LDW4c7Biy${2WQyjhoDwfQ%}1m<-sVQ(=)Co}S%jJl zy;+i~*P8+QHeEprfy4UA|LodiC3<-%3PCcaDEcbZp&)PDK5#=UpbzjJw({FsnQo1Q zTdtSX`TT0NMoonOX3ZvzpWH`D@;!w-hns`i6e0=ztVRc4PEKn6dn4xcz|Db2EoJ#H zCA3b*@UcGPnkY7NIsJ(;fp#+|G+?);`_Z><$h9^c14{-K;-dh`CdMqZs2W3t^PKT> z>a|<~AQp$fyTs@yxtX%$N-FkDD9tU1DF9U7M7h>8W;aPh<^uV=E+NqQUF_NQ+FmNipRk=d%!r| zp#$Tq_I+jTvgTQcB_(#hm4opsEsn5#jp9I(GOaNt`pMDl3f3UO62#CKE1U*wMCC7j z0^e9sk+C;}YCw{bCL`zy+0k}>8q$(7xd_{xeY(WZ?=!{`cACcCIgsR98&Ng7e5cebA zhoFLq3O$FWtwYgDZP3*}1P6U>iK1KGHMLEM9AbPF(uu7?=rj(>!7*_&7k*#ORV3xP z8ZziwJRwyiVif>a^!Eki_qHXvUj`dDMfxK~>x^F+M=`z0Z;^tR9QGV0oI6-JcUlIX z48{!b4Z^)Nk9JxK-0wb<&AOrbDuE%q|Ad%AEu$qO-ja}N+#qg%^C}C3R&%>D){h7#CRYAV6ptcKGp7JxT`q2<-9-29R6IWewzg9dV3?4zlq!27;iE! zwZO0a{U9ChpXD}5kAP|qRvu|a@W(sGyRo^>IuYSdd&+gmE&%?q0TAdlt>6p$Mr0D23e#{rdZ3$=El77PVC!um1 z6NBNAVEo#=a2eCy@uKe9SWv*KZyb0dgTE6m5-A9P`|($7XutP>{c;zkI;r zzjX$n157{fpJ`OlhJ5Fy)`#)jP_mwPTJ(0s)&js3;QVqyuyMVZ(W@nRyLS(!ss2Sb z-col^60IHukj%TZ6$C4m-e@w%7M{fa^CafwKS&zSV3Rl}g`2KuFpmuClO{#vmbeI>y%w?xOeo|&V{9o8y$C%6d&sj@we^Sm*H+l|!8_V>NQmIIw5V{a>ke#;Z;La$x z(_qGTxNG)6-f@f-pb`qbT|~EzmdT&(e|6i*6NrmhAV8{QWGcJg_&9VlpD=H?<`*jp z{zmVISM%U3WQxn+eHM9m&u&`+tIrAou0};|RC_ZFVEH1*L31c5V3c1O-O0qo7G`u> zu9OXPvZQ^W8HahssV10Yd{V50OHcIaA@Q@k-44?#!e&TxN{wzu&!Cs&1R5yhrgi zdwSnBf+^SkukkG%=j3Y`ixG?Nj!L$z;hPJDmP3eO2=!EymnY91tklW`gUC|zo{fQemaG0dmb z9OEeDl^hD{3b@(y|Q6k83Hk&F3#18U!0=JDTJef zlU2kqM9d5Q-$K&ZRK-F@VBxGSgOPYyN70yjXNEUv`qAn?j`wFxUj=aPW$evMr+epH zRNgicrG}EmS(83jra?M|7YA=(8=6A2CddSHBd8GZf@c{#|gu_tE%JXotJNN51Px3r~|Yv+)f5 zN)E@R=6-8?Uf1K;&(QMCB`D1uL;|L1{z-cW3}AROh&YKG(mptrFs1bvW=$#QDjD)# z<+>T54*G-LEaD&R92@hO5oZ-{LPnvT*{u_gyQ8p~z7E>{u>wiI?}Yc|o^%9-{RFGy zp|l@AJoV4n8MkI_pah~#?3P48abBF|NRa7uPwJfA{<`Rv(9?i+*Xj?sV$M;8Dqz~9 zlW`od#u=#cXww~l@QG!@r7J?ya&z8Jf@fBWE^f`82bWL-?e^fih9-UTFD{DK;6lrk zjfmSV&ocVtHN0a?H0mI7qv##^D-!;ucole;evE;yUKLhjw|u=a9-dN!D0%4G3u^;8 z0AQ$_-8KLxz~4E;J3Fl{9(?;*iEANlz?V;F*6d5;ApnOKlj9Qr6I<`VKT2BDH}yXm&1tX3uE}LxoGCTYDSK-e-24+uHTn5 zcq~}Q`?0al=HuP&&Ire%I}>AV&<9BJ_8FU#>J7Ag`*FF4n(L>QyvD^;1O9J`8u4TM zd;FZK`EYn}&!2(@P>AD1?uN37Hrlk*A{Kn^yH*MlUdZSmtIes5vR3F(5>U5dx zYGeG^aP@{#`En_?DdB1=h3MtlY{xyx-=Lq9f1JeMzj=No8br@^XB<`4wcR>r;I2hk z`TiA2oERo}FGd9ju7ax7AL~oTjHa zXPF1H*MI&r zJpAvUbH>v`+mE`AS5jFcYa2WULHC=Luvy};W2&>y}cO}-l^Yr;GvEm*z~#j*v3 z#2h=MY{y((9u9|6x|Wr*)U7J_l~mjDD~@Jiu>33Y?3X+IHM6To^#RV+Qc`EvD&yrb zcRI6|$|z*-i`<{ISvfGLXhXkXEiUICAd4hm7ROf+2_7}-}m}&kLtC)uF*AX)+|x4 z=a<4}?9!Y2k{327=3mb~x2~^LpFQCyjHgbRn*&O)FAfR=Z|^1Fzl?k8esYQ-!b81v{2_^iS|hB1|iIv}ee+QiHg?C>%a zY`nVcWvluL@F2W}dt5trPN6-bMd|W$iI2dOUT0Z4g1-0H>F0fNVvvn6YGsIc-4|^X zyx2|pJPWJ@eS=HsA9qj;UQF?wZV;`17Gw#?R_yAvefnfxWO=5%oPadgd;0v23EpkQ z{7Y?gfZ#sgQS5M=Zu%HJN<>20dO;5E2c~3HgOn;-5$ALTUWT8`aX0Fdqz1`RBrb+h zgwzWTHL*fZ)Td%Y@G{=y2XsAS#w+X_uqd69PD0Xpa-AXa=K&ww4=mnYx1>XWJtDGhAPDcJQ=@?=$vvGmr@gCTCzbcMw>St(EAI5qC3v2$JUObOW~V`p8ln#; z4j|nC7q_whFQpX4f_Ki31%K0BP-KgQ4Ot0>dR?18;yRM%v9%%01Xq3{va=uR{+=}n~7~r0C7DIBZE*GgmrkjfRp~d*(#BV z`y-8mF2Tz)R+3z5OISRUCS9T6OB8q08Qe{#Jb%Rot)$&`u5{2_0+)o!uY)tB9jrS@ zemBYr@i6HZcZu_Qg{~Z$tm^5b}x`yvasS)mPYsA^auG*vQ9Tf}{=9AOs?bx-nyu0I&Nd)K*AY`aX?66(USxG~zkK>S;*)QT*0=oU+Durej<(7DsMf&R~ z#qSni5UM|%H|KFE90%`;|8Tj4F>ih$qH-v80A?ChoYIDm;aji|h2m{w*v`KEF6V)M zL`AMU6X;Zhb)L{805yS&9G|@E#+Kzixg?a(6R= z41|Fi{4?$oWtFTGF?CUr=23)EaGtXL{Fj*FY0(}VQGP%NxO=-*tJEMTl&c)FMQ`I- z6j(oLdhTi5ngz02vo7EHm&~XPuaLGgrNqGZfol}_d*h*Miir%k z4%mS8z_2JerWtbuy5bTIdJfv|OGI2!KD1LGRDro2NtwdftxMnd-ci41i~*F@FHK*( zl>Oc#Lo-{fcZ}JA|NQFEaiRfZIewCj@Et)o0Lzdqx6gg}j7(_;{;My^qFv$kOMu4w z4Xb9YX*W;Jlb>v_>tTbjcIO+4+K{)x!06H^SkW_*k=6Pfdj}p~Gta;;ECw@BS?9f~ zIN#jW91+f28?Ou&6b&5uZO28mysw%2O$)X#-&N+HwMcR9DwTh-a|+BusuDsr{DQc> zV4wUtun_$PW1@p#qone6ZsVeC>PN=I3T^RUFL;8a+~+%Anry;3pYaidw~o+Ya2m@# zj>H=uv~k@X9Ix;c?(eCN>HygCzjS{E3fxgZyZKGIf+TeT^#p;Dzi@$DmjXt{d@k61X zo1&jFnq=Robld$MJ+DZ_pfz^8`OEFhw#|r~zt!r`mAhc#fk~1x-Y}Y1=i;JlN#S;L zfYwWinmuNuq^39#(ognhYeLrm`%;S33W-I6k!-7iPe;60is~~5Bn772SR?wMcjH(P zvC@AQbRRr4Suozk4qOI4d=!4V!k@D%{0u--J;`lUA>d!R$dfexcvVDv>tB`0-x=_O z3qrOg)8p2uy^OrWvg--aZ*Tau^O$Da;%Hz&09l|wt4L0|Meu2(uumgi;n$6FJfdf1PdJ-r$}Fd*%vGk&uQnC<-t%rJj8s~3yHWfbUCXD>PFf+NY>}eCs%8g>cu3Cs)XK1$OvVxJ>3Gcg|>N4h=qwq!# zpqkwt8DW<+@WL?l z-+99vw9+~&E#D6M`HBMIj_^QetV>2yqR7W%pj=Hjhj=~q*}0#{Tm{WnqRjb)=iPMZ zXnzHjLvagN+^-WdD~=5Z4JRzJ>S?IdJ@TceoO-WlCtD*l=F5O^8$5)d1gTm>KPs}^ zHJ2iBuVU#^i}`#vfj!NrqLIZ?av*vpxkeqlsv4{&vO7BbANQE+pTfl4gSxelr4u4&>$Ly^XVxG%48mn;T)f021>uP zk#Z#+rlGmn@QVsemWL{#dH9UU&Tj$Me-c3CwXZoK=>C4gZD&DMUX{A*4aQX%R>ec7 z;)^nqFaQ(-Blv&FRkYIk%R~O?Y~d73CTYa4oNUp#x~b=oOgRHj46v&00$p_HlLSm& zl5A*cKwO5L$jCWMVaLv~A$@1tFIlK?jM(;dJj(2U9rmG0&2XUv&IBrm zLQJ}-X|wQ1bhFiI_BVup)6+89*cDci$01nl_(+&IlkJIz>N=^sC*JwGP1RP?n`XWG z29PJmyk77a**Xpt#H^}ZxwKU+5jMVQ6u^{4(z)Sd8OUh#W1DwwGeozL-2fm>b8W5$ zM_@4b--?Px`>lF3T)0D~WO1YQ*emPfXBi8J$ZF})?@qmJp)HJEP8}j4;RY75_RTq} z;p3V@?E$Q;5{<9>b96eie@?u32wt0dfe6sP{|Dp~f)uOC+a?LrQxX~{YR4$`eUWH--VzZx zJA`f+uZ$Qa;j zbVn0ZdYOkLk@0ctCV4i4HEi3ov0{{;UZ*GK3*!7N>@nH~?*!+~C!kmGa7WmN6Jyod zvnKwk69=$J-nJsqwjJWX>NZL^BaK^CrmVe(O=O(Jic%vkUB<;rh)h8@Fur$T=D~VU z==rI!e)8WKz)n1$Zs+@Q8%VaZ8Bdm_cq`JO;WvISQSS}Npy?|SGZgtC1K2Btz#Plb zHq|+kBR5Xocqy>~@%v?|3qui>?3>W1%6^mam}B+u#LYPEtCsm9EzR;4MneF3wq|cl zohCEbW6UWSY^^Lgzh|pbT3U>gJuyTBx8YBif=3~Yqa^n*oe3aJ2|+*bAzPJ03&pd@ z(#yR+OEI{nm@D6VEpBqQ1^lR+xmuKoldee3VN*1579NrY!3;NR@r$*%o@`?hpo^c< zTlU5s|Mf(PP8=TfK-E-YuGQr(b;PF^% zi-w?Lb}u|>V~<;ZaR3`B1_9t${t)Q5PY3Upb`6^Eg$v@P@S7&UEVmaEO_ytiL3qk1 zhg-qjROXMoydq(zv67!~L^l$t$NGVe8*usk7wS6yTLHm&W&x=NxqeY5eY!?#6^ffp z(@CN-5C?klPe9e{3UivyYS1#BMd;RsRo{Uq&{tk&T<85rZr^ftmFke)^w2j2U+I}d zZ^e-6a?wH^zp~9v<4=cA_2~J19>Vq<7g*99K_w^PC4Dl^0ltU1BHRjI-%Scd6;+@Y zXd+F$Xeg;l>%thy!;Gq}X#7W1jam&0^d&;XM%r}b*gmfpwL#j&!<*O(dln}q6fahI z9TOpxV@&)iIZkQWy)xua$=5PA9hPkKxxx>AV8MT+$P(q^Q$RWDa9G|6$Jn2C=T;#i zW|w!(TRZW;Gs~c_bt+WHjpSK{pTkw+x=2%8(8ZknIRG(Q#PEG@B{i3xB(zq8)VlJu zi~8A4AIWjD?nH|?{7pH^q$h7s%7K+`Sck~@Ryvczos&~Thk5CpQmqltl~l$~qUfTL zcob4Vz1aAzh2Cq~Kg4?#=VRj3M1jn!oP{h?NiCsE9M-I~rjWooPPoaPZ$KBUMb7#A z0KJi7q0P7FIWh|4K@Z--!q+m=#&)GzY{4WLNd54Xu`!4)u^B1rf!7N^|(Y8=f6GNsKp61Y7MSchDTzU zjj2W!;PZ%_`Mv+tWL6dYqib4LQjb^pfScvE)W^yX*mqHi^04joR+z3}^rt|L<2zcL z@&O${9sr!m9~vqG?HH_I+MNtEun_UDiv{Gg4Rf5Mq!)S%^SV+{AeH#mVVQ+Wm_w;k zdL-}dfm+B+$uu7~U$@Ti4=9hu!Sz$klspmBdC&)DtL709iCvP)Ycc$|MW!`I|S_S{?4C8~K@eN!6jfDSvABsoUl*FOrH{z*1$u^H& za0jb>thGoe_H>o0`?T_5Ki5p2?%n|?RXjFE>+u|9{Vv|9X_6yg85`o78Jo^_7cs_3 zwwT~TcHeCu6!7tV)RO1CReh-oS$Qtl;DE_C0bSnxW&UPzD#n>`>GSko5rB0D3)dOB zg7a`gOIophobjt*|hZ5Q!C1$|7-cKsI$aiF>v^vczcF425tI%XeJ6F z0xzuf+*jW&68A6HVrK*&|M-9Z;{nAY01B<@F0_d^;aY=?%h>uD^+1pjDg%}wFunp& za0)g&W*a=&n%+!#!sK^`-%NY@kAHB7UYAjBU)_cN-J2w1MA{zp0yn{5@nf;(1LPDI zh}K_4kh(;!y&pgjKDzGDm*4 zx6m-!=S-n@U&KjQ6`jo|3{@gH2=EXq+p8iuKW>t-;i6Q|iPQ6V6u_9eezyHi>rQy8 zF1@DS;A9J$h|9>$p}2rSznN+*M{Od4Q(dg=S_j=Fb(~qM_6A(4_36Df3lH7!G3EQ^%?xk8~Reh!;WiWmeZ53tkAm289wdONt+V`nj z|H+i0Du|!lvspjwWnT7%vW^9^WzPHYTEB?4^|e~E?P%capDJLN#>CO=P7zrfB$7^D zTTi;z1WYm6&g9=n9uGQ1;QJk214<$4($`SrS&)HT6vaiq%U{x%m+}e3{NN$Zd5oFY`%O_%Z?nKTV6Hwqb_~>br+TJhlleIC zK(l5A;3}s>Rqo%FR}qaQe>Yn zS(>E_YMtdy_elq2Z}HtIBjfQQSAl`1)>qy4>R(Fz(@FMJkK?94!MCNcz2#W`yYJ)BM7Q6lo98+Za)wE znC_}b$i^m$UyHkNst}PZsZ!mYKq9Grp60ZK_Hf^Qa(aNn<7_S<}G^=g+ z{8gn!m%AGQVMJ zk1rV`Wyno9CxaxMg1&7_kv$G8~rV% ztctBED53{hxcvkR#gM^z6Im^bbg5M}T;WE&Y$QMzR^I}@U=)AHowJ0Kq)H8MVvGz1THxrehbBY58u9p~c5O!Rx0XDZ_C zw^mrR1mXtLXi3g)K#MLjJZm~^DsLxn-~H8EWcTyC^@?uflo`l#4uB&ylQ(Q@V&&UA zfhnYDdp^hR-a%-ogci4u@N;sdb!LZEC71?osqW0r@uwqjSyG;XM2FkiDiW3Fepyp9 z2Na!#_cQkSB$UaZtx-R@M)DY$rB^B4oNVlmKa#5rET2UpMJHZfa2yELTmXEv;)K@t464O6J||w2?$KGazG;9v9@u z=ahM3Mn}kF_;l)8Z#2cN$3?11~@1mL|JZ@B(5gM z9T+Txrr`AdaPM>L!tr;p7cB@?YLZEZS$;REJ}^QagWcq=jIYIOcQ}oUwnqrqNG@~f z7=(F3l>oS;;3xBzzQ+u@a9`~k95>t-4%~7 zV~rTqf0CHT7+Liaw;jcV!%Cm!F@lLDe zUFeQP_Q|mz2GBvUh+4k52_N(BlFE zgMSFjul(!1r)b``d8d10u2Z78feY2xUBH|6q~rX zixlLJEr8!P@%E|9RKxZdr`8o+`Gqd<9yGu)AEa9>#z1NGchnLD|KB*1`g@Jfob)lr zELb3t_a`QWcE1-AFzmk}`q!6>$BYN1J=&*oq!8J_y%Gd4;!EBO&sw6Vh293$CQe*8 zE=@HFOe&b2eMk4?FqyAP@WA^JnC5h64fVV~M#g1P4}i|lir&Rzqz3NkiGM-=>Eq9o z7U9*aqdAn3w7|>{%H>nZd^Ak0fD}kcQ#YUi@y26`Z^>*>wzT5B-UfOd8TT`Q1DnNO zo@QQu!bd~RO(+gTb=1z=h%dviy)gx1lNFbMZYMXPMm37iv1y3O&QL!^pjxflaqL*F z{P;tO^a}d7;a1&xLjTq-I6=w}JMgqIDkrH4BO6x1owbG0<`kC0==r~pSqNl03L>J! zZ>=O73W%iJVM=w3SJD;M`wjSo&15LhqzF_}R1e{&Hbki-vbCCbN zt^2N?%WJ0-)iz#Kv)lz5p?nv2Kf7?x(>BNx9Tf@^OynrmsU{jiB_FBvahVs+-`*&G zHG!I=c;@hxT^#>YU?{i;T%9~+v3oUqb7K9rF>QAX%Za1>;mAx_%hYQ^l$Pt{3h~(* zAZ7_*{j2r<3Np2eW4`8$dc#!A{y_vms2JX-JJi8O9C0T;A3#j8*)dl}Zx}6ZzuW(< zu>2MBR@uOycnDrfB0`t2S6g3=wN(a~?eZ7LB2=kb_Y|Qsm)8RO%KYLlZ^Act@{QxF z`EnM=JoALr0Pwhf%nB3GtU$Sr&i<*j24MlY7vzsNB!h@zjSuWb_11z6ccG6y*17|Q zrtEA;)cS)ufpb+Mwy+n+@gN42oO`yN-Qh#6%J`2i zVussFaW?egu7D6qn=Fsn8%g$e5<?=0d>A%w4- zP>ZX#F-6GQM&iOVqc$U>D@smCq2u3Kq8AvDK}@|E%dvo4-6qP4k*>d*v4>@1)0Q?= z6dH$cwX?5SD$*Xe-};Gcqw1XHKysQTp%f*;hlKKR*zwD)UY>b2fgxdqCf{7Og+60r zjB5C2HBqL9d~L-5?BH+3_eOyuFoOHtjS_Q931(M_Jp{4ZXv`nF+B|rv=Zx?TaCnKT zY*8E=gWp|gMLPMGiMqqpwCm$M#&~Gh(@~v;Ipz*MQ%Kz{w`0wc)}1GeNy| zEXO)j%#gsak)90JXN6%$_BnhCu@rXT8GJrWS^>MSHF9Azi?olZra9#2`?0(C)?2VX z=y3+g4>c=2HIOQTVA1B?bhQ)^a~=$elpm;nytROwKtMstO`fnT0G{s;vAzQ%n=sxdHU z913rc58;2+-b^x(nhebQ*S&zhRBgnCcjUv`U?C5nTbtvA(xY^n=hNZ74=qjNQ&p&O zzmkM2xOwx2cpAQ=5$dEDT+>v1t{S!<+s7B9vErAyOF~^ZVprbFxd_Mn03tF|nxDftN^3EF{|602-q8UA1rvto)Ae2Ol<_Kxkp; zx>sR4p)=GmeNnlzX%wxyk_PpZdM`SC==iS+O8EMS!DjP-G2>H^z~jOwspa|x$cooM zC?pC%;1yz_j2h1FYeW|DXThUh=gF05vg^Vv8^|Nl!_As~w|Qb0SkxleSYNU4%z7Y< zG4Cxe5Ww0=cSw^JQpM&^T_AtdS_38#Kjj2lpq;DT@F>F?ce87}-XD+oZ1np~h6ezz z_ZRX0s$+z9W4HjJ@A^$U;HrD8|2!*^&S`+t)&r@DCrn0CT0?)_KbKS(B%L1yUv?{- zx(USL{2d3nKdsEW9qRhx7PP{Q1H21b`wzFRsVp*BhCu)|DX2E978!BkDuifhTHgyigidPK2$y@B=5xg^x)%O1(*x+ z!^$uVWSkiRx3g1F0W8Q`WExWh3lm=J$F>%JC9ly({XKgb&r*}Q|KAv@Cbj@&P@mv?_7E} z3%W^s>u@X>p z=_H-z2*UcA->DEH@c?-LKTcaWaMzFG7kQ^XL)+$KjR-pz-20mxz(RFUfiEfXmf$l0 zs4Z85`HGJ5FYq#ot~qmSS}B9c_4K0~o8c{+1!9ziK(OH7!SqE5E|#V{2uN5`wIO== zw@~xg^*_#;2KE&zt@>rF-Aecj#&Yz(V`ff?4*EEhOjhKKQ3HLvhW~g*J`)l|^vAdT zx4(YSrVlGeA14zfBX>T`UQJidJQ>ntcO_9hlCV@sThYuX$-c2lL9bJaFdk{bSE7lH|7lvDndnZ{iDzVzH&3xhZGU5Ra_2f4 z2U_f-!K$4yZPq(`d0=+eBO4Aaij^A81}I*7e@r$>w_Nx;c(E@}|FP=)O&G>qBHMmp#o6ceAP_)N$^dG03NA z->KCqGU9Y8jRjcFD5*)w}E`G!lzY^Tyn-Jpto`|DIcNYtd9g?p+n zvAwlskbAqXWVGV z-5__gU?jkS6lT0j%kT5=cW4n-|7=9t@GMdzdQaEXxTnn(9BMbd$1tzWo;6nuLeB1& z71A$vPafXf;Ti(RGC6KSd7a`!Z+x4q=ZBqTz`?dWmiN1k9PT4__$B751)f_h%O(IC zZRC@+WQ>?V>FlnAPgms&4qo6j>PMR5psw@8bqju!&LklbdQ9Hf;3qhoAx#F^;4FdE zUs5KbQO8*O6)Q=|jr?$N;MKR`OsYp#&=IVGN6e7#02kKI3~dCpAAlD1}inno=Eg z!Q0zGUqE7t_NayD`WGzOJYh}%{P#a59Te!K!niT!J1}B=`|D#Q0$H?_#AShYfe+9|*TMRL)@Y`Pmo4==QJUIfYAdeV zSGvvQy!ST&k;}#?!c}mlRH50zz(b2lF7}S0yBdSrnJIfz2qGR~N0l5QfdP<~Z!N&zs1YR?s?xM&ag-j6Tu*#R3_r+2aqQ0*NB=%zrPZEZT<2ChvL{izIC}h z#&l%T<+$a!CI+L($ix}W&JJ17&o>;gZhR%;&^^}kMbaR?p7VMZiGX^Lep_9o4-Nfz z`MslfrB9Pj>?Rkvsb_eu`Vn5&td|!N>KBvUD+uX*w_MQ*Z7tIJ<#UHj@A6u86*9;l zwGvHIZciB6H5b!&oP0_q7nMF~BwG$8ebOu<%o9!q!1w;!4gIeS4Fb?a-c71a?L5nM zuy1nX3v%KMCHu-{WckLFi6!4Qe^Z9!KOj@c@AwFpmQ-BNeYn+=_I+yZsj-Vd6d2}z z!ca0mR>Gf&kb;;^o6wOqJ7c{%!VfIaP5S6Ep}X18R!O&#p@%mLIcH&%z|%WdRaU?S zf{b7qBX?>JMaj3euvTrG&G}IGOXuOWy((Hy zf(|Opr<25B3h2g*@r#VzQh!ei26j=eQvoT-`1_9P^RVKE+5_y5)Y++b_>l+9GInZ* zc>yPB=LsFTnOurbA(Lz*Y~Kz<99OxXiAS~R?_}n95LJc21|K%=n%Oxko5&RzDW@HI z`E0Yc?4HYA4S2fd(tZ$Y)B0lB!g_+pG2M>A_L%3VErRoSnLzM<=jHCWzq z1g{>G;rtx!Lf+)ZOXum0cTwQ=v5J0vIqt?K!fb{@=wJtnf2-tT-U+qCiiRjZxL*UK z%4OL1*~aV`&9wbXd;V2{L=*_of_Z_tv4%Iz1ig%%s=!pdn-3#&mx1n zYbfon8m5p*moArZwv6nL1v(d&$V&7+lt4PB#4rW&=gBeMUbq+fhhh?q0WYz5rQOA! iOoF1~Na;Xum0PXiT4QPfAdTVo0MN?-FtZB)`2PV=T>upT literal 0 HcmV?d00001 diff --git a/test/files/video.mp4 b/test/public/video.mp4 similarity index 100% rename from test/files/video.mp4 rename to test/public/video.mp4 diff --git a/test/vitest.config.ts b/test/vitest.config.ts new file mode 100644 index 0000000..90db06d --- /dev/null +++ b/test/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + browser: { + provider: 'webdriverio', + instances: [{ browser: 'chrome' }], + }, + }, +}); From 667bc6888cf3baa89833b539301f9f8390c07e07 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Wed, 3 Sep 2025 18:14:05 +0200 Subject: [PATCH 3/4] Enable ESLint as formatter (#87) * Enable ESLint as formatter I was working on a different computer today, and the formatting on save did not work out of the box for me. I had to manually enable this setting, most people have it probably but explicitly enabling it for this project makes it easier for contributors. * Update settings.json --- .vscode/settings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index baff025..7a1398e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } } From 1c14360d79cbfb8b5cf6705559060f54b871ba0b Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:15:58 +0200 Subject: [PATCH 4/4] Bump patch --- package-lock.json | 12 ++++++------ package.json | 2 +- packages/mp3-encoder/package.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ebc148..be2fc0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.13.0", + "version": "1.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.13.0", + "version": "1.13.1", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7749,9 +7749,9 @@ } }, "node_modules/mediabunny": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.12.1.tgz", - "integrity": "sha512-Jidi7oARd9/oUVg8tf8bbj5yQHTBAB2vp+iz+txQlqTFwuzqjRrij384pkTezJZRyeE1I1JKE2IY3/GQDMelfA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.13.0.tgz", + "integrity": "sha512-eAtCVOzceN3VkExQmYrDB+3bM6d98PfB6AhgueojYG6T5SNzc4cRR2gWm0oZ1HjhNN0J4hjtvtEpJ+AAMXF/dQ==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12242,7 +12242,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.13.0", + "version": "1.13.1", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index 8b6e2df..e5ae439 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.13.0", + "version": "1.13.1", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "workspaces": [ diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index e512b75..70c7ec4 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.13.0", + "version": "1.13.1", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",