Skip to content

Commit

Permalink
feat: pin Node.js to global package (#3780)
Browse files Browse the repository at this point in the history
  • Loading branch information
zkochan committed Sep 21, 2021
1 parent d62259d commit 553a5d8
Show file tree
Hide file tree
Showing 17 changed files with 148 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-ties-hear.md
@@ -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 changes: 5 additions & 0 deletions .changeset/serious-foxes-fold.md
@@ -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 changes: 5 additions & 0 deletions .changeset/strange-camels-wave.md
@@ -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`.
2 changes: 1 addition & 1 deletion packages/link-bins/package.json
Expand Up @@ -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",
Expand Down
27 changes: 19 additions & 8 deletions packages/link-bins/src/index.ts
Expand Up @@ -32,6 +32,7 @@ export default async (
binsDir: string,
opts: {
allowExoticManifests?: boolean
nodeExecPathByAlias?: Record<string, string>
warn: WarnFunction
}
): Promise<string[]> => {
Expand All @@ -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)
)
Expand All @@ -59,6 +65,7 @@ export default async (
export async function linkBinsOfPackages (
pkgs: Array<{
manifest: DependencyManifest
nodeExecPath?: string
location: string
}>,
binsTarget: string,
Expand All @@ -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)
)
Expand All @@ -83,6 +90,7 @@ type CommandInfo = Command & {
ownName: boolean
pkgName: string
makePowerShellShim: boolean
nodeExecPath?: string
}

async function linkBins (
Expand Down Expand Up @@ -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)
Expand All @@ -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,
}))
}

Expand All @@ -177,6 +187,7 @@ async function linkBin (cmd: CommandInfo, binsDir: string) {
return cmdShim(cmd.path, externalBinPath, {
createPwshFile: cmd.makePowerShellShim,
nodePath,
nodeExecPath: cmd.nodeExecPath,
})
}

Expand Down
7 changes: 7 additions & 0 deletions packages/manifest-utils/src/updateProjectManifestObject.ts
Expand Up @@ -7,6 +7,7 @@ import {

export interface PackageSpecObject {
alias: string
nodeExecPath?: string
peer?: boolean
pref?: string
saveType?: DependenciesField
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-commands-env/package.json
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-commands-installation/src/installDeps.ts
Expand Up @@ -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) {
Expand Down
53 changes: 53 additions & 0 deletions packages/plugin-commands-installation/test/global.ts
@@ -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)
})
1 change: 1 addition & 0 deletions packages/resolve-dependencies/src/index.ts
Expand Up @@ -57,6 +57,7 @@ interface ProjectToLink {

export type ImporterToResolve = Importer<{
isNew?: boolean
nodeExecPath?: string
pinnedVersion?: PinnedVersion
raw: string
updateSpec?: boolean
Expand Down
4 changes: 4 additions & 0 deletions packages/resolve-dependencies/src/updateProjectManifest.ts
Expand Up @@ -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,
Expand All @@ -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'],
})
Expand Down Expand Up @@ -66,6 +68,7 @@ function resolvedDirectDepToSpecObject (
}: ResolvedDirectDependency & { isNew?: Boolean, specRaw: string },
importer: ImporterToResolve,
opts: {
nodeExecPath?: string
pinnedVersion: PinnedVersion
preserveWorkspaceProtocol: boolean
saveWorkspaceProtocol: boolean
Expand Down Expand Up @@ -105,6 +108,7 @@ function resolvedDirectDepToSpecObject (
}
return {
alias,
nodeExecPath: opts.nodeExecPath,
peer: importer['peer'],
pref,
saveType: (isNew === true) ? importer['targetDependenciesField'] : undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/supi/src/install/extendInstallOptions.ts
Expand Up @@ -42,6 +42,7 @@ export interface StrictInstallOptions {
rawConfig: object
verifyStoreIntegrity: boolean
engineStrict: boolean
nodeExecPath?: string
nodeVersion: string
packageManager: {
name: string
Expand Down
8 changes: 7 additions & 1 deletion packages/supi/src/install/getWantedDependencies.ts
@@ -1,6 +1,7 @@
import { filterDependenciesByType } from '@pnpm/manifest-utils'
import {
Dependencies,
DependenciesMeta,
IncludedDependencies,
ProjectManifest,
} from '@pnpm/types'
Expand All @@ -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[] {
Expand All @@ -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,
Expand All @@ -50,6 +53,8 @@ function getWantedDependenciesFromGivenSet (
dependencies: Dependencies
devDependencies: Dependencies
optionalDependencies: Dependencies
dependenciesMeta: DependenciesMeta
nodeExecPath?: string
updatePref: (pref: string) => string
}
): WantedDependency[] {
Expand All @@ -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}`,
Expand Down
23 changes: 18 additions & 5 deletions packages/supi/src/install/index.ts
Expand Up @@ -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 }))

Expand Down Expand Up @@ -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 })),
})
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 }>,
Expand Down
5 changes: 5 additions & 0 deletions packages/supi/src/uninstall/removeDeps.ts
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/package.ts
Expand Up @@ -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[]
Expand All @@ -64,6 +70,7 @@ interface BaseManifest {
optionalDependencies?: Dependencies
peerDependencies?: Dependencies
peerDependenciesMeta?: PeerDependenciesMeta
dependenciesMeta?: DependenciesMeta
bundleDependencies?: string[]
bundledDependencies?: string[]
homepage?: string
Expand Down

0 comments on commit 553a5d8

Please sign in to comment.