mirror of
https://github.com/pnpm/pnpm.git
synced 2026-02-02 19:22:52 -05:00
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:
6
.changeset/sync-injected-deps-bins.md
Normal file
6
.changeset/sync-injected-deps-bins.md
Normal 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
12
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
68
pnpm/test/syncInjectedDepsAfterScripts-bin.ts
Normal file
68
pnpm/test/syncInjectedDepsAfterScripts-bin.ts
Normal 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'])
|
||||
})
|
||||
@@ -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:"
|
||||
},
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user