import { createHash } from 'node:crypto'; import { access, mkdir, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { collectDirectoryFiles, createDeterministicZip, createThirdPartyLicenseBundle, } from './release-archive.mjs'; const modulePath = fileURLToPath(import.meta.url); const defaultProjectRoot = path.resolve(path.dirname(modulePath), '..'); export async function collectReleaseMetadataFiles(projectRoot) { const files = {}; for (const fileName of [ 'CHANGELOG.md', 'LICENSE', 'README.md', 'SOURCE.md', 'THIRD_PARTY_NOTICES.md', ]) { files[fileName] = await readFile(path.join(projectRoot, fileName)); } for (const licenseName of [ 'AGPL-3.0.txt', 'MPL-2.0.txt', 'lzxd-MIT.txt', 'rust-cab-MIT.txt', ]) { files[`LICENSES/${licenseName}`] = await readFile( path.join(projectRoot, 'LICENSES', licenseName) ); } files['LICENSES/THIRD-PARTY-LICENSES.txt'] = await createThirdPartyLicenseBundle(projectRoot, [ '@add-ideas/toolbox-contract', '@add-ideas/toolbox-shell-react', 'fflate', 'react', 'react-dom', 'scheduler', ]); for (const documentName of [ 'FORMAT_SUPPORT.md', 'PORTING.md', 'REFERENCE_IMPLEMENTATIONS.md', 'SECURITY.md', ]) { files[`docs/${documentName}`] = await readFile( path.join(projectRoot, 'docs', documentName) ); } files['tests/fixtures/README.md'] = await readFile( path.join(projectRoot, 'tests', 'fixtures', 'README.md') ); return files; } async function packageRelease(projectRoot, arguments_) { const packageJson = JSON.parse( await readFile(path.join(projectRoot, 'package.json'), 'utf8') ); const releaseDirectory = path.join(projectRoot, 'release'); const archiveName = `onenote-tools-${packageJson.version}.zip`; const archivePath = path.join(releaseDirectory, archiveName); const checksumPath = path.join( releaseDirectory, `onenote-tools-${packageJson.version}.sha256` ); const force = arguments_.includes('--force'); const unknownArguments = arguments_.filter( (argument) => argument !== '--force' ); if (unknownArguments.length > 0) { throw new Error(`Unknown release argument: ${unknownArguments.join(', ')}`); } await mkdir(releaseDirectory, { recursive: true }); if (!force) { await assertDoesNotExist(archivePath, projectRoot); await assertDoesNotExist(checksumPath, projectRoot); } const files = await collectDirectoryFiles(path.join(projectRoot, 'dist')); if (Object.keys(files).some((fileName) => fileName.endsWith('.wasm'))) { throw new Error('Release input contains a forbidden WebAssembly file.'); } Object.assign(files, await collectReleaseMetadataFiles(projectRoot)); const archive = createDeterministicZip(files); const checksum = createHash('sha256').update(archive).digest('hex'); const writeFlag = force ? 'w' : 'wx'; await writeFile(archivePath, archive, { flag: writeFlag }); await writeFile(checksumPath, `${checksum} ${archiveName}\n`, { encoding: 'utf8', flag: writeFlag, }); console.log(`Created release/${archiveName}`); console.log(`SHA-256 ${checksum}`); } async function assertDoesNotExist(outputPath, projectRoot) { try { await access(outputPath); } catch (error) { if (error && typeof error === 'object' && error.code === 'ENOENT') return; throw error; } throw new Error( `${path.relative(projectRoot, outputPath)} already exists; pass --force to replace this exact version.` ); } if ( process.argv[1] !== undefined && path.resolve(process.argv[1]) === modulePath ) { await packageRelease(defaultProjectRoot, process.argv.slice(2)); }