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

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

No files matched your search

+5
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`.
+5
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.
+5
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`.
+1 -1
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",
+19 -8
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,
})
}
@@ -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({
+1 -1
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",
@@ -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) {
@@ -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)
})
@@ -57,6 +57,7 @@ interface ProjectToLink {
export type ImporterToResolve = Importer<{
isNew?: boolean
nodeExecPath?: string
pinnedVersion?: PinnedVersion
raw: string
updateSpec?: boolean
@@ -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,
@@ -42,6 +42,7 @@ export interface StrictInstallOptions {
rawConfig: object
verifyStoreIntegrity: boolean
engineStrict: boolean
nodeExecPath?: string
nodeVersion: string
packageManager: {
name: string
@@ -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}`,
+18 -5
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 }>,
@@ -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,
+7
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
+6 -6
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