Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 315 lines 11 kB view raw
1#!/usr/bin/env -S node 2/* eslint-disable import-x/no-nodejs-modules */ 3// @ts-check 4 5// https://github.com/karlhorky/pnpm-tricks#convert-patch-package-patches-to-pnpm-patches 6 7/** 8 * Convert patch-package patches to pnpm-native patches by: 9 * 10 * 1. Group all patch-package patches by package name 11 * 2. For non-overlapping patches: strip prefixes and 12 * concatenate 13 * 3. For overlapping patches (same file in multiple patches): 14 * download the original package, apply patches sequentially, 15 * and generate a squashed diff 16 * 4. Write a patch file: patches/<@scope__name>.patch 17 * 5. Update pnpm.patchedDependencies in package.json with a 18 * version-qualified key 19 * 20 * Original patch files not deleted, to allow for comparison. 21 */ 22 23import { execSync } from 'node:child_process' 24import { readFile, writeFile, cp, rm, mkdtemp, mkdir } from 'node:fs/promises' 25import os from 'node:os' 26import path from 'node:path' 27import util from 'node:util' 28 29import glob from 'glob' 30 31const globAsync = util.promisify(glob) 32 33const rootDir = process.cwd() 34const patchesDir = path.join(rootDir, 'patches') 35const packageJsonPath = path.join(rootDir, 'package.json') 36 37const patchPackagePatchPaths = (await globAsync('*+*.patch', { cwd: patchesDir })) 38 .map( 39 (file) => path.join(patchesDir, file), 40 ) 41patchPackagePatchPaths.sort() 42 43if (patchPackagePatchPaths.length === 0) { 44 console.log(`No patch-package patches found in ${patchesDir}.`) 45 process.exit(1) 46} 47 48const packageJsonContent = /** @type {{ pnpm?: { patchedDependencies?: Record<string, string> } }} */ (JSON.parse( 49 await readFile(packageJsonPath, 'utf8'), 50)) 51packageJsonContent.pnpm ??= {} 52packageJsonContent.pnpm.patchedDependencies ??= {} 53 54/** 55 * Parse a patch-package filename to extract package info. 56 * 57 * Handles formats like: 58 * react-native+0.81.5.patch 59 * react-native+0.81.5+001+initial.patch 60 * @atproto+api+0.14.21.patch 61 * parent++child+1.0.0.patch 62 * 63 * @param {string} filePath 64 */ 65function parsePatchFilename(filePath) { 66 const base = path.basename(filePath, '.patch') 67 68 // Handle parent++leaf separation 69 let parentEncoded 70 let leafPart = base 71 const parentSepIndex = base.indexOf('++') 72 if (parentSepIndex !== -1) { 73 parentEncoded = base.slice(0, parentSepIndex) 74 leafPart = base.slice(parentSepIndex + 2) 75 } 76 77 // Split by '+' and find the version (first segment starting with a digit) 78 const segments = leafPart.split('+') 79 let versionIndex = -1 80 for (let i = 1; i < segments.length; i++) { 81 if (/^\d/.test(segments[i])) { 82 versionIndex = i 83 break 84 } 85 } 86 87 if (versionIndex < 1) { 88 return null 89 } 90 91 const encodedLeafName = segments.slice(0, versionIndex).join('+') 92 // @scope+name -> @scope/name 93 const leafPackageName = encodedLeafName.replaceAll('+', '/') 94 95 const parentPackageName = parentEncoded 96 ? parentEncoded.replaceAll('+', '/') 97 : undefined 98 99 const nodeModulesPathPrefix = ( 100 parentPackageName ? [parentPackageName, leafPackageName] : [leafPackageName] 101 ) 102 .map((segment) => `node_modules/${segment}`) 103 .join('/') 104 105 return { 106 leafPackageName, 107 encodedLeafName, 108 nodeModulesPathPrefix, 109 } 110} 111 112// Group patches by leaf package name 113/** @type {Map<string, { encodedLeafName: string, nodeModulesPathPrefix: string, paths: string[] }>} */ 114const patchGroups = new Map() 115 116for (const patchPath of patchPackagePatchPaths) { 117 const parsed = parsePatchFilename(patchPath) 118 if (!parsed) { 119 console.error( 120 `Skipping ${patchPath}: cannot parse filename`, 121 ) 122 continue 123 } 124 125 if (!patchGroups.has(parsed.leafPackageName)) { 126 patchGroups.set(parsed.leafPackageName, { ...parsed, paths: [] }) 127 } 128 patchGroups.get(parsed.leafPackageName).paths.push(patchPath) 129} 130 131/** 132 * Extract the set of files modified by a patch. 133 * @param {string} patchContent 134 * @returns {Set<string>} 135 */ 136function getModifiedFiles(patchContent) { 137 const files = new Set() 138 for (const match of patchContent.matchAll(/^diff --git a\/(.+?) b\//gm)) { 139 files.add(match[1]) 140 } 141 return files 142} 143 144/** 145 * Check if multiple patches modify any of the same files. 146 * @param {string[]} patchPaths 147 * @param {string} nodeModulesPathPrefix 148 * @returns {Promise<boolean>} 149 */ 150async function hasOverlappingFiles(patchPaths, nodeModulesPathPrefix) { 151 const allFiles = new Set() 152 for (const patchPath of patchPaths) { 153 const content = await readFile(patchPath, 'utf8') 154 const stripped = content.replaceAll(`a/${nodeModulesPathPrefix}/`, 'a/') 155 for (const file of getModifiedFiles(stripped)) { 156 if (allFiles.has(file)) return true 157 allFiles.add(file) 158 } 159 } 160 return false 161} 162 163for (const [leafPackageName, group] of patchGroups) { 164 const packageDir = path.join(rootDir, 'node_modules', ...leafPackageName.split('/')) 165 166 // Read installed version for the version-qualified key 167 let installedVersion 168 try { 169 const installedPkgJson = JSON.parse( 170 await readFile(path.join(packageDir, 'package.json'), 'utf8'), 171 ) 172 installedVersion = installedPkgJson.version 173 } catch { 174 console.warn( 175 `Could not resolve installed version for ${leafPackageName}`, 176 ) 177 } 178 179 const overlapping = group.paths.length > 1 && 180 await hasOverlappingFiles(group.paths, group.nodeModulesPathPrefix) 181 182 let pnpmPatchContent 183 184 if (!overlapping) { 185 // Simple path: strip node_modules prefix and concatenate 186 const convertedParts = [] 187 for (const patchPath of group.paths) { 188 const content = await readFile(patchPath, 'utf8') 189 const converted = content 190 .replaceAll(`a/${group.nodeModulesPathPrefix}/`, 'a/') 191 .replaceAll(`b/${group.nodeModulesPathPrefix}/`, 'b/') 192 convertedParts.push(converted) 193 } 194 pnpmPatchContent = convertedParts.join('') 195 } else { 196 // Overlapping patches: download original, apply sequentially, squash 197 if (!installedVersion) { 198 console.error( 199 `Cannot squash overlapping patches for ${leafPackageName}: unknown installed version`, 200 ) 201 continue 202 } 203 204 const pkgSegments = leafPackageName.split('/') 205 const stripLevel = 2 + pkgSegments.length 206 207 const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'pnpm-patch-')) 208 const originalDir = path.join(tmpDir, 'original') 209 const patchedDir = path.join(tmpDir, 'patched') 210 211 try { 212 // Download original package tarball 213 console.log(`Downloading ${leafPackageName}@${installedVersion}...`) 214 execSync( 215 `npm pack "${leafPackageName}@${installedVersion}" --pack-destination "${tmpDir.replaceAll('\\', '/')}"`, 216 { stdio: 'pipe' }, 217 ) 218 219 // Find the tarball (npm creates <scope-less-name>-<version>.tgz) 220 const tarballs = (await util.promisify(glob)('*.tgz', { cwd: tmpDir })) 221 if (tarballs.length === 0) { 222 throw new Error(`No tarball found after npm pack for ${leafPackageName}`) 223 } 224 const tarballPath = path.join(tmpDir, tarballs[0]).replaceAll('\\', '/') 225 226 // Extract to both original and patched dirs 227 await mkdir(originalDir, { recursive: true }) 228 await mkdir(patchedDir, { recursive: true }) 229 execSync( 230 `tar xzf "${tarballPath}" -C "${originalDir.replaceAll('\\', '/')}" --strip-components=1`, 231 { stdio: 'pipe' }, 232 ) 233 execSync( 234 `tar xzf "${tarballPath}" -C "${patchedDir.replaceAll('\\', '/')}" --strip-components=1`, 235 { stdio: 'pipe' }, 236 ) 237 238 // Apply each patch in sequence to the patched copy 239 for (const patchPath of group.paths) { 240 const gitPatchPath = patchPath.replaceAll('\\', '/') 241 try { 242 execSync( 243 `git apply -p${stripLevel} --ignore-whitespace "${gitPatchPath}"`, 244 { cwd: patchedDir, stdio: 'pipe' }, 245 ) 246 } catch (/** @type {any} */ e) { 247 const stderr = e.stderr?.toString() || '' 248 console.error(`Warning: patch may not have applied cleanly: ${path.basename(patchPath)}`) 249 console.error(stderr) 250 // Try with more lenient settings 251 execSync( 252 `git apply -p${stripLevel} --ignore-whitespace -C0 "${gitPatchPath}"`, 253 { cwd: patchedDir, stdio: 'pipe' }, 254 ) 255 } 256 } 257 258 // Generate a single squashed diff 259 const gitOriginalDir = originalDir.replaceAll('\\', '/') 260 const gitPatchedDir = patchedDir.replaceAll('\\', '/') 261 let diff 262 try { 263 execSync( 264 `git diff --no-ext-diff --no-index -- "${gitOriginalDir}" "${gitPatchedDir}"`, 265 { encoding: 'utf8', stdio: 'pipe', maxBuffer: 50 * 1024 * 1024 }, 266 ) 267 console.log(`No differences found for ${leafPackageName}, skipping`) 268 continue 269 } catch (/** @type {any} */ e) { 270 if (e.status === 1) { 271 diff = /** @type {string} */ (e.stdout) 272 } else { 273 throw e 274 } 275 } 276 277 // Replace temp dir paths with standard a/ b/ prefixes 278 pnpmPatchContent = diff 279 .replaceAll(`a/${gitOriginalDir}/`, 'a/') 280 .replaceAll(`b/${gitPatchedDir}/`, 'b/') 281 .replaceAll(`${gitOriginalDir}/`, '') 282 .replaceAll(`${gitPatchedDir}/`, '') 283 } finally { 284 await rm(tmpDir, { recursive: true, force: true }) 285 } 286 } 287 288 // @scope+name -> @scope__name 289 const pnpmPatchPath = path.join( 290 patchesDir, 291 `${group.encodedLeafName.replaceAll('+', '__')}.patch`, 292 ) 293 await writeFile(pnpmPatchPath, pnpmPatchContent) 294 295 const patchedDepKey = installedVersion 296 ? `${leafPackageName}@${installedVersion}` 297 : leafPackageName 298 299 packageJsonContent.pnpm.patchedDependencies[patchedDepKey] = path 300 .relative(rootDir, pnpmPatchPath) 301 .replaceAll('\\', '/') 302 303 const patchNames = group.paths.map((p) => path.relative(rootDir, p)) 304 console.log( 305 `${overlapping ? 'Squashed' : 'Converted'} ${group.paths.length} patch(es) for ${leafPackageName}: ${patchNames.join(', ')}`, 306 ) 307} 308 309await writeFile( 310 packageJsonPath, 311 JSON.stringify(packageJsonContent, null, 2) + '\n', 312) 313console.log( 314 "All patches squashed to pnpm patches. Run 'pnpm install' to apply.", 315)