Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at cope-settings-sync 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)