import { createHash } from 'node:crypto'; import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); const expectedVersion = '0.12.10'; const outputRoot = join( repositoryRoot, 'public', 'vendor', 'ffmpeg', expectedVersion ); const packages = [ { packageName: '@ffmpeg/core', mode: 'st', files: ['ffmpeg-core.js', 'ffmpeg-core.wasm'], }, { packageName: '@ffmpeg/core-mt', mode: 'mt', files: ['ffmpeg-core.js', 'ffmpeg-core.wasm', 'ffmpeg-core.worker.js'], }, ]; const expectedHashes = new Map([ [ '@ffmpeg/core:ffmpeg-core.js', '67a48f11645f85439f3fde4f2119042c16b374b910206b7a7a24f342e28dcae3', ], [ '@ffmpeg/core:ffmpeg-core.wasm', '9f57947a5bd530d8f00c5b3f2cb2a3492faa7e5d823315342d6a8656d0a6b7b7', ], [ '@ffmpeg/core-mt:ffmpeg-core.js', '270a2e6ff945e173238610669a3f7132df5f9c52698a9bf708cf5c2ab6bda0de', ], [ '@ffmpeg/core-mt:ffmpeg-core.wasm', 'be2c97605366b78f3f13e21b52e81a55a79e1f29c133b03a68ec187b1a2ec41a', ], [ '@ffmpeg/core-mt:ffmpeg-core.worker.js', 'f77898d631dc010b45c29c23cb4379c611a7d7b131bf591d08a656bb729a4ca3', ], ]); async function loadPackageVersion(packageName) { const packagePath = join( repositoryRoot, 'node_modules', ...packageName.split('/'), 'package.json' ); const parsed = JSON.parse(await readFile(packagePath, 'utf8')); if (parsed.version !== expectedVersion) { throw new Error( `${packageName} ${parsed.version ?? ''} is installed; expected exactly ${expectedVersion}` ); } } async function copyAsset(packageName, mode, fileName) { const source = join( repositoryRoot, 'node_modules', ...packageName.split('/'), 'dist', 'esm', fileName ); const destination = join(outputRoot, mode, fileName); const sourceBytes = await readFile(source).catch((error) => { throw new Error(`Required FFmpeg asset is missing: ${source}`, { cause: error, }); }); const sourceDigest = createHash('sha256').update(sourceBytes).digest('hex'); const expectedDigest = expectedHashes.get(`${packageName}:${fileName}`); if (!expectedDigest || sourceDigest !== expectedDigest) { throw new Error( `Pinned FFmpeg asset checksum mismatch for ${packageName}/${fileName}` ); } await mkdir(dirname(destination), { recursive: true }); await cp(source, destination, { dereference: true, errorOnExist: false, force: true, preserveTimestamps: false, }); const destinationBytes = await readFile(destination); if (!sourceBytes.equals(destinationBytes)) { throw new Error(`Copied FFmpeg asset differs from its source: ${fileName}`); } return { path: relative(outputRoot, destination).replaceAll('\\', '/'), bytes: destinationBytes.byteLength, sha256: sourceDigest, }; } await rm(outputRoot, { force: true, recursive: true }); const assets = []; for (const entry of packages) { await loadPackageVersion(entry.packageName); for (const fileName of entry.files) { assets.push(await copyAsset(entry.packageName, entry.mode, fileName)); } } assets.sort((left, right) => left.path.localeCompare(right.path, 'en')); const versionManifest = { schemaVersion: 1, coreVersion: expectedVersion, packages: { '@ffmpeg/core': expectedVersion, '@ffmpeg/core-mt': expectedVersion, }, assets, }; await writeFile( join(outputRoot, 'version.json'), `${JSON.stringify(versionManifest, null, 2)}\n` ); console.log( `Copied ${assets.length} pinned FFmpeg assets to ${relative(repositoryRoot, outputRoot)}` );