feat: pin Node.js to global package (#3780)

This commit is contained in:
Zoltan Kochan
2021-09-22 01:32:33 +03:00
committed by GitHub
parent d62259d677
commit 553a5d840d
17 changed files with 148 additions and 22 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/manifest-utils": minor
---
The path to Node.js executable is added to `dependenciesMeta` when `nodeExecPath` is specified in the`PackageSpecObject`.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/link-bins": minor
---
Allow to specify the path to Node.js executable that should be called from the command shim.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-installation": minor
---
Globally installed packages should always use the active version of Node.js. So if webpack is installed while Node.js 16 is active, webpack will be executed using Node.js 16 even if the active Node.js version is switched using `pnpm env`.

View File

@@ -38,7 +38,7 @@
"@pnpm/read-package-json": "workspace:5.0.4",
"@pnpm/read-project-manifest": "workspace:2.0.5",
"@pnpm/types": "workspace:7.4.0",
"@zkochan/cmd-shim": "^5.1.3",
"@zkochan/cmd-shim": "^5.2.0",
"is-subdir": "^1.1.1",
"is-windows": "^1.0.2",
"normalize-path": "^3.0.0",

View File

@@ -32,6 +32,7 @@ export default async (
binsDir: string,
opts: {
allowExoticManifests?: boolean
nodeExecPathByAlias?: Record<string, string>
warn: WarnFunction
}
): Promise<string[]> => {
@@ -45,10 +46,15 @@ export default async (
const allCmds = unnest(
(await Promise.all(
allDeps
.map((depName) => path.resolve(modulesDir, depName))
.filter((depDir) => !isSubdir(depDir, binsDir)) // Don't link own bins
.map((depDir) => normalizePath(depDir))
.map(getPackageBins.bind(null, pkgBinOpts))
.map((alias) => ({
depDir: path.resolve(modulesDir, alias),
nodeExecPath: opts.nodeExecPathByAlias?.[alias],
}))
.filter(({ depDir }) => !isSubdir(depDir, binsDir)) // Don't link own bins
.map(({ depDir, nodeExecPath }) => {
const target = normalizePath(depDir)
return getPackageBins(pkgBinOpts, target, nodeExecPath)
})
))
.filter((cmds: Command[]) => cmds.length)
)
@@ -59,6 +65,7 @@ export default async (
export async function linkBinsOfPackages (
pkgs: Array<{
manifest: DependencyManifest
nodeExecPath?: string
location: string
}>,
binsTarget: string,
@@ -71,7 +78,7 @@ export async function linkBinsOfPackages (
const allCmds = unnest(
(await Promise.all(
pkgs
.map(async (pkg) => getPackageBinsFromManifest(pkg.manifest, pkg.location))
.map(async (pkg) => getPackageBinsFromManifest(pkg.manifest, pkg.location, pkg.nodeExecPath))
))
.filter((cmds: Command[]) => cmds.length)
)
@@ -83,6 +90,7 @@ type CommandInfo = Command & {
ownName: boolean
pkgName: string
makePowerShellShim: boolean
nodeExecPath?: string
}
async function linkBins (
@@ -130,7 +138,8 @@ async function getPackageBins (
allowExoticManifests: boolean
warn: WarnFunction
},
target: string
target: string,
nodeExecPath?: string
): Promise<CommandInfo[]> {
const manifest = opts.allowExoticManifests
? (await safeReadProjectManifestOnly(target) as DependencyManifest)
@@ -150,16 +159,17 @@ async function getPackageBins (
throw new PnpmError('INVALID_PACKAGE_NAME', `Package in ${target} must have a name to get bin linked.`)
}
return getPackageBinsFromManifest(manifest, target)
return getPackageBinsFromManifest(manifest, target, nodeExecPath)
}
async function getPackageBinsFromManifest (manifest: DependencyManifest, pkgDir: string): Promise<CommandInfo[]> {
async function getPackageBinsFromManifest (manifest: DependencyManifest, pkgDir: string, nodeExecPath?: string): Promise<CommandInfo[]> {
const cmds = await binify(manifest, pkgDir)
return cmds.map((cmd) => ({
...cmd,
ownName: cmd.name === manifest.name,
pkgName: manifest.name,
makePowerShellShim: POWER_SHELL_IS_SUPPORTED && manifest.name !== 'pnpm',
nodeExecPath,
}))
}
@@ -177,6 +187,7 @@ async function linkBin (cmd: CommandInfo, binsDir: string) {
return cmdShim(cmd.path, externalBinPath, {
createPwshFile: cmd.makePowerShellShim,
nodePath,
nodeExecPath: cmd.nodeExecPath,
})
}

View File

@@ -7,6 +7,7 @@ import {
export interface PackageSpecObject {
alias: string
nodeExecPath?: string
peer?: boolean
pref?: string
saveType?: DependenciesField
@@ -38,6 +39,12 @@ export async function updateProjectManifestObject (
packageManifest[usedDepType] = packageManifest[usedDepType] ?? {}
packageManifest[usedDepType]![packageSpec.alias] = packageSpec.pref
}
if (packageSpec.nodeExecPath) {
if (packageManifest.dependenciesMeta == null) {
packageManifest.dependenciesMeta = {}
}
packageManifest.dependenciesMeta[packageSpec.alias] = { node: packageSpec.nodeExecPath }
}
})
packageManifestLogger.debug({

View File

@@ -36,7 +36,7 @@
"@pnpm/package-store": "workspace:12.0.15",
"@pnpm/store-path": "^5.0.0",
"@pnpm/tarball-fetcher": "workspace:9.3.6",
"@zkochan/cmd-shim": "^5.1.3",
"@zkochan/cmd-shim": "^5.2.0",
"adm-zip": "^0.5.5",
"load-json-file": "^6.2.0",
"rename-overwrite": "^4.0.0",

View File

@@ -177,6 +177,9 @@ when running add/update with the --workspace option')
if (!opts.ignorePnpmfile) {
installOpts['hooks'] = requireHooks(opts.lockfileDir ?? dir, opts)
}
if (opts.global) {
installOpts['nodeExecPath'] = process.env.NODE ?? process.execPath
}
let { manifest, writeProjectManifest } = await tryReadProjectManifest(opts.dir, opts)
if (manifest === null) {

View File

@@ -0,0 +1,53 @@
import { promises as fs } from 'fs'
import path from 'path'
import { add } from '@pnpm/plugin-commands-installation'
import prepare from '@pnpm/prepare'
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import tempy from 'tempy'
const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}`
const tmp = tempy.directory()
const DEFAULT_OPTIONS = {
argv: {
original: [],
},
bail: false,
bin: 'node_modules/.bin',
cacheDir: path.join(tmp, 'cache'),
cliOptions: {},
include: {
dependencies: true,
devDependencies: true,
optionalDependencies: true,
},
lock: true,
pnpmfile: '.pnpmfile.cjs',
rawConfig: { registry: REGISTRY_URL },
rawLocalConfig: { registry: REGISTRY_URL },
registries: {
default: REGISTRY_URL,
},
sort: true,
storeDir: path.join(tmp, 'store'),
workspaceConcurrency: 1,
}
test('globally installed package is linked with active version of Node.js', async () => {
prepare()
await add.handler({
...DEFAULT_OPTIONS,
dir: process.cwd(),
global: true,
linkWorkspacePackages: false,
}, ['hello-world-js-bin'])
const manifest = (await import(path.resolve('package.json')))
expect(
manifest.dependenciesMeta['hello-world-js-bin']?.node
).toBeTruthy()
const shimContent = await fs.readFile('node_modules/.bin/hello-world-js-bin', 'utf-8')
expect(shimContent).toContain(process.env.NODE)
})

View File

@@ -57,6 +57,7 @@ interface ProjectToLink {
export type ImporterToResolve = Importer<{
isNew?: boolean
nodeExecPath?: string
pinnedVersion?: PinnedVersion
raw: string
updateSpec?: boolean

View File

@@ -25,6 +25,7 @@ export default async function updateProjectManifest (
.map((rdd, index) => {
const wantedDep = importer.wantedDependencies[index]!
return resolvedDirectDepToSpecObject({ ...rdd, isNew: wantedDep.isNew, specRaw: wantedDep.raw }, importer, {
nodeExecPath: wantedDep.nodeExecPath,
pinnedVersion: wantedDep.pinnedVersion ?? importer['pinnedVersion'] ?? 'major',
preserveWorkspaceProtocol: opts.preserveWorkspaceProtocol,
saveWorkspaceProtocol: opts.saveWorkspaceProtocol,
@@ -34,6 +35,7 @@ export default async function updateProjectManifest (
if (pkgToInstall.updateSpec && pkgToInstall.alias && !specsToUpsert.some(({ alias }) => alias === pkgToInstall.alias)) {
specsToUpsert.push({
alias: pkgToInstall.alias,
nodeExecPath: pkgToInstall.nodeExecPath,
peer: importer['peer'],
saveType: importer['targetDependenciesField'],
})
@@ -66,6 +68,7 @@ function resolvedDirectDepToSpecObject (
}: ResolvedDirectDependency & { isNew?: Boolean, specRaw: string },
importer: ImporterToResolve,
opts: {
nodeExecPath?: string
pinnedVersion: PinnedVersion
preserveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean
@@ -105,6 +108,7 @@ function resolvedDirectDepToSpecObject (
}
return {
alias,
nodeExecPath: opts.nodeExecPath,
peer: importer['peer'],
pref,
saveType: (isNew === true) ? importer['targetDependenciesField'] : undefined,

View File

@@ -42,6 +42,7 @@ export interface StrictInstallOptions {
rawConfig: object
verifyStoreIntegrity: boolean
engineStrict: boolean
nodeExecPath?: string
nodeVersion: string
packageManager: {
name: string

View File

@@ -1,6 +1,7 @@
import { filterDependenciesByType } from '@pnpm/manifest-utils'
import {
Dependencies,
DependenciesMeta,
IncludedDependencies,
ProjectManifest,
} from '@pnpm/types'
@@ -18,9 +19,10 @@ export interface WantedDependency {
}
export default function getWantedDependencies (
pkg: Pick<ProjectManifest, 'devDependencies' | 'dependencies' | 'optionalDependencies'>,
pkg: Pick<ProjectManifest, 'devDependencies' | 'dependencies' | 'optionalDependencies' | 'dependenciesMeta'>,
opts?: {
includeDirect?: IncludedDependencies
nodeExecPath?: string
updateWorkspaceDependencies?: boolean
}
): WantedDependency[] {
@@ -34,6 +36,7 @@ export default function getWantedDependencies (
dependencies: pkg.dependencies ?? {},
devDependencies: pkg.devDependencies ?? {},
optionalDependencies: pkg.optionalDependencies ?? {},
dependenciesMeta: pkg.dependenciesMeta ?? {},
updatePref: opts?.updateWorkspaceDependencies === true
? updateWorkspacePref
: (pref) => pref,
@@ -50,6 +53,8 @@ function getWantedDependenciesFromGivenSet (
dependencies: Dependencies
devDependencies: Dependencies
optionalDependencies: Dependencies
dependenciesMeta: DependenciesMeta
nodeExecPath?: string
updatePref: (pref: string) => string
}
): WantedDependency[] {
@@ -64,6 +69,7 @@ function getWantedDependenciesFromGivenSet (
alias,
dev: depType === 'dev',
optional: depType === 'optional',
nodeExecPath: opts.nodeExecPath ?? opts.dependenciesMeta[alias]?.node,
pinnedVersion: guessPinnedVersionFromExistingSpec(deps[alias]),
pref,
raw: `${alias}@${pref}`,

View File

@@ -410,6 +410,7 @@ export async function mutateModules (
const wantedDependencies = getWantedDependencies(project.manifest, {
includeDirect: opts.includeDirect,
updateWorkspaceDependencies: opts.update,
nodeExecPath: opts.nodeExecPath,
})
.map((wantedDependency) => ({ ...wantedDependency, updateSpec: true }))
@@ -453,7 +454,7 @@ export async function mutateModules (
projectsToInstall.push({
pruneDirectDependencies: false,
...project,
wantedDependencies: wantedDeps.map(wantedDep => ({ ...wantedDep, isNew: true, updateSpec: true })),
wantedDependencies: wantedDeps.map(wantedDep => ({ ...wantedDep, isNew: true, updateSpec: true, nodeExecPath: opts.nodeExecPath })),
})
}
@@ -891,8 +892,16 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
await Promise.all(projectsToResolve.map(async (project, index) => {
let linkedPackages!: string[]
if (ctx.publicHoistPattern?.length && path.relative(project.rootDir, opts.lockfileDir) === '') {
const nodeExecPathByAlias = Object.entries(project.manifest.dependenciesMeta ?? {})
.reduce((prev, [alias, { node }]) => {
if (node) {
prev[alias] = node
}
return prev
}, {})
linkedPackages = await linkBins(project.modulesDir, project.binsDir, {
allowExoticManifests: true,
nodeExecPathByAlias,
warn: binWarn.bind(null, project.rootDir),
})
} else {
@@ -909,10 +918,14 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
linkedPackages = await linkBinsOfPackages(
(
await Promise.all(
directPkgs.map(async (dep) => ({
location: dep.dir,
manifest: await dep.fetchingBundledManifest?.() ?? await safeReadProjectManifestOnly(dep.dir),
}))
directPkgs.map(async (dep) => {
const manifest = await dep.fetchingBundledManifest?.() ?? await safeReadProjectManifestOnly(dep.dir)
return {
location: dep.dir,
manifest,
nodeExecPath: project.manifest.dependenciesMeta?.[manifest!.name!]?.node,
}
})
)
)
.filter(({ manifest }) => manifest != null) as Array<{ location: string, manifest: DependencyManifest }>,

View File

@@ -33,6 +33,11 @@ export default async function (
delete packageManifest.peerDependencies[removedDependency]
}
}
if (packageManifest.dependenciesMeta != null) {
for (const removedDependency of removedPackages) {
delete packageManifest.dependenciesMeta[removedDependency]
}
}
packageManifestLogger.debug({
prefix: opts.prefix,

View File

@@ -46,6 +46,12 @@ export interface PeerDependenciesMeta {
}
}
export interface DependenciesMeta {
[dependencyName: string]: {
node?: string
}
}
export interface PublishConfig extends Record<string, unknown> {
directory?: string
executableFiles?: string[]
@@ -64,6 +70,7 @@ interface BaseManifest {
optionalDependencies?: Dependencies
peerDependencies?: Dependencies
peerDependenciesMeta?: PeerDependenciesMeta
dependenciesMeta?: DependenciesMeta
bundleDependencies?: string[]
bundledDependencies?: string[]
homepage?: string

12
pnpm-lock.yaml generated
View File

@@ -980,7 +980,7 @@ importers:
'@types/node': ^14.14.33
'@types/normalize-path': ^3.0.0
'@types/ramda': 0.27.39
'@zkochan/cmd-shim': ^5.1.3
'@zkochan/cmd-shim': ^5.2.0
is-subdir: ^1.1.1
is-windows: ^1.0.2
ncp: ^2.0.0
@@ -996,7 +996,7 @@ importers:
'@pnpm/read-package-json': link:../read-package-json
'@pnpm/read-project-manifest': link:../read-project-manifest
'@pnpm/types': link:../types
'@zkochan/cmd-shim': 5.1.3
'@zkochan/cmd-shim': 5.2.0
is-subdir: 1.2.0
is-windows: 1.0.2
normalize-path: 3.0.0
@@ -1786,7 +1786,7 @@ importers:
'@pnpm/store-path': ^5.0.0
'@pnpm/tarball-fetcher': workspace:9.3.6
'@types/adm-zip': ^0.4.34
'@zkochan/cmd-shim': ^5.1.3
'@zkochan/cmd-shim': ^5.2.0
adm-zip: ^0.5.5
execa: npm:safe-execa@^0.1.1
load-json-file: ^6.2.0
@@ -1805,7 +1805,7 @@ importers:
'@pnpm/package-store': link:../package-store
'@pnpm/store-path': 5.0.0
'@pnpm/tarball-fetcher': link:../tarball-fetcher
'@zkochan/cmd-shim': 5.1.3
'@zkochan/cmd-shim': 5.2.0
adm-zip: 0.5.5
load-json-file: 6.2.0
rename-overwrite: 4.0.0
@@ -5311,8 +5311,8 @@ packages:
tslib: 1.14.1
dev: false
/@zkochan/cmd-shim/5.1.3:
resolution: {integrity: sha512-XCy+ZwXoFKswHmJBFbhPIs+NBxYJpitzQ+kSvlhu2upIt74k0/OJsiOJnwJS4Usuydh+ipmcIjwQ55vIJOyKJg==}
/@zkochan/cmd-shim/5.2.0:
resolution: {integrity: sha512-lY0gYPCG09RcrOjvQtJpABF8YCjsaLjoYMOjAxZhNK1vKKlr0/UPvHsp66z/Gsj96+L1DRHL1oqwaPTXf368jg==}
engines: {node: '>=10.13'}
dependencies:
is-windows: 1.0.2