| 249 | * @throws If the dist-info directory or RECORD file is not found |
| 250 | */ |
| 251 | export async function extendDistRecord( |
| 252 | sitePackagesDir: string, |
| 253 | packageName: string, |
| 254 | paths: string[] |
| 255 | ): Promise<number> { |
| 256 | const normalizedTarget = normalizePackageName(packageName); |
| 257 | |
| 258 | // Find the matching .dist-info directory |
| 259 | const entries = await readdir(sitePackagesDir); |
| 260 | const distInfoDirName = entries.find(e => { |
| 261 | if (!e.endsWith('.dist-info')) return false; |
| 262 | // The directory name format is "{name}-{version}.dist-info". |
| 263 | // Extract the name portion (everything before the last hyphen preceding a version). |
| 264 | const withoutSuffix = e.slice(0, -'.dist-info'.length); |
| 265 | const lastHyphen = withoutSuffix.lastIndexOf('-'); |
| 266 | if (lastHyphen === -1) return false; |
| 267 | const dirName = withoutSuffix.slice(0, lastHyphen); |
| 268 | return normalizePackageName(dirName) === normalizedTarget; |
| 269 | }); |
| 270 | |
| 271 | if (!distInfoDirName) { |
| 272 | throw new Error( |
| 273 | `No .dist-info directory found for package "${packageName}" in ${sitePackagesDir}` |
| 274 | ); |
| 275 | } |
| 276 | |
| 277 | const recordPath = join(sitePackagesDir, distInfoDirName, 'RECORD'); |
| 278 | |
| 279 | let existingRecord: string; |
| 280 | try { |
| 281 | existingRecord = await readFile(recordPath, 'utf-8'); |
| 282 | } catch { |
| 283 | throw new Error(`RECORD file not found in ${distInfoDirName}`); |
| 284 | } |
| 285 | |
| 286 | // Parse existing paths into a Set for deduplication |
| 287 | const existingPaths = new Set( |
| 288 | existingRecord |
| 289 | .split('\n') |
| 290 | .filter(line => line.length > 0) |
| 291 | .map(line => line.split(',')[0]) |
| 292 | ); |
| 293 | |
| 294 | const newEntries = paths.filter(p => !existingPaths.has(p)); |
| 295 | if (newEntries.length > 0) { |
| 296 | const prefix = |
| 297 | existingRecord.length > 0 && !existingRecord.endsWith('\n') ? '\n' : ''; |
| 298 | const lines: string[] = []; |
| 299 | for (const p of newEntries) { |
| 300 | const fullPath = join(sitePackagesDir, p); |
| 301 | const { hash, size } = await hashFile(fullPath); |
| 302 | lines.push(`${p},sha256=${hash},${size}`); |
| 303 | } |
| 304 | await appendFile(recordPath, prefix + lines.join('\n') + '\n'); |
| 305 | } |
| 306 | |
| 307 | return newEntries.length; |
| 308 | } |