mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat!: the link command should add overrides (#8653)
This commit is contained in:
13
.changeset/breezy-eggs-repair.md
Normal file
13
.changeset/breezy-eggs-repair.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-installation": major
|
||||
"@pnpm/core": major
|
||||
"@pnpm/config": major
|
||||
"pnpm": major
|
||||
---
|
||||
|
||||
The `pnpm link` command adds overrides to the root `package.json`. In a workspace the override is added to the root of the workspace, so it links the dependency to all projects in a workspace.
|
||||
|
||||
To link a package globally, just run `pnpm link` from the package's directory. Previously, the command `pnpm link -g` was required to link a package globally.
|
||||
|
||||
Related PR: [#8653](https://github.com/pnpm/pnpm/pull/8653).
|
||||
|
||||
@@ -197,6 +197,7 @@ export interface Config {
|
||||
extendNodePath?: boolean
|
||||
gitBranchLockfile?: boolean
|
||||
globalDir?: string
|
||||
globalPkgDir: string
|
||||
lockfile?: boolean
|
||||
dedupeInjectedDeps?: boolean
|
||||
nodeOptions?: string
|
||||
|
||||
@@ -260,16 +260,16 @@ export async function getConfig (opts: {
|
||||
return undefined
|
||||
})()
|
||||
pnpmConfig.pnpmHomeDir = getDataDir(process)
|
||||
let globalDirRoot
|
||||
if (pnpmConfig.globalDir) {
|
||||
globalDirRoot = pnpmConfig.globalDir
|
||||
} else {
|
||||
globalDirRoot = path.join(pnpmConfig.pnpmHomeDir, 'global')
|
||||
}
|
||||
pnpmConfig.globalPkgDir = path.join(globalDirRoot, LAYOUT_VERSION.toString())
|
||||
|
||||
if (cliOptions['global']) {
|
||||
let globalDirRoot
|
||||
if (pnpmConfig.globalDir) {
|
||||
globalDirRoot = pnpmConfig.globalDir
|
||||
} else {
|
||||
globalDirRoot = path.join(pnpmConfig.pnpmHomeDir, 'global')
|
||||
}
|
||||
pnpmConfig.dir = path.join(globalDirRoot, LAYOUT_VERSION.toString())
|
||||
|
||||
pnpmConfig.dir = pnpmConfig.globalPkgDir
|
||||
pnpmConfig.bin = npmConfig.get('global-bin-dir') ?? env.PNPM_HOME
|
||||
if (pnpmConfig.bin) {
|
||||
fs.mkdirSync(pnpmConfig.bin, { recursive: true })
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './install'
|
||||
export { PeerDependencyIssuesError } from './install/reportPeerDependencyIssues'
|
||||
export * from './link'
|
||||
export * from './getPeerDependencyIssues'
|
||||
|
||||
@@ -45,9 +45,7 @@ import { getPreferredVersionsFromLockfileAndManifests } from '@pnpm/lockfile.pre
|
||||
import { logger, globalInfo, streamParser } from '@pnpm/logger'
|
||||
import { getAllDependenciesFromManifest, getAllUniqueSpecs } from '@pnpm/manifest-utils'
|
||||
import { writeModulesManifest } from '@pnpm/modules-yaml'
|
||||
import { readModulesDir } from '@pnpm/read-modules-dir'
|
||||
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
|
||||
import { removeBin } from '@pnpm/remove-bins'
|
||||
import {
|
||||
getWantedDependencies,
|
||||
type DependenciesGraph,
|
||||
@@ -70,10 +68,7 @@ import {
|
||||
type ReadPackageHook,
|
||||
type ProjectRootDir,
|
||||
} from '@pnpm/types'
|
||||
import rimraf from '@zkochan/rimraf'
|
||||
import isInnerLink from 'is-inner-link'
|
||||
import isSubdir from 'is-subdir'
|
||||
import pFilter from 'p-filter'
|
||||
import pLimit from 'p-limit'
|
||||
import mapValues from 'ramda/src/map'
|
||||
import clone from 'ramda/src/clone'
|
||||
@@ -133,20 +128,12 @@ export interface UninstallSomeDepsMutation {
|
||||
targetDependenciesField?: DependenciesField
|
||||
}
|
||||
|
||||
export interface UnlinkDepsMutation {
|
||||
mutation: 'unlink'
|
||||
}
|
||||
|
||||
export interface UnlinkSomeDepsMutation {
|
||||
mutation: 'unlinkSome'
|
||||
dependencyNames: string[]
|
||||
}
|
||||
|
||||
export type DependenciesMutation = InstallDepsMutation | InstallSomeDepsMutation | UninstallSomeDepsMutation | UnlinkDepsMutation | UnlinkSomeDepsMutation
|
||||
export type DependenciesMutation = InstallDepsMutation | InstallSomeDepsMutation | UninstallSomeDepsMutation
|
||||
|
||||
type Opts = Omit<InstallOptions, 'allProjects'> & {
|
||||
preferredVersions?: PreferredVersions
|
||||
pruneDirectDependencies?: boolean
|
||||
binsDir?: string
|
||||
} & InstallMutationOptions
|
||||
|
||||
export async function install (
|
||||
@@ -171,6 +158,7 @@ export async function install (
|
||||
buildIndex: 0,
|
||||
manifest,
|
||||
rootDir,
|
||||
binsDir: opts.binsDir,
|
||||
}],
|
||||
}
|
||||
)
|
||||
@@ -568,70 +556,6 @@ Note that in CI environments, this setting is enabled by default.`,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'unlink': {
|
||||
const packageDirs = await readModulesDir(projectOpts.modulesDir)
|
||||
const externalPackages = await pFilter(
|
||||
packageDirs!,
|
||||
async (packageDir: string) => isExternalLink(ctx.storeDir, projectOpts.modulesDir, packageDir)
|
||||
)
|
||||
const allDeps = getAllDependenciesFromManifest(projectOpts.manifest)
|
||||
const packagesToInstall: string[] = []
|
||||
for (const pkgName of externalPackages) {
|
||||
await rimraf(path.join(projectOpts.modulesDir, pkgName))
|
||||
if (allDeps[pkgName]) {
|
||||
packagesToInstall.push(pkgName)
|
||||
}
|
||||
}
|
||||
if (packagesToInstall.length === 0) {
|
||||
return {
|
||||
updatedProjects: projects.map((mutatedProject) => ctx.projects[mutatedProject.rootDir]),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: install only those that were unlinked
|
||||
// but don't update their version specs in package.json
|
||||
await installCase({ ...projectOpts, mutation: 'install' })
|
||||
break
|
||||
}
|
||||
case 'unlinkSome': {
|
||||
if (projectOpts.manifest?.name && opts.globalBin) {
|
||||
await removeBin(path.join(opts.globalBin, projectOpts.manifest?.name))
|
||||
}
|
||||
const packagesToInstall: string[] = []
|
||||
const allDeps = getAllDependenciesFromManifest(projectOpts.manifest)
|
||||
for (const depName of project.dependencyNames) {
|
||||
try {
|
||||
if (!await isExternalLink(ctx.storeDir, projectOpts.modulesDir, depName)) {
|
||||
logger.warn({
|
||||
message: `${depName} is not an external link`,
|
||||
prefix: project.rootDir,
|
||||
})
|
||||
continue
|
||||
}
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
if (err['code'] !== 'ENOENT') throw err
|
||||
}
|
||||
await rimraf(path.join(projectOpts.modulesDir, depName))
|
||||
if (allDeps[depName]) {
|
||||
packagesToInstall.push(depName)
|
||||
}
|
||||
}
|
||||
if (packagesToInstall.length === 0) {
|
||||
return {
|
||||
updatedProjects: projects.map((mutatedProject) => ctx.projects[mutatedProject.rootDir]),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: install only those that were unlinked
|
||||
// but don't update their version specs in package.json
|
||||
await installSome({
|
||||
...projectOpts,
|
||||
dependencySelectors: packagesToInstall,
|
||||
mutation: 'installSome',
|
||||
updatePackageManifest: false,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
@@ -747,12 +671,6 @@ function cacheExpired (prunedAt: string, maxAgeInMinutes: number): boolean {
|
||||
return ((Date.now() - new Date(prunedAt).valueOf()) / (1000 * 60)) > maxAgeInMinutes
|
||||
}
|
||||
|
||||
async function isExternalLink (storeDir: string, modules: string, pkgName: string): Promise<boolean> {
|
||||
const link = await isInnerLink(modules, pkgName)
|
||||
|
||||
return !link.isInner
|
||||
}
|
||||
|
||||
function pkgHasDependencies (manifest: ProjectManifest): boolean {
|
||||
return Boolean(
|
||||
(Object.keys(manifest.dependencies ?? {}).length > 0) ||
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
summaryLogger,
|
||||
} from '@pnpm/core-loggers'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { getContextForSingleImporter } from '@pnpm/get-context'
|
||||
import { linkBinsOfPackages } from '@pnpm/link-bins'
|
||||
import {
|
||||
getLockfileImporterId,
|
||||
type ProjectSnapshot,
|
||||
writeCurrentLockfile,
|
||||
writeLockfiles,
|
||||
} from '@pnpm/lockfile.fs'
|
||||
import { logger, streamParser } from '@pnpm/logger'
|
||||
import {
|
||||
getPref,
|
||||
getSpecFromPackageManifest,
|
||||
getDependencyTypeFromManifest,
|
||||
guessDependencyType,
|
||||
type PackageSpecObject,
|
||||
updateProjectManifestObject,
|
||||
} from '@pnpm/manifest-utils'
|
||||
import { pruneSharedLockfile } from '@pnpm/lockfile.pruner'
|
||||
import { readProjectManifest } from '@pnpm/read-project-manifest'
|
||||
import { symlinkDirectRootDependency } from '@pnpm/symlink-dependency'
|
||||
import {
|
||||
type DependenciesField,
|
||||
DEPENDENCIES_FIELDS,
|
||||
type DependencyManifest,
|
||||
type ProjectManifest,
|
||||
} from '@pnpm/types'
|
||||
import normalize from 'normalize-path'
|
||||
import {
|
||||
extendOptions,
|
||||
type LinkOptions,
|
||||
} from './options'
|
||||
|
||||
type LinkFunctionOptions = LinkOptions & {
|
||||
linkToBin?: string
|
||||
dir: string
|
||||
}
|
||||
|
||||
export type { LinkFunctionOptions }
|
||||
|
||||
export async function link (
|
||||
linkFromPkgs: Array<{ alias: string, path: string } | string>,
|
||||
destModules: string,
|
||||
maybeOpts: LinkFunctionOptions
|
||||
): Promise<ProjectManifest> {
|
||||
const reporter = maybeOpts?.reporter
|
||||
if ((reporter != null) && typeof reporter === 'function') {
|
||||
streamParser.on('data', reporter)
|
||||
}
|
||||
const opts = await extendOptions(maybeOpts)
|
||||
const ctx = await getContextForSingleImporter(opts.manifest, {
|
||||
...opts,
|
||||
extraBinPaths: [], // ctx.extraBinPaths is not needed, so this is fine
|
||||
}, true)
|
||||
|
||||
const importerId = getLockfileImporterId(ctx.lockfileDir, opts.dir)
|
||||
const specsToUpsert = [] as PackageSpecObject[]
|
||||
|
||||
const linkedPkgs = await Promise.all(
|
||||
linkFromPkgs.map(async (linkFrom) => {
|
||||
let linkFromPath: string
|
||||
let linkFromAlias: string | undefined
|
||||
if (typeof linkFrom === 'string') {
|
||||
linkFromPath = linkFrom
|
||||
} else {
|
||||
linkFromPath = linkFrom.path
|
||||
linkFromAlias = linkFrom.alias
|
||||
}
|
||||
const { manifest } = await readProjectManifest(linkFromPath) as { manifest: DependencyManifest }
|
||||
if (typeof linkFrom === 'string' && manifest.name === undefined) {
|
||||
throw new PnpmError('INVALID_PACKAGE_NAME', `Package in ${linkFromPath} must have a name field to be linked`)
|
||||
}
|
||||
|
||||
const targetDependencyType = getDependencyTypeFromManifest(opts.manifest, manifest.name) ?? opts.targetDependenciesField
|
||||
|
||||
specsToUpsert.push({
|
||||
alias: manifest.name,
|
||||
pref: getPref(manifest.name, manifest.name, manifest.version, {
|
||||
pinnedVersion: opts.pinnedVersion,
|
||||
}),
|
||||
saveType: (targetDependencyType ?? (ctx.manifest && guessDependencyType(manifest.name, ctx.manifest))) as DependenciesField,
|
||||
})
|
||||
|
||||
const packagePath = normalize(path.relative(opts.dir, linkFromPath))
|
||||
const addLinkOpts = {
|
||||
linkedPkgName: linkFromAlias ?? manifest.name,
|
||||
manifest: ctx.manifest,
|
||||
packagePath,
|
||||
}
|
||||
addLinkToLockfile(ctx.currentLockfile.importers[importerId], addLinkOpts)
|
||||
addLinkToLockfile(ctx.wantedLockfile.importers[importerId], addLinkOpts)
|
||||
|
||||
return {
|
||||
alias: linkFromAlias ?? manifest.name,
|
||||
manifest,
|
||||
path: linkFromPath,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const updatedCurrentLockfile = pruneSharedLockfile(ctx.currentLockfile)
|
||||
|
||||
const warn = (message: string) => {
|
||||
logger.warn({ message, prefix: opts.dir })
|
||||
}
|
||||
const updatedWantedLockfile = pruneSharedLockfile(ctx.wantedLockfile, { warn })
|
||||
|
||||
// Linking should happen after removing orphans
|
||||
// Otherwise would've been removed
|
||||
await Promise.all(linkedPkgs.map(async ({ alias, manifest, path }) => {
|
||||
// TODO: cover with test that linking reports with correct dependency types
|
||||
const stu = specsToUpsert.find((s) => s.alias === manifest.name)
|
||||
const targetDependencyType = getDependencyTypeFromManifest(opts.manifest, manifest.name) ?? opts.targetDependenciesField
|
||||
await symlinkDirectRootDependency(path, destModules, alias, {
|
||||
fromDependenciesField: stu?.saveType ?? (targetDependencyType as DependenciesField),
|
||||
linkedPackage: manifest,
|
||||
prefix: opts.dir,
|
||||
})
|
||||
}))
|
||||
|
||||
const linkToBin = maybeOpts?.linkToBin ?? path.join(destModules, '.bin')
|
||||
await linkBinsOfPackages(linkedPkgs.map((p) => ({ manifest: p.manifest, location: p.path })), linkToBin, {
|
||||
extraNodePaths: ctx.extraNodePaths,
|
||||
preferSymlinkedExecutables: opts.preferSymlinkedExecutables,
|
||||
})
|
||||
|
||||
let newPkg!: ProjectManifest
|
||||
if (opts.targetDependenciesField) {
|
||||
newPkg = await updateProjectManifestObject(opts.dir, opts.manifest, specsToUpsert)
|
||||
for (const { alias } of specsToUpsert) {
|
||||
updatedWantedLockfile.importers[importerId].specifiers[alias] = getSpecFromPackageManifest(newPkg, alias)
|
||||
}
|
||||
} else {
|
||||
newPkg = opts.manifest
|
||||
}
|
||||
const lockfileOpts = { useGitBranchLockfile: opts.useGitBranchLockfile, mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles }
|
||||
if (opts.useLockfile) {
|
||||
await writeLockfiles({
|
||||
currentLockfile: updatedCurrentLockfile,
|
||||
currentLockfileDir: ctx.virtualStoreDir,
|
||||
wantedLockfile: updatedWantedLockfile,
|
||||
wantedLockfileDir: ctx.lockfileDir,
|
||||
...lockfileOpts,
|
||||
})
|
||||
} else {
|
||||
await writeCurrentLockfile(ctx.virtualStoreDir, updatedCurrentLockfile)
|
||||
}
|
||||
|
||||
summaryLogger.debug({ prefix: opts.dir })
|
||||
|
||||
if ((reporter != null) && typeof reporter === 'function') {
|
||||
streamParser.removeListener('data', reporter)
|
||||
}
|
||||
|
||||
return newPkg
|
||||
}
|
||||
|
||||
function addLinkToLockfile (
|
||||
projectSnapshot: ProjectSnapshot,
|
||||
opts: {
|
||||
linkedPkgName: string
|
||||
packagePath: string
|
||||
manifest?: ProjectManifest
|
||||
}
|
||||
) {
|
||||
const id = `link:${opts.packagePath}`
|
||||
let addedTo: DependenciesField | undefined
|
||||
for (const depType of DEPENDENCIES_FIELDS) {
|
||||
if (!addedTo && opts.manifest?.[depType]?.[opts.linkedPkgName]) {
|
||||
addedTo = depType
|
||||
projectSnapshot[depType] = projectSnapshot[depType] ?? {}
|
||||
projectSnapshot[depType]![opts.linkedPkgName] = id
|
||||
} else if (projectSnapshot[depType] != null) {
|
||||
delete projectSnapshot[depType]![opts.linkedPkgName]
|
||||
}
|
||||
}
|
||||
|
||||
// package.json might not be available when linking to global
|
||||
if (opts.manifest == null) return
|
||||
|
||||
const availableSpec = getSpecFromPackageManifest(opts.manifest, opts.linkedPkgName)
|
||||
if (availableSpec) {
|
||||
projectSnapshot.specifiers[opts.linkedPkgName] = availableSpec
|
||||
} else {
|
||||
delete projectSnapshot.specifiers[opts.linkedPkgName]
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import path from 'path'
|
||||
import { normalizeRegistries, DEFAULT_REGISTRIES } from '@pnpm/normalize-registries'
|
||||
import { type StoreController } from '@pnpm/store-controller-types'
|
||||
import {
|
||||
type DependenciesField,
|
||||
type ProjectManifest,
|
||||
type Registries,
|
||||
} from '@pnpm/types'
|
||||
import { type ReporterFunction } from '../types'
|
||||
|
||||
interface StrictLinkOptions {
|
||||
autoInstallPeers: boolean
|
||||
binsDir: string
|
||||
excludeLinksFromLockfile: boolean
|
||||
force: boolean
|
||||
useLockfile: boolean
|
||||
lockfileDir: string
|
||||
nodeLinker: 'isolated' | 'hoisted' | 'pnp'
|
||||
pinnedVersion: 'major' | 'minor' | 'patch'
|
||||
storeController: StoreController
|
||||
manifest: ProjectManifest
|
||||
registries: Registries
|
||||
storeDir: string
|
||||
reporter: ReporterFunction
|
||||
targetDependenciesField?: DependenciesField
|
||||
dir: string
|
||||
preferSymlinkedExecutables: boolean
|
||||
|
||||
hoistPattern: string[] | undefined
|
||||
forceHoistPattern: boolean
|
||||
|
||||
publicHoistPattern: string[] | undefined
|
||||
forcePublicHoistPattern: boolean
|
||||
|
||||
useGitBranchLockfile: boolean
|
||||
mergeGitBranchLockfiles: boolean
|
||||
virtualStoreDirMaxLength: number
|
||||
peersSuffixMaxLength: number
|
||||
}
|
||||
|
||||
export type LinkOptions =
|
||||
& Partial<StrictLinkOptions>
|
||||
& Pick<StrictLinkOptions, 'storeController' | 'manifest'>
|
||||
|
||||
export async function extendOptions (opts: LinkOptions): Promise<StrictLinkOptions> {
|
||||
if (opts) {
|
||||
for (const key in opts) {
|
||||
if (opts[key as keyof LinkOptions] === undefined) {
|
||||
delete opts[key as keyof LinkOptions]
|
||||
}
|
||||
}
|
||||
}
|
||||
const defaultOpts = await defaults(opts)
|
||||
const extendedOpts = { ...defaultOpts, ...opts, storeDir: defaultOpts.storeDir }
|
||||
extendedOpts.registries = normalizeRegistries(extendedOpts.registries)
|
||||
return extendedOpts
|
||||
}
|
||||
|
||||
async function defaults (opts: LinkOptions): Promise<StrictLinkOptions> {
|
||||
const dir = opts.dir ?? process.cwd()
|
||||
return {
|
||||
binsDir: path.join(dir, 'node_modules', '.bin'),
|
||||
dir,
|
||||
force: false,
|
||||
hoistPattern: undefined,
|
||||
lockfileDir: opts.lockfileDir ?? dir,
|
||||
nodeLinker: 'isolated',
|
||||
registries: DEFAULT_REGISTRIES,
|
||||
storeController: opts.storeController,
|
||||
storeDir: opts.storeDir,
|
||||
useLockfile: true,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
} as StrictLinkOptions
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { testDefaults } from './utils'
|
||||
|
||||
test('API', () => {
|
||||
expect(typeof pnpm.install).toBe('function')
|
||||
expect(typeof pnpm.link).toBe('function')
|
||||
})
|
||||
|
||||
// TODO: some sort of this validation might need to exist
|
||||
|
||||
@@ -1,52 +1,13 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import {
|
||||
addDependenciesToPackage,
|
||||
install,
|
||||
link,
|
||||
} from '@pnpm/core'
|
||||
import { addDependenciesToPackage, install } from '@pnpm/core'
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { addDistTag } from '@pnpm/registry-mock'
|
||||
import { type RootLog } from '@pnpm/core-loggers'
|
||||
import sinon from 'sinon'
|
||||
import writeJsonFile from 'write-json-file'
|
||||
import symlink from 'symlink-dir'
|
||||
import symlinkDir from 'symlink-dir'
|
||||
import { testDefaults } from './utils'
|
||||
|
||||
const f = fixtures(__dirname)
|
||||
|
||||
test('relative link', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
const linkedPkgName = 'hello-world-js-bin'
|
||||
const linkedPkgPath = path.resolve('..', linkedPkgName)
|
||||
|
||||
f.copy(linkedPkgName, linkedPkgPath)
|
||||
await link([`../${linkedPkgName}`], path.join(process.cwd(), 'node_modules'), testDefaults({
|
||||
dir: process.cwd(),
|
||||
manifest: {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/hello-world-js-bin': '*',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
project.isExecutable('.bin/hello-world-js-bin')
|
||||
|
||||
const wantedLockfile = project.readLockfile()
|
||||
expect(wantedLockfile.importers['.'].dependencies?.['@pnpm.e2e/hello-world-js-bin']).toStrictEqual({
|
||||
version: 'link:../hello-world-js-bin',
|
||||
specifier: '*',
|
||||
})
|
||||
|
||||
const currentLockfile = project.readCurrentLockfile()
|
||||
expect(currentLockfile.importers['.'].dependencies?.['@pnpm.e2e/hello-world-js-bin']).toStrictEqual({
|
||||
version: 'link:../hello-world-js-bin',
|
||||
specifier: '*',
|
||||
})
|
||||
})
|
||||
|
||||
test('relative link is linked by the name of the alias', async () => {
|
||||
const linkedPkgName = 'hello-world-js-bin'
|
||||
|
||||
@@ -80,34 +41,10 @@ test('relative link is not rewritten by argumentless install', async () => {
|
||||
const linkedPkgName = 'hello-world-js-bin'
|
||||
const linkedPkgPath = path.resolve('..', linkedPkgName)
|
||||
|
||||
const reporter = sinon.spy()
|
||||
const opts = testDefaults()
|
||||
|
||||
f.copy(linkedPkgName, linkedPkgPath)
|
||||
const manifest = await link(
|
||||
[linkedPkgPath],
|
||||
path.join(process.cwd(), 'node_modules'),
|
||||
{
|
||||
...opts,
|
||||
dir: process.cwd(),
|
||||
manifest: {},
|
||||
reporter,
|
||||
})
|
||||
symlinkDir.sync(linkedPkgPath, path.resolve('node_modules/@pnpm.e2e/hello-world-js-bin'))
|
||||
|
||||
expect(reporter.calledWithMatch({
|
||||
added: {
|
||||
dependencyType: undefined,
|
||||
linkedFrom: linkedPkgPath,
|
||||
name: '@pnpm.e2e/hello-world-js-bin',
|
||||
realName: '@pnpm.e2e/hello-world-js-bin',
|
||||
version: '1.0.0',
|
||||
},
|
||||
level: 'debug',
|
||||
name: 'pnpm:root',
|
||||
prefix: process.cwd(),
|
||||
} as RootLog)).toBeTruthy()
|
||||
|
||||
await install(manifest, opts)
|
||||
await install({}, testDefaults())
|
||||
|
||||
expect(project.requireModule('@pnpm.e2e/hello-world-js-bin/package.json').isLocal).toBeTruthy()
|
||||
})
|
||||
@@ -119,35 +56,12 @@ test('relative link is rewritten by named installation to regular dependency', a
|
||||
const linkedPkgName = 'hello-world-js-bin'
|
||||
const linkedPkgPath = path.resolve('..', linkedPkgName)
|
||||
|
||||
const reporter = sinon.spy()
|
||||
const opts = testDefaults({ fastUnpack: false })
|
||||
|
||||
f.copy(linkedPkgName, linkedPkgPath)
|
||||
let manifest = await link(
|
||||
[linkedPkgPath],
|
||||
path.join(process.cwd(), 'node_modules'),
|
||||
{
|
||||
...opts,
|
||||
dir: process.cwd(),
|
||||
manifest: {},
|
||||
reporter,
|
||||
}
|
||||
)
|
||||
symlinkDir.sync(linkedPkgPath, path.resolve('node_modules/@pnpm.e2e/hello-world-js-bin'))
|
||||
|
||||
expect(reporter.calledWithMatch({
|
||||
added: {
|
||||
dependencyType: undefined,
|
||||
linkedFrom: linkedPkgPath,
|
||||
name: '@pnpm.e2e/hello-world-js-bin',
|
||||
realName: '@pnpm.e2e/hello-world-js-bin',
|
||||
version: '1.0.0',
|
||||
},
|
||||
level: 'debug',
|
||||
name: 'pnpm:root',
|
||||
prefix: process.cwd(),
|
||||
} as RootLog)).toBeTruthy()
|
||||
|
||||
manifest = await addDependenciesToPackage(manifest, ['@pnpm.e2e/hello-world-js-bin'], opts)
|
||||
const manifest = await addDependenciesToPackage({}, ['@pnpm.e2e/hello-world-js-bin'], opts)
|
||||
|
||||
expect(manifest.dependencies).toStrictEqual({ '@pnpm.e2e/hello-world-js-bin': '^1.0.0' })
|
||||
|
||||
@@ -160,91 +74,6 @@ test('relative link is rewritten by named installation to regular dependency', a
|
||||
expect(currentLockfile.importers['.'].dependencies?.['@pnpm.e2e/hello-world-js-bin'].version).toBe('1.0.0')
|
||||
})
|
||||
|
||||
test('relative link uses realpath when contained in a symlinked dir', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
// `process.cwd()` is now `.tmp/X/project`.
|
||||
|
||||
f.copy('symlink-workspace', path.resolve('../symlink-workspace'))
|
||||
|
||||
const app1RelPath = '../symlink-workspace/app1'
|
||||
const app2RelPath = '../symlink-workspace/app2'
|
||||
|
||||
const app1 = path.resolve(app1RelPath)
|
||||
const app2 = path.resolve(app2RelPath)
|
||||
|
||||
const dest = path.join(app2, 'packages/public')
|
||||
const src = path.resolve(app1, 'packages/public')
|
||||
|
||||
console.log(`${dest}->${src}`)
|
||||
|
||||
// We must manually create the symlink so it works in Windows too.
|
||||
await symlink(src, dest)
|
||||
|
||||
process.chdir(path.join(app2, '/packages/public/foo'))
|
||||
|
||||
// `process.cwd()` is now `.tmp/X/symlink-workspace/app2/packages/public/foo`.
|
||||
|
||||
const linkFrom = path.join(app1, '/packages/public/bar')
|
||||
const linkTo = path.join(app2, '/packages/public/foo', 'node_modules')
|
||||
|
||||
await link([linkFrom], linkTo, testDefaults({ manifest: {}, dir: process.cwd() }))
|
||||
|
||||
const linkToRelLink = fs.readlinkSync(path.join(linkTo, 'bar'))
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
expect(path.relative(linkToRelLink, path.join(src, 'bar'))).toBe('') // link points to real location
|
||||
} else {
|
||||
expect(linkToRelLink).toBe('../../bar')
|
||||
|
||||
// If we don't use real paths we get a link like this.
|
||||
expect(linkToRelLink).not.toBe('../../../../../app1/packages/public/bar')
|
||||
}
|
||||
})
|
||||
|
||||
test('throws error is package name is not defined', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
writeJsonFile.sync('../is-positive/package.json', { version: '1.0.0' })
|
||||
|
||||
const manifest = await addDependenciesToPackage({}, ['is-positive@1.0.0'], testDefaults())
|
||||
|
||||
try {
|
||||
await link(['../is-positive'], path.resolve('node_modules'), testDefaults({ manifest, dir: process.cwd() }))
|
||||
throw new Error('link package should fail')
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
expect(err.message).toBe('Package in ../is-positive must have a name field to be linked')
|
||||
expect(err.code).toBe('ERR_PNPM_INVALID_PACKAGE_NAME')
|
||||
}
|
||||
})
|
||||
|
||||
test('link should not change the type of the dependency', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
const linkedPkgName = 'hello-world-js-bin'
|
||||
const linkedPkgPath = path.resolve('..', linkedPkgName)
|
||||
|
||||
f.copy(linkedPkgName, linkedPkgPath)
|
||||
await link([`../${linkedPkgName}`], path.join(process.cwd(), 'node_modules'), testDefaults({
|
||||
dir: process.cwd(),
|
||||
manifest: {
|
||||
devDependencies: {
|
||||
'@pnpm.e2e/hello-world-js-bin': '*',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
project.isExecutable('.bin/hello-world-js-bin')
|
||||
|
||||
const wantedLockfile = project.readLockfile()
|
||||
expect(wantedLockfile.importers['.'].devDependencies).toStrictEqual({
|
||||
'@pnpm.e2e/hello-world-js-bin': {
|
||||
version: 'link:../hello-world-js-bin',
|
||||
specifier: '*',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// test.skip('relative link when an external lockfile is used', async () => {
|
||||
// const projects = prepare(t, [
|
||||
// {
|
||||
|
||||
@@ -5,11 +5,11 @@ import { fixtures } from '@pnpm/test-fixtures'
|
||||
import {
|
||||
addDependenciesToPackage,
|
||||
install,
|
||||
link,
|
||||
mutateModulesInSingleProject,
|
||||
} from '@pnpm/core'
|
||||
import { type ProjectRootDir } from '@pnpm/types'
|
||||
import sinon from 'sinon'
|
||||
import symlinkDir from 'symlink-dir'
|
||||
import { testDefaults } from './utils'
|
||||
|
||||
const f = fixtures(__dirname)
|
||||
@@ -23,7 +23,7 @@ test('prune removes extraneous packages', async () => {
|
||||
manifest = await addDependenciesToPackage(manifest, ['applyq@0.2.1'], { ...opts, targetDependenciesField: 'devDependencies' })
|
||||
manifest = await addDependenciesToPackage(manifest, ['fnumber@0.1.0'], { ...opts, targetDependenciesField: 'optionalDependencies' })
|
||||
manifest = await addDependenciesToPackage(manifest, ['is-positive@2.0.0', '@zkochan/logger@0.1.0'], opts)
|
||||
manifest = await link([linkedPkg], path.resolve('node_modules'), { ...opts, manifest, dir: process.cwd() })
|
||||
symlinkDir.sync(linkedPkg, path.resolve('node_modules/@pnpm.e2e/hello-world-js-bin'))
|
||||
|
||||
project.has('@pnpm.e2e/hello-world-js-bin') // external link added
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { type ProjectRootDir, type PackageManifest } from '@pnpm/types'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import symlinkDir from 'symlink-dir'
|
||||
import {
|
||||
addDependenciesToPackage,
|
||||
link,
|
||||
mutateModules,
|
||||
mutateModulesInSingleProject,
|
||||
} from '@pnpm/core'
|
||||
@@ -195,15 +195,16 @@ test('relative link is uninstalled', async () => {
|
||||
const linkedPkgPath = path.resolve('..', linkedPkgName)
|
||||
|
||||
f.copy(linkedPkgName, linkedPkgPath)
|
||||
const manifest = await link([`../${linkedPkgName}`], path.join(process.cwd(), 'node_modules'), opts as (typeof opts & { dir: string, manifest: PackageManifest }))
|
||||
symlinkDir.sync(linkedPkgPath, path.resolve('node_modules/@pnpm.e2e/hello-world-js-bin'))
|
||||
project.has('@pnpm.e2e/hello-world-js-bin')
|
||||
await mutateModulesInSingleProject({
|
||||
dependencyNames: [linkedPkgName],
|
||||
manifest,
|
||||
dependencyNames: ['@pnpm.e2e/hello-world-js-bin'],
|
||||
manifest: {},
|
||||
mutation: 'uninstallSome',
|
||||
rootDir: process.cwd() as ProjectRootDir,
|
||||
}, opts)
|
||||
|
||||
project.hasNot(linkedPkgName)
|
||||
project.hasNot('@pnpm.e2e/hello-world-js-bin')
|
||||
})
|
||||
|
||||
test('pendingBuilds gets updated after uninstall', async () => {
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import {
|
||||
addDependenciesToPackage,
|
||||
install,
|
||||
link,
|
||||
mutateModulesInSingleProject,
|
||||
} from '@pnpm/core'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { WANTED_LOCKFILE } from '@pnpm/constants'
|
||||
import { addDistTag } from '@pnpm/registry-mock'
|
||||
import { type ProjectRootDir } from '@pnpm/types'
|
||||
import sinon from 'sinon'
|
||||
import writeJsonFile from 'write-json-file'
|
||||
import isInnerLink from 'is-inner-link'
|
||||
import { testDefaults } from './utils'
|
||||
|
||||
test('unlink 1 package that exists in package.json', async () => {
|
||||
const project = prepareEmpty()
|
||||
process.chdir('..')
|
||||
|
||||
await Promise.all([
|
||||
writeJsonFile('is-subdir/package.json', {
|
||||
dependencies: {
|
||||
'is-windows': '^1.0.0',
|
||||
},
|
||||
name: 'is-subdir',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
writeJsonFile('is-positive/package.json', {
|
||||
name: 'is-positive',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
])
|
||||
|
||||
// TODO: unset useLockfileV6
|
||||
const opts = testDefaults({ fastUnpack: false, store: path.resolve('.store'), useLockfileV6: false })
|
||||
|
||||
let manifest = await link(
|
||||
['is-subdir', 'is-positive'],
|
||||
path.join('project', 'node_modules'),
|
||||
{
|
||||
...opts,
|
||||
dir: path.resolve('project'),
|
||||
manifest: {
|
||||
dependencies: {
|
||||
'is-positive': '^1.0.0',
|
||||
'is-subdir': '^1.0.0',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
process.chdir('project')
|
||||
|
||||
manifest = await install(manifest, opts)
|
||||
|
||||
await mutateModulesInSingleProject({
|
||||
dependencyNames: ['is-subdir'],
|
||||
manifest,
|
||||
mutation: 'unlinkSome',
|
||||
rootDir: process.cwd() as ProjectRootDir,
|
||||
}, opts)
|
||||
|
||||
expect(typeof project.requireModule('is-subdir')).toBe('function')
|
||||
expect((await isInnerLink('node_modules', 'is-positive')).isInner).toBeFalsy()
|
||||
})
|
||||
|
||||
test("don't update package when unlinking", async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.0.0', distTag: 'latest' })
|
||||
const opts = testDefaults({ dir: process.cwd() })
|
||||
let manifest = await addDependenciesToPackage({}, ['@pnpm.e2e/foo'], opts)
|
||||
|
||||
process.chdir('..')
|
||||
|
||||
writeJsonFile.sync('foo/package.json', {
|
||||
name: '@pnpm.e2e/foo',
|
||||
version: '100.0.0',
|
||||
})
|
||||
|
||||
manifest = await link(['foo'], path.join('project', 'node_modules'), { ...opts, manifest })
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
process.chdir('project')
|
||||
await mutateModulesInSingleProject({
|
||||
dependencyNames: ['@pnpm.e2e/foo'],
|
||||
manifest,
|
||||
mutation: 'unlinkSome',
|
||||
rootDir: process.cwd() as ProjectRootDir,
|
||||
}, opts)
|
||||
|
||||
expect(project.requireModule('@pnpm.e2e/foo/package.json').version).toBe('100.0.0')
|
||||
})
|
||||
|
||||
test(`don't update package when unlinking. Initial link is done on a package w/o ${WANTED_LOCKFILE}`, async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
const opts = testDefaults({ dir: process.cwd(), resolutionMode: 'lowest-direct' })
|
||||
process.chdir('..')
|
||||
|
||||
writeJsonFile.sync('foo/package.json', {
|
||||
name: '@pnpm.e2e/foo',
|
||||
version: '100.0.0',
|
||||
})
|
||||
|
||||
const manifest = await link(['foo'], path.join('project', 'node_modules'), {
|
||||
...opts,
|
||||
manifest: {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/foo': '^100.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
await addDistTag({ package: '@pnpm.e2e/foo', version: '100.1.0', distTag: 'latest' })
|
||||
|
||||
process.chdir('project')
|
||||
const unlinkResult = await mutateModulesInSingleProject({
|
||||
dependencyNames: ['@pnpm.e2e/foo'],
|
||||
manifest,
|
||||
mutation: 'unlinkSome',
|
||||
rootDir: process.cwd() as ProjectRootDir,
|
||||
}, opts)
|
||||
|
||||
expect(project.requireModule('@pnpm.e2e/foo/package.json').version).toBe('100.0.0')
|
||||
expect(unlinkResult.manifest.dependencies).toStrictEqual({ '@pnpm.e2e/foo': '^100.0.0' })
|
||||
})
|
||||
|
||||
test('unlink 2 packages. One of them exists in package.json', async () => {
|
||||
const project = prepareEmpty()
|
||||
const opts = testDefaults({ fastUnpack: false, dir: process.cwd() })
|
||||
process.chdir('..')
|
||||
|
||||
await Promise.all([
|
||||
writeJsonFile('is-subdir/package.json', {
|
||||
dependencies: {
|
||||
'is-windows': '^1.0.0',
|
||||
},
|
||||
name: 'is-subdir',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
writeJsonFile('is-positive/package.json', {
|
||||
name: 'is-positive',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
])
|
||||
|
||||
const manifest = await link(['is-subdir', 'is-positive'], path.join('project', 'node_modules'), {
|
||||
...opts,
|
||||
manifest: {
|
||||
dependencies: {
|
||||
'is-subdir': '^1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
process.chdir('project')
|
||||
await mutateModulesInSingleProject({
|
||||
dependencyNames: ['is-subdir', 'is-positive'],
|
||||
manifest,
|
||||
mutation: 'unlinkSome',
|
||||
rootDir: process.cwd() as ProjectRootDir,
|
||||
}, opts)
|
||||
|
||||
expect(typeof project.requireModule('is-subdir')).toBe('function')
|
||||
expect(fs.existsSync(path.join('node_modules', 'is-positive'))).toBeFalsy()
|
||||
})
|
||||
|
||||
test('unlink all packages', async () => {
|
||||
const project = prepareEmpty()
|
||||
const opts = testDefaults({ fastUnpack: false, dir: process.cwd() })
|
||||
process.chdir('..')
|
||||
|
||||
await Promise.all([
|
||||
writeJsonFile('is-subdir/package.json', {
|
||||
dependencies: {
|
||||
'is-windows': '^1.0.0',
|
||||
},
|
||||
name: 'is-subdir',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
writeJsonFile('logger/package.json', {
|
||||
name: '@zkochan/logger',
|
||||
version: '0.1.0',
|
||||
}),
|
||||
])
|
||||
|
||||
const manifest = await link(['is-subdir', 'logger'], path.join('project', 'node_modules'), {
|
||||
...opts,
|
||||
manifest: {
|
||||
dependencies: {
|
||||
'@zkochan/logger': '^0.1.0',
|
||||
'is-subdir': '^1.0.0',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await mutateModulesInSingleProject({
|
||||
manifest,
|
||||
mutation: 'unlink',
|
||||
rootDir: path.resolve('project') as ProjectRootDir,
|
||||
}, opts)
|
||||
|
||||
expect(typeof project.requireModule('is-subdir')).toBe('function')
|
||||
expect(typeof project.requireModule('@zkochan/logger')).toBe('object')
|
||||
})
|
||||
|
||||
test("don't warn about scoped packages when running unlink w/o params", async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const manifest = await addDependenciesToPackage({}, ['@zkochan/logger'], testDefaults())
|
||||
|
||||
const reporter = sinon.spy()
|
||||
await mutateModulesInSingleProject({
|
||||
manifest,
|
||||
mutation: 'unlink',
|
||||
rootDir: process.cwd() as ProjectRootDir,
|
||||
}, testDefaults({ reporter }))
|
||||
|
||||
expect(reporter.calledWithMatch({
|
||||
level: 'warn',
|
||||
message: '@zkochan/logger is not an external link',
|
||||
})).toBeFalsy()
|
||||
})
|
||||
|
||||
test("don't unlink package that is not a link", async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const reporter = sinon.spy()
|
||||
|
||||
const manifest = await addDependenciesToPackage({}, ['is-positive'], testDefaults())
|
||||
|
||||
await mutateModulesInSingleProject({
|
||||
dependencyNames: ['is-positive'],
|
||||
manifest,
|
||||
mutation: 'unlinkSome',
|
||||
rootDir: process.cwd() as ProjectRootDir,
|
||||
}, testDefaults({ reporter }))
|
||||
|
||||
expect(reporter.calledWithMatch({
|
||||
level: 'warn',
|
||||
message: 'is-positive is not an external link',
|
||||
})).toBeTruthy()
|
||||
})
|
||||
|
||||
test('unlink would remove global bin', async () => {
|
||||
prepareEmpty()
|
||||
process.chdir('..')
|
||||
fs.mkdirSync('bin')
|
||||
fs.mkdirSync('is-subdir')
|
||||
fs.writeFileSync('is-subdir/index.js', ' ')
|
||||
|
||||
await Promise.all([
|
||||
writeJsonFile('is-subdir/package.json', {
|
||||
bin: 'index.js',
|
||||
dependencies: {
|
||||
'is-windows': '^1.0.0',
|
||||
},
|
||||
name: 'is-subdir',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
])
|
||||
|
||||
const opts = testDefaults({
|
||||
fastUnpack: false,
|
||||
globalBin: path.resolve('bin'),
|
||||
linkToBin: path.resolve('bin'),
|
||||
store: path.resolve('.store'),
|
||||
})
|
||||
|
||||
const manifest = await link(
|
||||
['is-subdir'],
|
||||
path.join('project', 'node_modules'),
|
||||
{
|
||||
...opts,
|
||||
dir: path.resolve('project'),
|
||||
manifest: {
|
||||
dependencies: {
|
||||
'is-subdir': '^1.0.0',
|
||||
},
|
||||
name: 'is-subdir',
|
||||
},
|
||||
}
|
||||
)
|
||||
expect(fs.existsSync(path.resolve('bin/is-subdir'))).toBeTruthy()
|
||||
|
||||
await mutateModulesInSingleProject({
|
||||
dependencyNames: ['is-subdir'],
|
||||
manifest,
|
||||
mutation: 'unlinkSome',
|
||||
rootDir: process.cwd() as ProjectRootDir,
|
||||
}, opts)
|
||||
|
||||
expect(fs.existsSync(path.resolve('bin/is-subdir'))).toBeFalsy()
|
||||
})
|
||||
@@ -81,6 +81,7 @@
|
||||
"@pnpm/plugin-commands-rebuild": "workspace:*",
|
||||
"@pnpm/pnpmfile": "workspace:*",
|
||||
"@pnpm/read-project-manifest": "workspace:*",
|
||||
"@pnpm/write-project-manifest": "workspace:*",
|
||||
"@pnpm/resolver-base": "workspace:*",
|
||||
"@pnpm/semver-diff": "catalog:",
|
||||
"@pnpm/sort-packages": "workspace:*",
|
||||
@@ -101,7 +102,6 @@
|
||||
"mem": "catalog:",
|
||||
"p-filter": "catalog:",
|
||||
"p-limit": "catalog:",
|
||||
"path-absolute": "catalog:",
|
||||
"ramda": "catalog:",
|
||||
"render-help": "catalog:",
|
||||
"version-selector-type": "catalog:",
|
||||
|
||||
@@ -308,6 +308,7 @@ export type InstallCommandOptions = Pick<Config,
|
||||
original: string[]
|
||||
}
|
||||
fixLockfile?: boolean
|
||||
frozenLockfileIfExists?: boolean
|
||||
useBetaCli?: boolean
|
||||
pruneDirectDependencies?: boolean
|
||||
pruneStore?: boolean
|
||||
@@ -330,9 +331,11 @@ export async function handler (opts: InstallCommandOptions): Promise<void> {
|
||||
const fetchFullMetadata: true | undefined = opts.rootProjectManifest?.pnpm?.supportedArchitectures?.libc && true
|
||||
const installDepsOptions: InstallDepsOptions = {
|
||||
...opts,
|
||||
frozenLockfileIfExists: isCI && !opts.lockfileOnly &&
|
||||
frozenLockfileIfExists: opts.frozenLockfileIfExists ?? (
|
||||
isCI && !opts.lockfileOnly &&
|
||||
typeof opts.rawLocalConfig['frozen-lockfile'] === 'undefined' &&
|
||||
typeof opts.rawLocalConfig['prefer-frozen-lockfile'] === 'undefined',
|
||||
typeof opts.rawLocalConfig['prefer-frozen-lockfile'] === 'undefined'
|
||||
),
|
||||
include,
|
||||
includeDirect: include,
|
||||
prepareExecutionEnv: prepareExecutionEnv.bind(null, opts),
|
||||
|
||||
@@ -1,53 +1,44 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
docsUrl,
|
||||
getConfig,
|
||||
readProjectManifest,
|
||||
readProjectManifestOnly,
|
||||
tryReadProjectManifest,
|
||||
type ReadProjectManifestOpts,
|
||||
} from '@pnpm/cli-utils'
|
||||
import { UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help'
|
||||
import { type Config, getOptionsFromRootManifest, types as allTypes } from '@pnpm/config'
|
||||
import { type Config, types as allTypes } from '@pnpm/config'
|
||||
import { DEPENDENCIES_FIELDS, type ProjectManifest, type Project } from '@pnpm/types'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { findWorkspaceDir } from '@pnpm/find-workspace-dir'
|
||||
import { arrayOfWorkspacePackagesToMap } from '@pnpm/get-context'
|
||||
import { findWorkspacePackages } from '@pnpm/workspace.find-packages'
|
||||
import { type StoreController } from '@pnpm/package-store'
|
||||
import { createOrConnectStoreControllerCached, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
|
||||
import { writeProjectManifest } from '@pnpm/write-project-manifest'
|
||||
import {
|
||||
addDependenciesToPackage,
|
||||
install,
|
||||
type InstallOptions,
|
||||
link,
|
||||
type LinkFunctionOptions,
|
||||
type WorkspacePackages,
|
||||
} from '@pnpm/core'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { type Project } from '@pnpm/types'
|
||||
import pLimit from 'p-limit'
|
||||
import pathAbsolute from 'path-absolute'
|
||||
import pick from 'ramda/src/pick'
|
||||
import partition from 'ramda/src/partition'
|
||||
import renderHelp from 'render-help'
|
||||
import * as installCommand from './install'
|
||||
import { getSaveType } from './getSaveType'
|
||||
import * as install from './install'
|
||||
|
||||
// @ts-expect-error
|
||||
const isWindows = process.platform === 'win32' || global['FAKE_WINDOWS']
|
||||
const isFilespec = isWindows ? /^(?:[.]|~[/]|[/\\]|[a-zA-Z]:)/ : /^(?:[.]|~[/]|[/]|[a-zA-Z]:)/
|
||||
const installLimit = pLimit(4)
|
||||
|
||||
type LinkOpts = CreateStoreControllerOptions & Pick<Config,
|
||||
type LinkOpts = Pick<Config,
|
||||
| 'bin'
|
||||
| 'cliOptions'
|
||||
| 'engineStrict'
|
||||
| 'rootProjectManifest'
|
||||
| 'rootProjectManifestDir'
|
||||
| 'saveDev'
|
||||
| 'saveOptional'
|
||||
| 'saveProd'
|
||||
| 'workspaceDir'
|
||||
| 'workspacePackagePatterns'
|
||||
| 'sharedWorkspaceLockfile'
|
||||
> & Partial<Pick<Config, 'linkWorkspacePackages'>>
|
||||
| 'globalPkgDir'
|
||||
> & Partial<Pick<Config, 'linkWorkspacePackages'>> & install.InstallCommandOptions
|
||||
|
||||
export const rcOptionsTypes = cliOptionsTypes
|
||||
|
||||
@@ -77,21 +68,13 @@ export function help (): string {
|
||||
{
|
||||
title: 'Options',
|
||||
|
||||
list: [
|
||||
...UNIVERSAL_OPTIONS,
|
||||
{
|
||||
description: 'Link package to/from global node_modules',
|
||||
name: '--global',
|
||||
shortAlias: '-g',
|
||||
},
|
||||
],
|
||||
list: UNIVERSAL_OPTIONS,
|
||||
},
|
||||
],
|
||||
url: docsUrl('link'),
|
||||
usages: [
|
||||
'pnpm link <dir>',
|
||||
'pnpm link --global (in package dir)',
|
||||
'pnpm link --global <pkg>',
|
||||
'pnpm link <dir|pkg name>',
|
||||
'pnpm link',
|
||||
],
|
||||
})
|
||||
}
|
||||
@@ -121,9 +104,6 @@ export async function handler (
|
||||
opts: LinkOpts,
|
||||
params?: string[]
|
||||
): Promise<void> {
|
||||
const cwd = process.cwd()
|
||||
|
||||
const storeControllerCache = new Map<string, Promise<{ dir: string, ctrl: StoreController }>>()
|
||||
let workspacePackagesArr: Project[]
|
||||
let workspacePackages!: WorkspacePackages
|
||||
if (opts.workspaceDir) {
|
||||
@@ -136,121 +116,66 @@ export async function handler (
|
||||
workspacePackages = new Map()
|
||||
}
|
||||
|
||||
const store = await createOrConnectStoreControllerCached(storeControllerCache, opts)
|
||||
const linkOpts = Object.assign(opts, {
|
||||
storeController: store.ctrl,
|
||||
storeDir: store.dir,
|
||||
targetDependenciesField: getSaveType(opts),
|
||||
workspacePackages,
|
||||
binsDir: opts.bin,
|
||||
})
|
||||
|
||||
const linkCwdDir = opts.cliOptions?.dir && opts.cliOptions?.global ? path.resolve(opts.cliOptions.dir) : cwd
|
||||
|
||||
// pnpm link
|
||||
// "pnpm link"
|
||||
if ((params == null) || (params.length === 0)) {
|
||||
const cwd = process.cwd()
|
||||
if (path.relative(linkOpts.dir, cwd) === '') {
|
||||
throw new PnpmError('LINK_BAD_PARAMS', 'You must provide a parameter')
|
||||
}
|
||||
|
||||
await checkPeerDeps(linkCwdDir, opts)
|
||||
await checkPeerDeps(cwd, opts)
|
||||
|
||||
const { manifest, writeProjectManifest } = await tryReadProjectManifest(opts.dir, opts)
|
||||
const newManifest = await addDependenciesToPackage(
|
||||
manifest ?? {},
|
||||
[`link:${linkCwdDir}`],
|
||||
linkOpts
|
||||
)
|
||||
await writeProjectManifest(newManifest)
|
||||
const newManifest = opts.rootProjectManifest ?? {}
|
||||
await addLinkToManifest(opts, newManifest, cwd, linkOpts.dir)
|
||||
await writeProjectManifest(path.join(opts.rootProjectManifestDir, 'package.json'), newManifest)
|
||||
await install.handler({
|
||||
...linkOpts,
|
||||
frozenLockfileIfExists: false,
|
||||
rootProjectManifest: newManifest,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const [pkgPaths, pkgNames] = partition((inp) => isFilespec.test(inp), params)
|
||||
|
||||
await Promise.all(
|
||||
pkgPaths.map(async (dir) => installLimit(async () => {
|
||||
const s = await createOrConnectStoreControllerCached(storeControllerCache, opts)
|
||||
const config = await getConfig(
|
||||
{ ...opts.cliOptions, dir },
|
||||
{
|
||||
excludeReporter: true,
|
||||
rcOptionsTypes: installCommand.rcOptionsTypes(),
|
||||
workspaceDir: await findWorkspaceDir(dir),
|
||||
}
|
||||
)
|
||||
await install(
|
||||
await readProjectManifestOnly(dir, opts), {
|
||||
...config,
|
||||
...getOptionsFromRootManifest(config.rootProjectManifestDir, config.rootProjectManifest ?? {}),
|
||||
include: {
|
||||
dependencies: config.production !== false,
|
||||
devDependencies: config.dev !== false,
|
||||
optionalDependencies: config.optional !== false,
|
||||
},
|
||||
storeController: s.ctrl,
|
||||
storeDir: s.dir,
|
||||
workspacePackages,
|
||||
} as InstallOptions
|
||||
)
|
||||
}))
|
||||
)
|
||||
|
||||
if (pkgNames.length > 0) {
|
||||
let globalPkgNames!: string[]
|
||||
if (opts.workspaceDir) {
|
||||
workspacePackagesArr = await findWorkspacePackages(opts.workspaceDir, {
|
||||
...opts,
|
||||
patterns: opts.workspacePackagePatterns,
|
||||
})
|
||||
|
||||
const pkgsFoundInWorkspace = workspacePackagesArr
|
||||
.filter(({ manifest }) => manifest.name && pkgNames.includes(manifest.name))
|
||||
pkgsFoundInWorkspace.forEach((pkgFromWorkspace) => pkgPaths.push(pkgFromWorkspace.rootDir))
|
||||
|
||||
if ((pkgsFoundInWorkspace.length > 0) && !linkOpts.targetDependenciesField) {
|
||||
linkOpts.targetDependenciesField = 'dependencies'
|
||||
}
|
||||
|
||||
globalPkgNames = pkgNames.filter((pkgName) => !pkgsFoundInWorkspace.some((pkgFromWorkspace) => pkgFromWorkspace.manifest.name === pkgName))
|
||||
} else {
|
||||
globalPkgNames = pkgNames
|
||||
}
|
||||
const globalPkgPath = pathAbsolute(opts.dir)
|
||||
globalPkgNames.forEach((pkgName) => pkgPaths.push(path.join(globalPkgPath, 'node_modules', pkgName)))
|
||||
}
|
||||
|
||||
const { manifest, writeProjectManifest } = await readProjectManifest(linkCwdDir, opts)
|
||||
pkgNames.forEach((pkgName) => pkgPaths.push(path.join(opts.globalPkgDir, 'node_modules', pkgName)))
|
||||
|
||||
const newManifest = opts.rootProjectManifest ?? {}
|
||||
await Promise.all(
|
||||
pkgPaths.map(async (dir) => {
|
||||
await addLinkToManifest(opts, newManifest, dir, opts.dir)
|
||||
await checkPeerDeps(dir, opts)
|
||||
})
|
||||
)
|
||||
|
||||
const linkConfig = await getConfig(
|
||||
{ ...opts.cliOptions, dir: cwd },
|
||||
{
|
||||
excludeReporter: true,
|
||||
rcOptionsTypes: installCommand.rcOptionsTypes(),
|
||||
workspaceDir: await findWorkspaceDir(cwd),
|
||||
}
|
||||
)
|
||||
const storeL = await createOrConnectStoreControllerCached(storeControllerCache, linkConfig)
|
||||
const newManifest = await link(pkgPaths, path.join(linkCwdDir, 'node_modules'), {
|
||||
...linkConfig,
|
||||
targetDependenciesField: linkOpts.targetDependenciesField,
|
||||
storeController: storeL.ctrl,
|
||||
storeDir: storeL.dir,
|
||||
manifest,
|
||||
} as LinkFunctionOptions)
|
||||
if (!opts.cliOptions?.global) {
|
||||
await writeProjectManifest(newManifest)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Array.from(storeControllerCache.values())
|
||||
.map(async (storeControllerPromise) => {
|
||||
const storeControllerHolder = await storeControllerPromise
|
||||
await storeControllerHolder.ctrl.close()
|
||||
})
|
||||
)
|
||||
await writeProjectManifest(path.join(opts.rootProjectManifestDir, 'package.json'), newManifest)
|
||||
await install.handler({
|
||||
...linkOpts,
|
||||
frozenLockfileIfExists: false,
|
||||
rootProjectManifest: newManifest,
|
||||
})
|
||||
}
|
||||
|
||||
async function addLinkToManifest (opts: ReadProjectManifestOpts, manifest: ProjectManifest, linkedDepDir: string, dependentDir: string) {
|
||||
if (!manifest.pnpm) {
|
||||
manifest.pnpm = {
|
||||
overrides: {},
|
||||
}
|
||||
} else if (!manifest.pnpm.overrides) {
|
||||
manifest.pnpm.overrides = {}
|
||||
}
|
||||
const { manifest: linkedManifest } = await tryReadProjectManifest(linkedDepDir, opts)
|
||||
const linkedPkgName = linkedManifest?.name ?? path.basename(linkedDepDir)
|
||||
const linkedPkgSpec = `link:${path.relative(dependentDir, linkedDepDir)}`
|
||||
manifest.pnpm.overrides![linkedPkgName] = linkedPkgSpec
|
||||
if (DEPENDENCIES_FIELDS.every((depField) => manifest[depField]?.[linkedPkgName] == null)) {
|
||||
manifest.dependencies = manifest.dependencies ?? {}
|
||||
manifest.dependencies[linkedPkgName] = linkedPkgSpec
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
type InstallOptions,
|
||||
type MutatedProject,
|
||||
mutateModules,
|
||||
type MutateModulesResult,
|
||||
type ProjectOptions,
|
||||
type UpdateMatchingFunction,
|
||||
type WorkspacePackages,
|
||||
@@ -103,7 +102,7 @@ export async function recursive (
|
||||
allProjects: Project[],
|
||||
params: string[],
|
||||
opts: RecursiveOptions,
|
||||
cmdFullName: 'install' | 'add' | 'remove' | 'unlink' | 'update' | 'import'
|
||||
cmdFullName: 'install' | 'add' | 'remove' | 'update' | 'import'
|
||||
): Promise<boolean | string> {
|
||||
if (allProjects.length === 0) {
|
||||
// It might make sense to throw an exception in this case
|
||||
@@ -121,9 +120,7 @@ export async function recursive (
|
||||
|
||||
const store = await createOrConnectStoreController(opts)
|
||||
|
||||
const workspacePackages: WorkspacePackages = cmdFullName !== 'unlink'
|
||||
? arrayOfWorkspacePackagesToMap(allProjects) as WorkspacePackages
|
||||
: new Map()
|
||||
const workspacePackages: WorkspacePackages = arrayOfWorkspacePackagesToMap(allProjects) as WorkspacePackages
|
||||
const targetDependenciesField = getSaveType(opts)
|
||||
const rootManifestDir = (opts.lockfileDir ?? opts.dir) as ProjectRootDir
|
||||
const installOpts = Object.assign(opts, {
|
||||
@@ -313,9 +310,6 @@ export async function recursive (
|
||||
|
||||
let action!: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
switch (cmdFullName) {
|
||||
case 'unlink':
|
||||
action = (currentInput.length === 0 ? unlink : unlinkPkgs.bind(null, currentInput))
|
||||
break
|
||||
case 'remove':
|
||||
action = async (manifest: PackageManifest, opts: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const mutationResult = await mutateModules([
|
||||
@@ -385,8 +379,7 @@ export async function recursive (
|
||||
!opts.lockfileOnly && !opts.ignoreScripts && (
|
||||
cmdFullName === 'add' ||
|
||||
cmdFullName === 'install' ||
|
||||
cmdFullName === 'update' ||
|
||||
cmdFullName === 'unlink'
|
||||
cmdFullName === 'update'
|
||||
)
|
||||
) {
|
||||
await rebuild.handler({
|
||||
@@ -406,31 +399,6 @@ export async function recursive (
|
||||
return true
|
||||
}
|
||||
|
||||
async function unlink (manifest: ProjectManifest, opts: any): Promise<MutateModulesResult> { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
return mutateModules(
|
||||
[
|
||||
{
|
||||
mutation: 'unlink',
|
||||
rootDir: opts.dir,
|
||||
},
|
||||
],
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
async function unlinkPkgs (dependencyNames: string[], manifest: ProjectManifest, opts: any): Promise<MutateModulesResult> { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
return mutateModules(
|
||||
[
|
||||
{
|
||||
dependencyNames,
|
||||
mutation: 'unlinkSome',
|
||||
rootDir: opts.dir,
|
||||
},
|
||||
],
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
function calculateRepositoryRoot (
|
||||
workspaceDir: string,
|
||||
projectDirs: string[]
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { docsUrl, readProjectManifestOnly } from '@pnpm/cli-utils'
|
||||
import path from 'path'
|
||||
import { docsUrl } from '@pnpm/cli-utils'
|
||||
import { UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help'
|
||||
import { type Config, getOptionsFromRootManifest } from '@pnpm/config'
|
||||
import { createOrConnectStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
|
||||
import { mutateModulesInSingleProject } from '@pnpm/core'
|
||||
import { type ProjectRootDir } from '@pnpm/types'
|
||||
import { writeProjectManifest } from '@pnpm/write-project-manifest'
|
||||
import renderHelp from 'render-help'
|
||||
import { cliOptionsTypes, rcOptionsTypes } from './install'
|
||||
import { recursive } from './recursive'
|
||||
import * as install from './install'
|
||||
|
||||
export { cliOptionsTypes, rcOptionsTypes }
|
||||
export const cliOptionsTypes = install.cliOptionsTypes
|
||||
|
||||
export const rcOptionsTypes = install.rcOptionsTypes
|
||||
|
||||
export const commandNames = ['unlink', 'dislink']
|
||||
|
||||
@@ -41,57 +40,25 @@ For options that may be used with `-r`, see "pnpm help recursive"',
|
||||
}
|
||||
|
||||
export async function handler (
|
||||
opts: CreateStoreControllerOptions &
|
||||
Pick<Config,
|
||||
| 'allProjects'
|
||||
| 'allProjectsGraph'
|
||||
| 'bail'
|
||||
| 'bin'
|
||||
| 'engineStrict'
|
||||
| 'hooks'
|
||||
| 'linkWorkspacePackages'
|
||||
| 'saveWorkspaceProtocol'
|
||||
| 'selectedProjectsGraph'
|
||||
| 'rawLocalConfig'
|
||||
| 'registries'
|
||||
| 'rootProjectManifest'
|
||||
| 'rootProjectManifestDir'
|
||||
| 'pnpmfile'
|
||||
| 'workspaceDir'
|
||||
> & {
|
||||
recursive?: boolean
|
||||
},
|
||||
opts: install.InstallCommandOptions,
|
||||
params: string[]
|
||||
): Promise<void> {
|
||||
if (opts.recursive && (opts.allProjects != null) && (opts.selectedProjectsGraph != null) && opts.workspaceDir) {
|
||||
await recursive(opts.allProjects, params, {
|
||||
...opts,
|
||||
allProjectsGraph: opts.allProjectsGraph!,
|
||||
selectedProjectsGraph: opts.selectedProjectsGraph,
|
||||
workspaceDir: opts.workspaceDir,
|
||||
}, 'unlink')
|
||||
return
|
||||
}
|
||||
const store = await createOrConnectStoreController(opts)
|
||||
const unlinkOpts = Object.assign(opts, {
|
||||
...getOptionsFromRootManifest(opts.rootProjectManifestDir, opts.rootProjectManifest ?? {}),
|
||||
globalBin: opts.bin,
|
||||
storeController: store.ctrl,
|
||||
storeDir: store.dir,
|
||||
})
|
||||
): Promise<undefined | string> {
|
||||
if (!opts.rootProjectManifest?.pnpm?.overrides) return 'Nothing to unlink'
|
||||
|
||||
if (!params || (params.length === 0)) {
|
||||
await mutateModulesInSingleProject({
|
||||
dependencyNames: params,
|
||||
manifest: await readProjectManifestOnly(opts.dir, opts),
|
||||
mutation: 'unlinkSome',
|
||||
rootDir: opts.dir as ProjectRootDir,
|
||||
}, unlinkOpts)
|
||||
return
|
||||
for (const selector in opts.rootProjectManifest.pnpm.overrides) {
|
||||
if (opts.rootProjectManifest.pnpm.overrides[selector].startsWith('link:')) {
|
||||
delete opts.rootProjectManifest.pnpm.overrides[selector]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const selector in opts.rootProjectManifest.pnpm.overrides) {
|
||||
if (opts.rootProjectManifest.pnpm.overrides[selector].startsWith('link:') && params.includes(selector)) {
|
||||
delete opts.rootProjectManifest.pnpm.overrides[selector]
|
||||
}
|
||||
}
|
||||
}
|
||||
await mutateModulesInSingleProject({
|
||||
manifest: await readProjectManifestOnly(opts.dir, opts),
|
||||
mutation: 'unlink',
|
||||
rootDir: opts.dir as ProjectRootDir,
|
||||
}, unlinkOpts)
|
||||
await writeProjectManifest(path.join(opts.rootProjectManifestDir, 'package.json'), opts.rootProjectManifest)
|
||||
await install.handler(opts)
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import { install, link } from '@pnpm/plugin-commands-installation'
|
||||
import { prepare, preparePackages } from '@pnpm/prepare'
|
||||
import { assertProject, isExecutable } from '@pnpm/assert-project'
|
||||
import { isExecutable } from '@pnpm/assert-project'
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { logger } from '@pnpm/logger'
|
||||
import { sync as loadJsonFile } from 'load-json-file'
|
||||
@@ -25,28 +24,27 @@ test('linking multiple packages', async () => {
|
||||
|
||||
process.chdir('linked-foo')
|
||||
|
||||
console.log('linking linked-foo to global package')
|
||||
const linkOpts = {
|
||||
// linking linked-foo to global package
|
||||
await link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
bin: path.join(globalDir, 'bin'),
|
||||
dir: globalDir,
|
||||
}
|
||||
await link.handler({
|
||||
...linkOpts,
|
||||
globalPkgDir: globalDir,
|
||||
rootProjectManifestDir: globalDir,
|
||||
})
|
||||
|
||||
process.chdir('..')
|
||||
process.chdir('project')
|
||||
|
||||
await link.handler({
|
||||
...linkOpts,
|
||||
...DEFAULT_OPTS,
|
||||
dir: process.cwd(),
|
||||
globalPkgDir: globalDir,
|
||||
rootProjectManifestDir: process.cwd(),
|
||||
}, ['linked-foo', '../linked-bar'])
|
||||
|
||||
project.has('linked-foo')
|
||||
project.has('linked-bar')
|
||||
|
||||
const modules = readYamlFile<any>('../linked-bar/node_modules/.modules.yaml') // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
expect(modules.hoistPattern).toStrictEqual(['*']) // the linked package used its own configs during installation // eslint-disable-line @typescript-eslint/dot-notation
|
||||
})
|
||||
|
||||
test('link global bin', async function () {
|
||||
@@ -66,11 +64,10 @@ test('link global bin', async function () {
|
||||
|
||||
await link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
cliOptions: {
|
||||
global: true,
|
||||
},
|
||||
bin: globalBin,
|
||||
dir: globalDir,
|
||||
globalPkgDir: globalDir,
|
||||
rootProjectManifestDir: globalDir,
|
||||
})
|
||||
process.env[PATH] = oldPath
|
||||
|
||||
@@ -79,35 +76,6 @@ test('link global bin', async function () {
|
||||
}, path.join(globalBin, 'package-with-bin'))
|
||||
})
|
||||
|
||||
test('link to global bin from the specified directory', async function () {
|
||||
prepare()
|
||||
process.chdir('..')
|
||||
|
||||
const globalDir = path.resolve('global')
|
||||
const globalBin = path.join(globalDir, 'bin')
|
||||
const oldPath = process.env[PATH]
|
||||
process.env[PATH] = `${globalBin}${path.delimiter}${oldPath ?? ''}`
|
||||
fs.mkdirSync(globalBin, { recursive: true })
|
||||
|
||||
await writePkg('./dir/package-with-bin-in-dir', { name: 'package-with-bin-in-dir', version: '1.0.0', bin: 'bin.js' })
|
||||
fs.writeFileSync('./dir/package-with-bin-in-dir/bin.js', '#!/usr/bin/env node\nconsole.log(/hi/)\n', 'utf8')
|
||||
|
||||
await link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
cliOptions: {
|
||||
global: true,
|
||||
dir: path.resolve('./dir/package-with-bin-in-dir'),
|
||||
},
|
||||
bin: globalBin,
|
||||
dir: globalDir,
|
||||
})
|
||||
process.env[PATH] = oldPath
|
||||
|
||||
isExecutable((value) => {
|
||||
expect(value).toBeTruthy()
|
||||
}, path.join(globalBin, 'package-with-bin-in-dir'))
|
||||
})
|
||||
|
||||
test('link a global package to the specified directory', async function () {
|
||||
const project = prepare({ dependencies: { 'global-package-with-bin': '0.0.0' } })
|
||||
process.chdir('..')
|
||||
@@ -126,11 +94,10 @@ test('link a global package to the specified directory', async function () {
|
||||
// link to global
|
||||
await link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
cliOptions: {
|
||||
global: true,
|
||||
},
|
||||
bin: globalBin,
|
||||
dir: globalDir,
|
||||
globalPkgDir: globalDir,
|
||||
rootProjectManifestDir: globalDir,
|
||||
})
|
||||
|
||||
process.chdir('..')
|
||||
@@ -139,13 +106,12 @@ test('link a global package to the specified directory', async function () {
|
||||
// link from global
|
||||
await link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
cliOptions: {
|
||||
global: true,
|
||||
dir: projectDir,
|
||||
},
|
||||
bin: globalBin,
|
||||
dir: globalDir,
|
||||
// bin: globalBin,
|
||||
dir: projectDir,
|
||||
saveProd: true, // @pnpm/config sets this setting to true when global is true. This should probably be changed.
|
||||
globalPkgDir: globalDir,
|
||||
rootProjectManifest: { dependencies: { 'global-package-with-bin': '0.0.0' } },
|
||||
rootProjectManifestDir: projectDir,
|
||||
}, ['global-package-with-bin'])
|
||||
|
||||
process.env[PATH] = oldPath
|
||||
@@ -169,19 +135,21 @@ test('relative link', async () => {
|
||||
await link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
dir: process.cwd(),
|
||||
globalPkgDir: '',
|
||||
rootProjectManifest: {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/hello-world-js-bin': '*',
|
||||
},
|
||||
},
|
||||
rootProjectManifestDir: process.cwd(),
|
||||
}, [`../${linkedPkgName}`])
|
||||
|
||||
project.isExecutable('.bin/hello-world-js-bin')
|
||||
|
||||
// The linked package has been installed successfully as well with bins linked
|
||||
// to node_modules/.bin
|
||||
const linkedProject = assertProject(linkedPkgPath)
|
||||
linkedProject.isExecutable('.bin/cowsay')
|
||||
|
||||
const wantedLockfile = project.readLockfile()
|
||||
expect(wantedLockfile.importers['.'].dependencies?.['@pnpm.e2e/hello-world-js-bin']).toStrictEqual({
|
||||
specifier: '*', // specifier of linked dependency added to ${WANTED_LOCKFILE}
|
||||
version: 'link:../hello-world-js-bin', // link added to wanted lockfile
|
||||
specifier: 'link:../hello-world-js-bin',
|
||||
version: 'link:../hello-world-js-bin',
|
||||
})
|
||||
|
||||
const currentLockfile = project.readCurrentLockfile()
|
||||
@@ -202,18 +170,20 @@ test('absolute link', async () => {
|
||||
await link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
dir: process.cwd(),
|
||||
globalPkgDir: '',
|
||||
rootProjectManifestDir: process.cwd(),
|
||||
rootProjectManifest: {
|
||||
dependencies: {
|
||||
'@pnpm.e2e/hello-world-js-bin': '*',
|
||||
},
|
||||
},
|
||||
}, [linkedPkgPath])
|
||||
|
||||
project.isExecutable('.bin/hello-world-js-bin')
|
||||
|
||||
// The linked package has been installed successfully as well with bins linked
|
||||
// to node_modules/.bin
|
||||
const linkedProject = assertProject(linkedPkgPath)
|
||||
linkedProject.isExecutable('.bin/cowsay')
|
||||
|
||||
const wantedLockfile = project.readLockfile()
|
||||
expect(wantedLockfile.importers['.'].dependencies?.['@pnpm.e2e/hello-world-js-bin']).toStrictEqual({
|
||||
specifier: '*', // specifier of linked dependency added to ${WANTED_LOCKFILE}
|
||||
specifier: 'link:../hello-world-js-bin', // specifier of linked dependency added to ${WANTED_LOCKFILE}
|
||||
version: 'link:../hello-world-js-bin', // link added to wanted lockfile
|
||||
})
|
||||
|
||||
@@ -222,18 +192,19 @@ test('absolute link', async () => {
|
||||
})
|
||||
|
||||
test('link --production', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
name: 'target',
|
||||
version: '1.0.0',
|
||||
const targetManifest = {
|
||||
name: 'target',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
dependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
}
|
||||
const projects = preparePackages([
|
||||
targetManifest,
|
||||
{
|
||||
name: 'source',
|
||||
version: '1.0.0',
|
||||
@@ -257,11 +228,11 @@ test('link --production', async () => {
|
||||
...DEFAULT_OPTS,
|
||||
cliOptions: { production: true },
|
||||
dir: process.cwd(),
|
||||
globalPkgDir: '',
|
||||
rootProjectManifestDir: process.cwd(),
|
||||
rootProjectManifest: targetManifest,
|
||||
}, ['../source'])
|
||||
|
||||
projects['source'].has('is-positive')
|
||||
projects['source'].hasNot('is-negative')
|
||||
|
||||
// --production should not have effect on the target
|
||||
projects['target'].has('is-positive')
|
||||
projects['target'].has('is-negative')
|
||||
@@ -274,6 +245,7 @@ test('link fails if nothing is linked', async () => {
|
||||
link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
dir: '',
|
||||
globalPkgDir: '',
|
||||
}, [])
|
||||
).rejects.toThrow(/You must provide a parameter/)
|
||||
})
|
||||
@@ -296,20 +268,21 @@ test('logger warns about peer dependencies when linking', async () => {
|
||||
|
||||
process.chdir('linked-with-peer-deps')
|
||||
|
||||
const linkOpts = {
|
||||
await link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
bin: path.join(globalDir, 'bin'),
|
||||
dir: globalDir,
|
||||
}
|
||||
await link.handler({
|
||||
...linkOpts,
|
||||
globalPkgDir: globalDir,
|
||||
rootProjectManifestDir: globalDir,
|
||||
})
|
||||
|
||||
process.chdir('..')
|
||||
process.chdir('project')
|
||||
|
||||
await link.handler({
|
||||
...linkOpts,
|
||||
...DEFAULT_OPTS,
|
||||
dir: process.cwd(),
|
||||
globalPkgDir: globalDir,
|
||||
}, ['linked-with-peer-deps'])
|
||||
|
||||
expect(warnMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
@@ -335,20 +308,22 @@ test('logger should not warn about peer dependencies when it is an empty object'
|
||||
|
||||
process.chdir('linked-with-empty-peer-deps')
|
||||
|
||||
const linkOpts = {
|
||||
await link.handler({
|
||||
...DEFAULT_OPTS,
|
||||
globalPkgDir: '',
|
||||
bin: path.join(globalDir, 'bin'),
|
||||
dir: globalDir,
|
||||
}
|
||||
await link.handler({
|
||||
...linkOpts,
|
||||
rootProjectManifestDir: globalDir,
|
||||
})
|
||||
|
||||
process.chdir('..')
|
||||
process.chdir('project')
|
||||
|
||||
await link.handler({
|
||||
...linkOpts,
|
||||
...DEFAULT_OPTS,
|
||||
globalPkgDir: globalDir,
|
||||
dir: process.cwd(),
|
||||
rootProjectManifestDir: process.cwd(),
|
||||
}, ['linked-with-empty-peer-deps'])
|
||||
|
||||
expect(warnMock).not.toHaveBeenCalledWith(expect.objectContaining({
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { LOCKFILE_VERSION } from '@pnpm/constants'
|
||||
import { filterPackagesFromDir } from '@pnpm/workspace.filter-packages-from-dir'
|
||||
import { install, unlink } from '@pnpm/plugin-commands-installation'
|
||||
import { preparePackages } from '@pnpm/prepare'
|
||||
import { DEFAULT_OPTS } from './utils'
|
||||
|
||||
test('recursive linking/unlinking', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
|
||||
devDependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'is-positive',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const { allProjects, allProjectsGraph, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [])
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
saveWorkspaceProtocol: false,
|
||||
selectedProjectsGraph,
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(projects['is-positive'].requireModule('is-negative')).toBeTruthy()
|
||||
expect(projects['project-1'].requireModule('is-positive/package.json').author).toBeFalsy()
|
||||
|
||||
{
|
||||
const project1Lockfile = projects['project-1'].readLockfile()
|
||||
expect(project1Lockfile.importers['.'].devDependencies?.['is-positive'].version).toBe('link:../is-positive')
|
||||
}
|
||||
|
||||
await unlink.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
saveWorkspaceProtocol: false,
|
||||
selectedProjectsGraph,
|
||||
workspaceDir: process.cwd(),
|
||||
}, [])
|
||||
|
||||
process.chdir('project-1')
|
||||
expect(fs.existsSync(path.resolve('node_modules', 'is-positive', 'index.js'))).toBeTruthy()
|
||||
|
||||
{
|
||||
const project1Lockfile = projects['project-1'].readLockfile()
|
||||
expect(project1Lockfile.lockfileVersion).toBe(LOCKFILE_VERSION)
|
||||
expect(project1Lockfile.importers['.'].devDependencies?.['is-positive'].version).toBe('1.0.0')
|
||||
expect(project1Lockfile.packages['is-positive@1.0.0']).toBeTruthy()
|
||||
}
|
||||
|
||||
const isPositiveLockfile = projects['is-positive'].readLockfile()
|
||||
expect(isPositiveLockfile.lockfileVersion).toBe(LOCKFILE_VERSION)
|
||||
})
|
||||
|
||||
test('recursive unlink specific package', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
|
||||
devDependencies: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'is-positive',
|
||||
version: '1.0.0',
|
||||
|
||||
dependencies: {
|
||||
'is-negative': '1.0.0',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const { allProjects, allProjectsGraph, selectedProjectsGraph } = await filterPackagesFromDir(process.cwd(), [])
|
||||
await install.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
saveWorkspaceProtocol: false,
|
||||
selectedProjectsGraph,
|
||||
workspaceDir: process.cwd(),
|
||||
})
|
||||
|
||||
expect(projects['is-positive'].requireModule('is-negative')).toBeTruthy()
|
||||
expect(projects['project-1'].requireModule('is-positive/package.json').author).toBeFalsy()
|
||||
|
||||
{
|
||||
const project1Lockfile = projects['project-1'].readLockfile()
|
||||
expect(project1Lockfile.importers['.'].devDependencies?.['is-positive'].version).toBe('link:../is-positive')
|
||||
}
|
||||
|
||||
await unlink.handler({
|
||||
...DEFAULT_OPTS,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
dir: process.cwd(),
|
||||
recursive: true,
|
||||
saveWorkspaceProtocol: false,
|
||||
selectedProjectsGraph,
|
||||
workspaceDir: process.cwd(),
|
||||
}, ['is-positive'])
|
||||
|
||||
process.chdir('project-1')
|
||||
expect(fs.existsSync(path.resolve('node_modules', 'is-positive', 'index.js'))).toBeTruthy()
|
||||
|
||||
{
|
||||
const project1Lockfile = projects['project-1'].readLockfile()
|
||||
expect(project1Lockfile.lockfileVersion).toBe(LOCKFILE_VERSION)
|
||||
expect(project1Lockfile.importers['.'].devDependencies?.['is-positive'].version).toBe('1.0.0')
|
||||
expect(project1Lockfile.packages['is-positive@1.0.0']).toBeTruthy()
|
||||
}
|
||||
|
||||
const isPositiveLockfile = projects['is-positive'].readLockfile()
|
||||
expect(isPositiveLockfile.lockfileVersion).toBe(LOCKFILE_VERSION)
|
||||
})
|
||||
@@ -1,9 +1,10 @@
|
||||
import path from 'path'
|
||||
import { add, install, link, prune } from '@pnpm/plugin-commands-installation'
|
||||
import { add, install, prune } from '@pnpm/plugin-commands-installation'
|
||||
import { prepare } from '@pnpm/prepare'
|
||||
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { createTestIpcServer } from '@pnpm/test-ipc-server'
|
||||
import symlinkDir from 'symlink-dir'
|
||||
import fs from 'fs'
|
||||
|
||||
const REGISTRY_URL = `http://localhost:${REGISTRY_MOCK_PORT}`
|
||||
@@ -44,12 +45,7 @@ test('prune removes external link that is not in package.json', async () => {
|
||||
const storeDir = path.resolve('store')
|
||||
f.copy('local-pkg', 'local')
|
||||
|
||||
await link.handler({
|
||||
...DEFAULT_OPTIONS,
|
||||
cacheDir: path.resolve('cache'),
|
||||
dir: process.cwd(),
|
||||
storeDir,
|
||||
}, ['./local'])
|
||||
symlinkDir.sync(path.resolve('local'), path.join('node_modules/local-pkg'))
|
||||
|
||||
project.has('local-pkg')
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@
|
||||
{
|
||||
"path": "../../pkg-manifest/read-project-manifest"
|
||||
},
|
||||
{
|
||||
"path": "../../pkg-manifest/write-project-manifest"
|
||||
},
|
||||
{
|
||||
"path": "../../resolving/resolver-base"
|
||||
},
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -5054,6 +5054,9 @@ importers:
|
||||
'@pnpm/workspace.pkgs-graph':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/pkgs-graph
|
||||
'@pnpm/write-project-manifest':
|
||||
specifier: workspace:*
|
||||
version: link:../../pkg-manifest/write-project-manifest
|
||||
'@yarnpkg/core':
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.5(typanion@3.14.0)
|
||||
@@ -5093,9 +5096,6 @@ importers:
|
||||
p-limit:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.0
|
||||
path-absolute:
|
||||
specifier: 'catalog:'
|
||||
version: 1.0.1
|
||||
ramda:
|
||||
specifier: 'catalog:'
|
||||
version: '@pnpm/ramda@0.28.1'
|
||||
|
||||
@@ -76,47 +76,6 @@ test('incorrect workspace manifest', async () => {
|
||||
expect(status).toBe(1)
|
||||
})
|
||||
|
||||
test('linking a package inside a monorepo', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
name: 'project-1',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-2',
|
||||
version: '2.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-3',
|
||||
version: '3.0.0',
|
||||
},
|
||||
{
|
||||
name: 'project-4',
|
||||
version: '4.0.0',
|
||||
},
|
||||
])
|
||||
|
||||
writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })
|
||||
|
||||
process.chdir('project-1')
|
||||
|
||||
await execPnpm(['link', 'project-2'])
|
||||
|
||||
await execPnpm(['link', 'project-3', '--save-dev'])
|
||||
|
||||
await execPnpm(['link', 'project-4', '--save-optional'])
|
||||
|
||||
const { default: pkg } = await import(path.resolve('package.json'))
|
||||
|
||||
expect(pkg?.dependencies).toStrictEqual({ 'project-2': '^2.0.0' }) // spec of linked package added to dependencies
|
||||
expect(pkg?.devDependencies).toStrictEqual({ 'project-3': '^3.0.0' }) // spec of linked package added to devDependencies
|
||||
expect(pkg?.optionalDependencies).toStrictEqual({ 'project-4': '^4.0.0' }) // spec of linked package added to optionalDependencies
|
||||
|
||||
projects['project-1'].has('project-2')
|
||||
projects['project-1'].has('project-3')
|
||||
projects['project-1'].has('project-4')
|
||||
})
|
||||
|
||||
test('linking a package inside a monorepo with --link-workspace-packages when installing new dependencies', async () => {
|
||||
const projects = preparePackages([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user