fix: sync bin links after injected deps sync (#10064)

* fix: sync bin links after injected deps sync

* test: add integration test for bin sync after scripts

* docs: add changeset for bin sync fix

* style: apply Copilot's suggestion
This commit is contained in:
月正海角
2025-10-13 21:45:59 +08:00
committed by GitHub
parent f3195f0de8
commit 6089939e15
6 changed files with 159 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/workspace.injected-deps-syncer": patch
"pnpm": patch
---
Sync bin links after injected dependencies are updated by build scripts. This ensures that binaries created during build processes are properly linked and accessible to consuming projects [#10057](https://github.com/pnpm/pnpm/issues/10057).

12
pnpm-lock.yaml generated
View File

@@ -8719,9 +8719,21 @@ importers:
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/link-bins':
specifier: workspace:*
version: link:../../pkg-manager/link-bins
'@pnpm/modules-yaml':
specifier: workspace:*
version: link:../../pkg-manager/modules-yaml
'@pnpm/read-package-json':
specifier: workspace:*
version: link:../../pkg-manifest/read-package-json
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
'@pnpm/workspace.find-packages':
specifier: workspace:*
version: link:../find-packages
'@types/normalize-path':
specifier: 'catalog:'
version: 3.0.2

View File

@@ -0,0 +1,68 @@
import fs from 'fs'
import { preparePackages } from '@pnpm/prepare'
import { sync as writeYamlFile } from 'write-yaml-file'
import { execPnpm } from './utils/index.js'
test('sync bin links after build script', async () => {
preparePackages([
{
name: 'cli-tool',
version: '1.0.0',
bin: {
'cli-tool': 'bin/cli.js',
},
scripts: {
build: 'node -e "const fs = require(\'fs\'); fs.mkdirSync(\'bin\', { recursive: true }); fs.writeFileSync(\'bin/cli.js\', \'#!/usr/bin/env node\\nconsole.log(\\\'CLI tool works!\\\')\\n\', \'utf-8\')"',
},
},
{
name: 'consumer',
version: '1.0.0',
dependencies: {
'cli-tool': 'workspace:*',
},
dependenciesMeta: {
'cli-tool': {
injected: true,
},
},
scripts: {
test: 'cli-tool',
},
},
])
writeYamlFile('pnpm-workspace.yaml', {
packages: ['*'],
})
fs.writeFileSync('.npmrc', [
'reporter=append-only',
'inject-workspace-packages=true',
'dedupe-injected-deps=false',
'sync-injected-deps-after-scripts[]=build',
].join('\n'))
// Install - bin won't be created because bin/cli.js doesn't exist yet
await execPnpm(['install'])
// Verify injection happened
expect(fs.readdirSync('node_modules/.pnpm')).toContain('cli-tool@file+cli-tool')
// Build cli-tool
await execPnpm(['--filter=cli-tool', 'run', 'build'])
// Verify bin/cli.js was created
expect(fs.existsSync('cli-tool/bin/cli.js')).toBe(true)
// Verify bin was synced to the injected location
const injectedBinPath = 'node_modules/.pnpm/cli-tool@file+cli-tool/node_modules/cli-tool/bin/cli.js'
expect(fs.existsSync(injectedBinPath)).toBe(true)
// Verify bin link was created
const binPath = 'node_modules/.pnpm/cli-tool@file+cli-tool/node_modules/.bin/cli-tool'
expect(fs.existsSync(binPath) || fs.existsSync(`${binPath}.CMD`) || fs.existsSync(`${binPath}.ps1`)).toBe(true)
// Run the consumer's test script which uses the bin
await execPnpm(['--filter=consumer', 'test'])
})

View File

@@ -33,7 +33,11 @@
"dependencies": {
"@pnpm/directory-fetcher": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/link-bins": "workspace:*",
"@pnpm/modules-yaml": "workspace:*",
"@pnpm/read-package-json": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/workspace.find-packages": "workspace:*",
"@types/normalize-path": "catalog:",
"normalize-path": "catalog:"
},

View File

@@ -1,7 +1,11 @@
import path from 'path'
import { PnpmError } from '@pnpm/error'
import { linkBins, linkBinsOfPackages } from '@pnpm/link-bins'
import { logger as createLogger } from '@pnpm/logger'
import { readModulesManifest } from '@pnpm/modules-yaml'
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
import { type DependencyManifest } from '@pnpm/types'
import { findWorkspacePackagesNoCheck } from '@pnpm/workspace.find-packages'
import normalizePath from 'normalize-path'
import { DirPatcher } from './DirPatcher.js'
@@ -57,4 +61,57 @@ export async function syncInjectedDeps (opts: SyncInjectedDepsOptions): Promise<
targetDirs.map(targetDir => path.resolve(opts.workspaceDir!, targetDir))
)
await Promise.all(patchers.map(patcher => patcher.apply()))
// After syncing files, also sync bin links if the package has binaries
await syncBinLinks(pkgRootDir, targetDirs, opts.workspaceDir)
}
async function syncBinLinks (
pkgRootDir: string,
targetDirs: string[],
workspaceDir: string
): Promise<void> {
const manifest = await safeReadPackageJsonFromDir(pkgRootDir) as DependencyManifest | undefined
if (!manifest?.bin || !manifest?.name) {
return
}
// Step 1: Link bins in .pnpm virtual store
const binLinkPromises = targetDirs.map(async (targetDir) => {
const resolvedTargetDir = path.resolve(workspaceDir, targetDir)
const parentNodeModulesDir = path.dirname(resolvedTargetDir)
const binDir = path.join(parentNodeModulesDir, '.bin')
await linkBinsOfPackages(
[{
manifest,
location: resolvedTargetDir,
}],
binDir,
{}
)
})
// Step 2: Relink bins for all workspace projects
// We need to relink bins for all workspace projects because injected deps
// can be used by any project in the workspace. We relink all bins (not just
// this package) to ensure consistency.
const allProjects = await findWorkspacePackagesNoCheck(workspaceDir, {})
const consumerLinkPromises = allProjects.map(async (project) => {
const projectNodeModules = path.join(project.rootDir, 'node_modules')
const projectBinDir = path.join(projectNodeModules, '.bin')
// Relink all bins in the project's node_modules
await linkBins(projectNodeModules, projectBinDir, {
allowExoticManifests: true,
projectManifest: project.manifest,
warn: (msg: string) => {
console.warn(`[linkBins warning] ${msg}`)
},
})
})
await Promise.all([...binLinkPromises, ...consumerLinkPromises])
}

View File

@@ -21,8 +21,20 @@
{
"path": "../../packages/logger"
},
{
"path": "../../packages/types"
},
{
"path": "../../pkg-manager/link-bins"
},
{
"path": "../../pkg-manager/modules-yaml"
},
{
"path": "../../pkg-manifest/read-package-json"
},
{
"path": "../find-packages"
}
]
}