Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions packages/vxrn/src/utils/patches.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import FSExtra from 'fs-extra'
import { join } from 'node:path'
import { mkdtemp, rm, symlink } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

// mock heavy deps that aren't needed for patch resolution tests
vi.mock('@vxrn/compiler', () => ({ transformSWC: vi.fn() }))
vi.mock('@vxrn/vite-flow', () => ({ transformFlowBabel: vi.fn() }))

import {
type DepPatch,
applyDependencyPatches,
moduleToPnpmStorePattern,
} from './patches'

// helper to create a minimal package.json
function makePkg(name: string, version: string, extras?: Record<string, any>) {
return JSON.stringify({ name, version, ...extras }, null, 2)
}

describe('moduleToPnpmStorePattern', () => {
it('converts scoped package names', () => {
expect(moduleToPnpmStorePattern('@react-navigation/core')).toBe(
'@react-navigation+core'
)
})

it('converts other scoped packages', () => {
expect(moduleToPnpmStorePattern('@expo/cli')).toBe('@expo+cli')
})

it('passes through unscoped packages unchanged', () => {
expect(moduleToPnpmStorePattern('react-native-web')).toBe('react-native-web')
})
})

describe('applyDependencyPatches', () => {
let tmpDir: string

beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'vxrn-patch-test-'))
})

afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true })
})

// helper to set up node_modules structure and run patches
async function setupAndPatch(opts: {
patches: DepPatch[]
setupFs: (nmDir: string) => Promise<void>
}) {
const nmDir = join(tmpDir, 'node_modules')
await FSExtra.ensureDir(nmDir)
await opts.setupFs(nmDir)
await applyDependencyPatches(opts.patches, { root: tmpDir })
return nmDir
}

it('discovers packages in pnpm content-addressable store (non-hoisted)', async () => {
const patches: DepPatch[] = [
{
module: '@react-navigation/core',
patchFiles: {
'package.json': (contents) => {
if (!contents) return
const pkg = JSON.parse(contents)
pkg.exports = pkg.exports || {}
pkg.exports['./lib/module/NavigationBuilderContext'] =
'./lib/module/NavigationBuilderContext.js'
return JSON.stringify(pkg, null, 2)
},
},
},
]

const nmDir = await setupAndPatch({
patches,
setupFs: async (nm) => {
// create pnpm store entry (package NOT hoisted to top-level or .pnpm/node_modules)
const storePkgDir = join(
nm,
'.pnpm',
'@react-navigation+core@7.16.1_react@19.0.0',
'node_modules',
'@react-navigation',
'core'
)
await FSExtra.ensureDir(storePkgDir)
await FSExtra.writeFile(
join(storePkgDir, 'package.json'),
makePkg('@react-navigation/core', '7.16.1')
)
},
})

// verify the patch was applied in the store
const patchedPkg = await FSExtra.readJSON(
join(
nmDir,
'.pnpm',
'@react-navigation+core@7.16.1_react@19.0.0',
'node_modules',
'@react-navigation',
'core',
'package.json'
)
)
expect(patchedPkg.exports['./lib/module/NavigationBuilderContext']).toBe(
'./lib/module/NavigationBuilderContext.js'
)
})

it('deduplicates symlink and store entry pointing to same files', async () => {
let patchCallCount = 0

const patches: DepPatch[] = [
{
module: 'test-pkg',
patchFiles: {
'index.js': (contents) => {
patchCallCount++
return (contents || '') + '\n// patched'
},
},
},
]

await setupAndPatch({
patches,
setupFs: async (nm) => {
// create the real package in pnpm store
const storeDir = join(
nm,
'.pnpm',
'test-pkg@1.0.0',
'node_modules',
'test-pkg'
)
await FSExtra.ensureDir(storeDir)
await FSExtra.writeFile(join(storeDir, 'package.json'), makePkg('test-pkg', '1.0.0'))
await FSExtra.writeFile(join(storeDir, 'index.js'), 'module.exports = {}')

// create symlink at top-level node_modules (like pnpm does when hoisted)
const symlinkTarget = join(nm, 'test-pkg')
await symlink(storeDir, symlinkTarget, 'dir')

// also create .pnpm/node_modules hoisted symlink
const pnpmHoisted = join(nm, '.pnpm', 'node_modules', 'test-pkg')
await FSExtra.ensureDir(join(nm, '.pnpm', 'node_modules'))
await symlink(storeDir, pnpmHoisted, 'dir')
},
})

// the non-idempotent patch should have been called exactly once due to dedup
expect(patchCallCount).toBe(1)
})

it('patches flat node_modules (non-pnpm regression)', async () => {
const patches: DepPatch[] = [
{
module: 'test-pkg',
patchFiles: {
'index.js': (contents) => {
return (contents || '') + '\n// patched'
},
},
},
]

const nmDir = await setupAndPatch({
patches,
setupFs: async (nm) => {
// standard flat node_modules layout, no .pnpm
const pkgDir = join(nm, 'test-pkg')
await FSExtra.ensureDir(pkgDir)
await FSExtra.writeFile(join(pkgDir, 'package.json'), makePkg('test-pkg', '1.0.0'))
await FSExtra.writeFile(join(pkgDir, 'index.js'), 'module.exports = {}')
},
})

const content = await FSExtra.readFile(join(nmDir, 'test-pkg', 'index.js'), 'utf-8')
expect(content).toContain('// patched')
})

it('respects version constraints across multiple store versions', async () => {
const patches: DepPatch[] = [
{
module: 'test-pkg',
patchFiles: {
version: '1.*',
'index.js': () => '// v1 patched',
},
},
]

await setupAndPatch({
patches,
setupFs: async (nm) => {
// v1 store entry
const v1Dir = join(nm, '.pnpm', 'test-pkg@1.0.0', 'node_modules', 'test-pkg')
await FSExtra.ensureDir(v1Dir)
await FSExtra.writeFile(join(v1Dir, 'package.json'), makePkg('test-pkg', '1.0.0'))
await FSExtra.writeFile(join(v1Dir, 'index.js'), 'module.exports = "v1"')

// v2 store entry
const v2Dir = join(nm, '.pnpm', 'test-pkg@2.0.0', 'node_modules', 'test-pkg')
await FSExtra.ensureDir(v2Dir)
await FSExtra.writeFile(join(v2Dir, 'package.json'), makePkg('test-pkg', '2.0.0'))
await FSExtra.writeFile(join(v2Dir, 'index.js'), 'module.exports = "v2"')
},
})

const nmDir = join(tmpDir, 'node_modules')

// v1 should be patched
const v1Content = await FSExtra.readFile(
join(nmDir, '.pnpm', 'test-pkg@1.0.0', 'node_modules', 'test-pkg', 'index.js'),
'utf-8'
)
expect(v1Content).toBe('// v1 patched')

// v2 should NOT be patched
const v2Content = await FSExtra.readFile(
join(nmDir, '.pnpm', 'test-pkg@2.0.0', 'node_modules', 'test-pkg', 'index.js'),
'utf-8'
)
expect(v2Content).toBe('module.exports = "v2"')
})

it('deduplicates when hoisted symlink and store entry coexist', async () => {
let patchCallCount = 0

const patches: DepPatch[] = [
{
module: 'test-pkg',
patchFiles: {
'index.js': (contents) => {
patchCallCount++
return (contents || '') + '\n// marker'
},
},
},
]

const nmDir = await setupAndPatch({
patches,
setupFs: async (nm) => {
// real package in store
const storeDir = join(
nm,
'.pnpm',
'test-pkg@1.0.0',
'node_modules',
'test-pkg'
)
await FSExtra.ensureDir(storeDir)
await FSExtra.writeFile(join(storeDir, 'package.json'), makePkg('test-pkg', '1.0.0'))
await FSExtra.writeFile(join(storeDir, 'index.js'), 'module.exports = {}')

// hoisted symlink in .pnpm/node_modules
await FSExtra.ensureDir(join(nm, '.pnpm', 'node_modules'))
await symlink(storeDir, join(nm, '.pnpm', 'node_modules', 'test-pkg'), 'dir')
},
})

expect(patchCallCount).toBe(1)

// verify the file was actually patched
const content = await FSExtra.readFile(
join(
nmDir,
'.pnpm',
'test-pkg@1.0.0',
'node_modules',
'test-pkg',
'index.js'
),
'utf-8'
)
expect(content).toContain('// marker')
})
})
57 changes: 54 additions & 3 deletions packages/vxrn/src/utils/patches.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { transformSWC } from '@vxrn/compiler'
import { transformFlowBabel } from '@vxrn/vite-flow'
import Glob from 'fast-glob'
import findNodeModules from 'find-node-modules'
import FSExtra from 'fs-extra'
import { realpathSync } from 'node:fs'
import { join } from 'node:path'
import { rename } from 'node:fs/promises'
import semver from 'semver'
Expand Down Expand Up @@ -93,22 +95,58 @@ async function writePatchStats(nodeModulesDir: string, stats: PatchStats) {

type PatchResult = 'applied' | 'ok' | 'skipped'

/**
* Convert a module name to a pnpm content-addressable store glob pattern.
* Scoped packages: @scope/name -> @scope+name
* Unscoped packages pass through unchanged.
*/
export function moduleToPnpmStorePattern(moduleName: string): string {
return moduleName.replace('/', '+')
}

export async function applyDependencyPatches(
patches: DepPatch[],
{ root = process.cwd() }: { root?: string } = {}
) {
// collect unique module names for pnpm store globbing
const patchModuleNames = [...new Set(patches.map((p) => p.module))]

const nodeModulesDirs = findNodeModules({ cwd: root }).flatMap((relativePath) => {
const dir = join(root, relativePath)
const dirs = [dir]

const pnpmBase = join(dir, '.pnpm')
if (!FSExtra.existsSync(pnpmBase)) return dirs

// pnpm hoists transitive deps into .pnpm/node_modules/ (symlinks to store)
// so we need to patch there too
const pnpmDir = join(dir, '.pnpm', 'node_modules')
return FSExtra.existsSync(pnpmDir) ? [dir, pnpmDir] : [dir]
const pnpmHoistedDir = join(pnpmBase, 'node_modules')
if (FSExtra.existsSync(pnpmHoistedDir)) {
dirs.push(pnpmHoistedDir)
}

// pnpm content-addressable store: scan for store entries matching our patch modules
// handles cases where packages aren't hoisted (strict mode, peer dep conflicts, hoist=false)
for (const moduleName of patchModuleNames) {
const storePattern = moduleToPnpmStorePattern(moduleName)
const storeMatches = Glob.sync(`${storePattern}@*/node_modules`, {
cwd: pnpmBase,
onlyDirectories: true,
})
for (const match of storeMatches) {
dirs.push(join(pnpmBase, match))
}
}

return dirs
})

// track results per module
const results = new Map<string, PatchResult>()
// track modules that already warned about transform failures (to avoid spam)
const transformWarnedModules = new Set<string>()
// track real paths per (patch index, real dir) to prevent double-patching
// when symlinks and store entries resolve to the same physical files
const patchedRealPaths = new Set<string>()

await Promise.all(
nodeModulesDirs.map(async (nodeModulesDir) => {
Expand All @@ -123,6 +161,19 @@ export async function applyDependencyPatches(

if (!FSExtra.existsSync(nodeModuleDir)) return

// deduplicate by real path to avoid patching the same files twice
// (multiple symlinks/paths can resolve to the same pnpm store entry)
let realModuleDir: string
try {
realModuleDir = realpathSync(nodeModuleDir)
} catch {
return
}
const patchIdx = patches.indexOf(patch)
const dedupKey = `${patchIdx}\0${realModuleDir}`
if (patchedRealPaths.has(dedupKey)) return
patchedRealPaths.add(dedupKey)

const version = patch.patchFiles.version
if (typeof version === 'string') {
const pkgJSON = await FSExtra.readJSON(join(nodeModuleDir, 'package.json'))
Expand Down